Merge branch 'main' of https://github.com/pangeachat/client into remove-duplicate-push

This commit is contained in:
Kelrap 2025-05-20 10:57:20 -04:00
commit ceb1af2631
234 changed files with 11141 additions and 6652 deletions

1
.env
View file

@ -1,3 +1,4 @@
ENVIRONMENT = 'staging'
CHOREO_API = 'https://api.staging.pangea.chat'
FRONTEND_URL='https://app.staging.pangea.chat'
SYNAPSE_URL = 'matrix.staging.pangea.chat'

View file

@ -1,39 +1,39 @@
name: Dart Code Formatter
# name: Dart Code Formatter
on:
pull_request:
push:
branches: main
# on:
# pull_request:
# push:
# branches: main
jobs:
format:
runs-on: ubuntu-latest
# jobs:
# format:
# runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
# with:
# ref: ${{ github.head_ref }}
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
# - uses: subosito/flutter-action@v2
# with:
# flutter-version: ${{ env.FLUTTER_VERSION }}
# cache: true
- name: Auto-format Dart code
run: |
dart format lib/ test/
dart run import_sorter:main --no-comments
if ! git diff --exit-code; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "generated"
git push
fi
# - name: Auto-format Dart code
# run: |
# dart format lib/ test/
# dart run import_sorter:main --no-comments
# if ! git diff --exit-code; then
# git config user.name "github-actions[bot]"
# git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# git add .
# git commit -m "generated"
# git push
# fi
- name: Check for unformatted files
if: ${{ failure() }}
run: |
echo "Code was formatted. Please verify the changes in the PR."
# - name: Check for unformatted files
# if: ${{ failure() }}
# run: |
# echo "Code was formatted. Please verify the changes in the PR."

View file

@ -8,7 +8,8 @@ on:
env:
WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
ENV_OVERRIDES: ${{ vars.ENV_OVERRIDES }}
jobs:
build_web:
runs-on: ubuntu-latest
@ -19,10 +20,6 @@ jobs:
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Remove Emoji Font
run: |
rm -rf fonts/NotoEmoji
yq -i 'del( .flutter.fonts[] | select(.family == "NotoEmoji") )' pubspec.yaml
- run: flutter pub get
- name: Prepare web
run: ./scripts/prepare-web.sh
@ -51,6 +48,8 @@ jobs:
touch public/.env
echo "$WEB_APP_ENV" >> public/.env
cp public/.env public/assets/.env
touch public/assets/envs.json
echo "$ENV_OVERRIDES" >> public/assets/envs.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
@ -82,4 +81,4 @@ jobs:
- name: Update packages
run: flutter pub get
- name: Update sentry
run: flutter packages pub run sentry_dart_plugin
run: flutter packages pub run sentry_dart_plugin

View file

@ -20,6 +20,7 @@ jobs:
name: ${{ inputs.environment }}
env:
WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
ENV_OVERRIDES: ${{ vars.ENV_OVERRIDES }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -45,6 +46,8 @@ jobs:
rm assets/.env
echo "$WEB_APP_ENV" >> .env
cp .env assets/.env
touch assets/envs.json
echo "$ENV_OVERRIDES" >> assets/envs.json
- name: Apply .env patch
run: git apply ./scripts/enable_mobile_env.patch
- name: Install Fastlane

View file

@ -0,0 +1,26 @@
# name: Matrix Notification
# on:
# issues:
# types: [ opened ]
# issue_comment:
# types: [ created ]
# jobs:
# notify:
# runs-on: ubuntu-latest
# steps:
# - name: Send Matrix Notification
# env:
# MATRIX_URL: https://matrix.janian.de/_matrix/client/v3/rooms/${{ secrets.MATRIX_MANAGEMENT_ROOM }}/send/m.room.message
# run: |
# if [ "${{ github.event.action }}" == "opened" ]; then
# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Issue from ${{ github.event.issue.user.login }}\\n${{ github.event.issue.title }}\\n\\n${{ github.event.issue.body }}\\n\\nURL: ${{ github.event.issue.html_url }}\"}"
# elif [ "${{ github.event.action }}" == "created" ]; then
# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Comment from ${{ github.event.comment.user.login }}\\n\\n${{ github.event.comment.body }}\\n\\nURL: ${{ github.event.comment.html_url }}\"}"
# fi
# curl -X POST -H "Authorization: Bearer ${{ secrets.MATRIX_BOT_TOKEN }}" \
# -H "Content-Type: application/json" \
# -d "$PAYLOAD" \
# $MATRIX_URL

View file

@ -48,10 +48,6 @@ jobs:
cache: true
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install nodejs -y
- name: Remove Emoji Font
run: |
rm -rf fonts/NotoEmoji
yq -i 'del( .flutter.fonts[] | select(.family == "NotoEmoji") )' pubspec.yaml
- run: flutter pub get
- name: Prepare web
run: ./scripts/prepare-web.sh

View file

@ -1,2 +1,2 @@
FLUTTER_VERSION=3.27.4
FLUTTER_VERSION=3.29.3
JAVA_VERSION=17

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ keys.json
!/public/.env
*.env.local_choreo
*.env.prod
envs.json
# libolm package
/assets/js/package

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# Privacy
FluffyChat is available on Android, iOS and as a web version. Desktop versions for Windows, Linux and macOS may follow.
FluffyChat is available on Android, iOS, Linux and as a web version. Desktop versions for Windows and macOS may follow.
* [Matrix](#matrix)
* [Database](#database)
@ -109,4 +109,4 @@ To enhance user safety and help protect against the sexual abuse and exploitatio
In addition to reporting messages, users can also report other users following a similar process.
We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/
We encourage server administrators to adhere to strict safety standards and provide mechanisms for addressing and moderating inappropriate content. For more information on the Matrix protocol and its safety standards, please refer to the following link: https://matrix.org/docs/older/moderation/

View file

@ -41,8 +41,6 @@
* Also thanks to all translators and testers! With your help, fluffychat is now available in more than 12 languages.
* <a href="https://github.com/googlefonts/noto-emoji/">Noto Emoji Font</a> for the awesome emojis.
* <a href="https://github.com/madsrh/WoodenBeaver">WoodenBeaver</a> sound theme for the notification sound.
* The Matrix Foundation for making and maintaining the [emoji translations](https://github.com/matrix-org/matrix-spec/blob/main/data-definitions/sas-emoji.json) used for emoji verification, licensed Apache 2.0

View file

@ -1,6 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.talktolearn.chat" android:installLocation="auto">
android:installLocation="auto">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
@ -42,7 +42,7 @@
>
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"

View file

@ -2,17 +2,9 @@
import com.famedly.fcm_shared_isolate.FcmSharedIsolateService
import chat.fluffy.fluffychat.MainActivity
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.view.FlutterMain
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
class FcmPushService : FcmSharedIsolateService() {
override fun getEngine(): FlutterEngine {

View file

@ -45,6 +45,7 @@
<locale android:name="sr"/>
<locale android:name="sv"/>
<locale android:name="ta"/>
<locale android:name="te"/>
<locale android:name="th"/>
<locale android:name="tr"/>
<locale android:name="uk"/>

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dists

View file

@ -1,3 +1,4 @@
ENVIRONMENT = 'staging'
CHOREO_API = 'https://api.staging.pangea.chat'
FRONTEND_URL = 'https://app.staging.pangea.chat'

View file

@ -205,7 +205,7 @@
}
}
},
"changedTheChatDescriptionTo": "{username} hat die Chat-Beschreibung geändert zu: \"{description}\"",
"changedTheChatDescriptionTo": "{username} hat die Chatbeschreibung geändert in: '{description}'",
"@changedTheChatDescriptionTo": {
"type": "String",
"placeholders": {
@ -217,7 +217,7 @@
}
}
},
"changedTheChatNameTo": "{username} hat den Chat-Namen geändert zu: \"{chatname}\"",
"changedTheChatNameTo": "{username} hat den Chatnamen geändert in: '{chatname}'",
"@changedTheChatNameTo": {
"type": "String",
"placeholders": {
@ -238,7 +238,7 @@
}
}
},
"changedTheDisplaynameTo": "{username} hat den Nicknamen geändert zu: \"{displayname}\"",
"changedTheDisplaynameTo": "{username} hat den Spitznamen geändert in: '{displayname}'",
"@changedTheDisplaynameTo": {
"type": "String",
"placeholders": {
@ -3247,5 +3247,47 @@
"notificationRuleSuppressNoticesDescription": "Unterdrückt Benachrichtigungen von automatisierten Clients wie Bots.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleInviteForMe": "Einladung für mich",
"@notificationRuleInviteForMe": {}
"@notificationRuleInviteForMe": {},
"notificationRuleReaction": "Reaktion",
"@notificationRuleReaction": {},
"notificationRuleReactionDescription": "Unterdrückt Benachrichtigungen für Reaktionen.",
"@notificationRuleReactionDescription": {},
"notificationRuleSuppressEditsDescription": "Unterdrückt Benachrichtigungen für bearbeitete Nachrichten.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleCall": "Anruf",
"@notificationRuleCall": {},
"notificationRuleCallDescription": "Benachrichtigt den Benutzer über Anrufe.",
"@notificationRuleCallDescription": {},
"notificationRuleEncrypted": "Verschlüsselt",
"@notificationRuleEncrypted": {},
"more": "Mehr",
"@more": {},
"notificationRuleSuppressEdits": "Unterdrückt Bearbeitungen",
"@notificationRuleSuppressEdits": {},
"notificationRuleRoomServerAclDescription": "Unterdrückt Benachrichtigungen für Raumserver-Zugriffskontrolllisten (ACL).",
"@notificationRuleRoomServerAclDescription": {},
"notificationRuleMessage": "Nachricht",
"@notificationRuleMessage": {},
"notificationRuleMessageDescription": "Informiert den Benutzer über allgemeine Nachrichten.",
"@notificationRuleMessageDescription": {},
"notificationRuleJitsi": "Jitsi",
"@notificationRuleJitsi": {},
"allDevices": "Alle Geräte",
"@allDevices": {},
"enterNewChat": "Neuen Chat starten",
"@enterNewChat": {},
"shareKeysWith": "Schlüssel teilen mit...",
"@shareKeysWith": {},
"shareKeysWithDescription": "Welchen Geräten sollte vertraut werden, damit sie deine Nachrichten in verschlüsselten Chats mitlesen können?",
"@shareKeysWithDescription": {},
"verifiedDevicesOnly": "Nur verifizierte Geräte",
"@verifiedDevicesOnly": {},
"takeAPhoto": "Foto aufnehmen",
"@takeAPhoto": {},
"recordAVideo": "Video aufnehmen",
"@recordAVideo": {},
"optionalMessage": "(Optionale) Nachricht...",
"@optionalMessage": {},
"notSupportedOnThisDevice": "Nicht unterstützt auf diesem Gerät",
"@notSupportedOnThisDevice": {}
}

View file

@ -9,6 +9,10 @@
"@repeatPassword": {},
"notAnImage": "Not an image file.",
"@notAnImage": {},
"setCustomPermissionLevel": "Set custom permission level",
"setPermissionsLevelDescription": "Please choose a predefined role below or enter a custom permission level between 0 and 100.",
"ignoreUser": "Ignore user",
"normalUser": "Normal user",
"remove": "Remove",
"@remove": {
"type": "String",
@ -102,6 +106,7 @@
"type": "String",
"placeholders": {}
},
"commandHint_roomupgrade": "Upgrade this room to the given room version",
"commandHint_googly": "Send some googly eyes",
"@commandHint_googly": {},
"commandHint_cuddle": "Send a cuddle",
@ -3206,6 +3211,7 @@
"recordAVideo": "Record a video",
"optionalMessage": "(Optional) message...",
"notSupportedOnThisDevice": "Not supported on this device",
"enterNewChat": "Enter new chat",
"accountInformation": "Account information",
"addGroupDescription": "Add a chat description",
"addNewFriend": "Add new friend",
@ -4863,8 +4869,8 @@
"emptyChatWarningDesc": "You haven't invited anyone to your chat. Go to Chat settings to invite your contacts or the Bot. You can also do this later.",
"areYouLikeMe": "Are you like me?",
"tryAgainLater": "Too many attempts made. Please try again in 5 minutes.",
"enterSpaceCode": "Enter the Space Code",
"shareSpaceLink": "Share link to space",
"enterSpaceCode": "Enter space code",
"shareSpaceLink": "Share link",
"byUsingPangeaChat": "By using Pangea Chat, I agree to the ",
"details": "Details",
"languageLevelPreA1Desc": "I have never learned or used the language.",
@ -4896,5 +4902,46 @@
},
"ban": "Ban",
"unban": "Unban",
"kick": "Kick"
}
"kick": "Kick",
"approve": "Approve",
"youHaveKnocked": "You have knocked",
"pleaseWaitUntilInvited": "Please wait now, until someone from the room invites you.",
"lemma": "Lemma",
"grammarFeature": "Grammar feature",
"grammarTag": "Grammar tag",
"forms": "Forms",
"exampleMessages": "Example messages",
"timesUsedIndependently": "Times used independently",
"timesUsedWithAssistance": "Times used with assistance",
"goToSpaceButton": "Go to space",
"shareInviteCode": "Share invite code: {code}",
"@shareInviteCode": {
"placeholders": {
"code": {
"type": "String"
}
}
},
"leaderboard": "Leaderboard",
"welcomeUser": "Welcome {user}",
"@welcomeUser": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"joinSpaceOnboardingDesc": "Do you have an invite code or link to a learning community?",
"skipForNow": "Skip for now",
"permissions": "Permissions",
"spaceChildPermission": "Who can add new chats and subspaces to this space",
"addEnvironmentOverride": "Add environment override",
"defaultOption": "Default",
"chatWithActivities": "Chat with activities",
"findYourPeople": "Find your people",
"launch": "Launch",
"launchActivityToChats": "Launch activity to chats",
"searchChats": "Search chats",
"selectChats": "Select chats",
"selectChatToStart": "Complete! Select a chat to start"
}

View file

@ -11,7 +11,7 @@
"type": "String",
"placeholders": {}
},
"acceptedTheInvitation": "{username} aceptó la invitación",
"acceptedTheInvitation": "👍 {username} aceptó la invitación",
"@acceptedTheInvitation": {
"type": "String",
"placeholders": {
@ -68,7 +68,7 @@
"type": "String",
"placeholders": {}
},
"areGuestsAllowedToJoin": "¿Pueden unirse los usuarios visitantes?",
"areGuestsAllowedToJoin": "¿Pueden unirse usuarios de visita?",
"@areGuestsAllowedToJoin": {
"type": "String",
"placeholders": {}
@ -1943,7 +1943,7 @@
},
"scanQrCode": "Escanear código QR",
"@scanQrCode": {},
"homeserver": "Homeserver",
"homeserver": "Servidor inicial",
"@homeserver": {},
"newChat": "Nuevo chat",
"@newChat": {
@ -2193,7 +2193,7 @@
"@youAcceptedTheInvitation": {},
"widgetEtherpad": "Nota de texto",
"@widgetEtherpad": {},
"commandHint_cuddle": "Mandar una carantoña",
"commandHint_cuddle": "Enviar un abrazo",
"@commandHint_cuddle": {},
"supposedMxid": "Esto debería ser {mxid}",
"@supposedMxid": {
@ -2206,7 +2206,7 @@
},
"importFromZipFile": "Importar de un archivo .zip",
"@importFromZipFile": {},
"exportEmotePack": "Exportar paquete de emotes como .zip",
"exportEmotePack": "Exportar paquete de emotes a .zip",
"@exportEmotePack": {},
"addChatDescription": "Añadir una descripción del chat...",
"@addChatDescription": {},
@ -2320,7 +2320,7 @@
"@enterSpace": {},
"pleaseEnterRecoveryKey": "Por favor, introduzca su clave de recuperación:",
"@pleaseEnterRecoveryKey": {},
"widgetNameError": "Por favor, introduzca un nombre a mostrar.",
"widgetNameError": "Por favor proporciona un nombre para mostrar.",
"@widgetNameError": {},
"addWidget": "Añadir widget",
"@addWidget": {},
@ -3337,6 +3337,16 @@
"@previous": {},
"otherPartyNotLoggedIn": "La otra parte ahora mismo no está conectada y por tanto ¡no puede recibir mensajes!",
"@otherPartyNotLoggedIn": {},
"takeAPhoto": "Tomar foto",
"@takeAPhoto": {},
"recordAVideo": "Grabar video",
"@recordAVideo": {},
"optionalMessage": "(Opcional) mensaje...",
"@optionalMessage": {},
"notSupportedOnThisDevice": "No es compatible con este dispositivo",
"@notSupportedOnThisDevice": {},
"enterNewChat": "Ingresar a nuevo chat",
"@enterNewChat": {},
"accountInformation": "Información de la cuenta",
"addGroupDescription": "Agregar una descripción al grupo",
"alreadyHaveAnAccount": "¿Ya tiene una cuenta?",
@ -5449,4 +5459,4 @@
},
"downloadGboard": "Descargar Gboard",
"autocorrectNotAvailable": "Desafortunadamente, tu plataforma no es compatible actualmente con esta función. ¡Mantente atento a futuros desarrollos!"
}
}

View file

@ -3341,5 +3341,9 @@
"optionalMessage": "Sõnum (kui soovid lisada)...",
"@optionalMessage": {},
"notSupportedOnThisDevice": "See pole antud seadmes toetatud",
"@notSupportedOnThisDevice": {}
"@notSupportedOnThisDevice": {},
"enterNewChat": "Liitu uue vestlusega",
"@enterNewChat": {},
"commandHint_roomupgrade": "Uuenda see jututuba antud jututoa versioonini",
"@commandHint_roomupgrade": {}
}

View file

@ -3218,7 +3218,7 @@
"@newChatRequest": {},
"contentNotificationSettings": "Edukiaren jakinarazpenen ezarpenak",
"@contentNotificationSettings": {},
"notificationRuleContainsUserNameDescription": "Jakinarazten du mezuan erabiltzaile-izena aipatzen denean.",
"notificationRuleContainsUserNameDescription": "Mezuan erabiltzaile-izena aipatzen denean jakinarazten du.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMasterDescription": "Gainerako arauak gainidatzi eta jakinarazpenak ezgaitzen ditu.",
"@notificationRuleMasterDescription": {},
@ -3240,5 +3240,104 @@
}
},
"notificationRuleInviteForMe": "Gonbidapena niretzat",
"@notificationRuleInviteForMe": {}
"@notificationRuleInviteForMe": {},
"notificationRuleInviteForMeDescription": "Erabiltzailea gela batera gonbidatzen dutenean jakinarazten du.",
"@notificationRuleInviteForMeDescription": {},
"notificationRuleSuppressNotices": "Ezkutatu mezu automatikoak",
"@notificationRuleSuppressNotices": {},
"notificationRuleSuppressNoticesDescription": "BOTen eta bestelako bezero automatikoen jakinarazpenak ezkutatzen ditu.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleMemberEvent": "Kideen gertaera",
"@notificationRuleMemberEvent": {},
"notificationRuleMemberEventDescription": "Kideen gertaeren jakinarazpenak ezkutatzen ditu.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleIsUserMention": "Erabiltzailea aipatzea",
"@notificationRuleIsUserMention": {},
"notificationRuleContainsDisplayName": "Pantaila-izena dauka",
"@notificationRuleContainsDisplayName": {},
"notificationRuleIsUserMentionDescription": "Erabiltzailea mezu zuzen batean aipatzen dutenean jakinarazten du.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayNameDescription": "Mezu batek erabiltzailearen pantaila-izena duenean jakinarazten du.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsRoomMention": "Gelaren aipamena",
"@notificationRuleIsRoomMention": {},
"notificationRuleIsRoomMentionDescription": "Gela aipatzen denean erabiltzailea jakinarazten du.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleRoomnotif": "Gelaren jakinarazpena",
"@notificationRuleRoomnotif": {},
"notificationRuleRoomnotifDescription": "Mezu batek '@room' duenean erabiltzaileari jakinarazten dio.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleTombstone": "Hilarria",
"@notificationRuleTombstone": {},
"notificationRuleReaction": "Erreakzioa",
"@notificationRuleReaction": {},
"notificationRuleReactionDescription": "Erreakzioen jakinarazpenak ezkutatzen ditu.",
"@notificationRuleReactionDescription": {},
"notificationRuleTombstoneDescription": "Gela desaktibatzeko mezuei buruz jakinarazten dio erabiltzaileari.",
"@notificationRuleTombstoneDescription": {},
"notificationRuleRoomServerAcl": "Gelaren zerbitzariaren ACLa",
"@notificationRuleRoomServerAcl": {},
"notificationRuleSuppressEdits": "Ezkutatu edizioak",
"@notificationRuleSuppressEdits": {},
"notificationRuleCall": "Deia",
"@notificationRuleCall": {},
"notificationRuleEncryptedRoomOneToOne": "Zifratutako bien arteko gela",
"@notificationRuleEncryptedRoomOneToOne": {},
"notificationRuleSuppressEditsDescription": "Editatutako mezuen jakinarazpenak ezkutatzen ditu.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleCallDescription": "Erabiltzaileari deiei buruz jakinarazten dio.",
"@notificationRuleCallDescription": {},
"notificationRuleEncryptedRoomOneToOneDescription": "Erabiltzailea jakinarazten du zifratutako bien arteko geletako mezuei buruz.",
"@notificationRuleEncryptedRoomOneToOneDescription": {},
"notificationRuleRoomOneToOne": "Bien arteko gela",
"@notificationRuleRoomOneToOne": {},
"notificationRuleRoomOneToOneDescription": "Erabiltzailea jakinarazten du bien arteko geletako mezuei buruz.",
"@notificationRuleRoomOneToOneDescription": {},
"notificationRuleMessage": "Mezua",
"@notificationRuleMessage": {},
"notificationRuleMessageDescription": "Erabiltzailea jakinarazten du mezu orokorrei buruz.",
"@notificationRuleMessageDescription": {},
"notificationRuleEncrypted": "Zifratuak",
"@notificationRuleEncrypted": {},
"notificationRuleEncryptedDescription": "Erabiltzailea jakinarazten du zifratutako geletako mezuei buruz.",
"@notificationRuleEncryptedDescription": {},
"notificationRuleJitsi": "Jitsi",
"@notificationRuleJitsi": {},
"notificationRuleJitsiDescription": "Erabiltzailea jakinarazten du Jitsi widgetaren gertaerei buruz.",
"@notificationRuleJitsiDescription": {},
"notificationRuleServerAcl": "Ezkutatu zerbitzariaren ACL gertaerak",
"@notificationRuleServerAcl": {},
"notificationRuleServerAclDescription": "Zerbitzariaren ACL gertaerak ezkutatzen ditu.",
"@notificationRuleServerAclDescription": {},
"unknownPushRule": "Push arau ezezaguna '{rule}'",
"@unknownPushRule": {
"type": "String",
"placeholders": {
"rule": {
"type": "String"
}
}
},
"deletePushRuleCanNotBeUndone": "Jakinarazpen ezarpen hau ezabatzen baduzu, ezin da desegin.",
"@deletePushRuleCanNotBeUndone": {},
"shareKeysWith": "Partekatu gakoak…",
"@shareKeysWith": {},
"allDevices": "Gailu guztiekin",
"@allDevices": {},
"shareKeysWithDescription": "Zein gailu hartu beharko litzateke fidagarritzat zifratutako txaten mezuak irakur ditzaten?",
"@shareKeysWithDescription": {},
"crossVerifiedDevicesIfEnabled": "Egiaztapen gurutzatuko gailuekin, gaituta badaude",
"@crossVerifiedDevicesIfEnabled": {},
"verifiedDevicesOnly": "Egiaztatutako gailuekin soilik",
"@verifiedDevicesOnly": {},
"crossVerifiedDevices": "Egiaztapen gurutzatuko gailuekin",
"@crossVerifiedDevices": {},
"takeAPhoto": "Egin argazkia",
"@takeAPhoto": {},
"recordAVideo": "Grabatu bideoa",
"@recordAVideo": {},
"optionalMessage": "Mezua (aukerakoa)…",
"@optionalMessage": {},
"notSupportedOnThisDevice": "Ez da gailu honekin bateragarria",
"@notSupportedOnThisDevice": {}
}

View file

@ -908,7 +908,7 @@
}
}
},
"defaultPermissionLevel": "Default na antas ng pahintulot",
"defaultPermissionLevel": "Default na antas ng pahintulot para sa mga bagong user",
"@defaultPermissionLevel": {
"type": "String",
"placeholders": {}
@ -962,5 +962,102 @@
"@fontSize": {
"type": "String",
"placeholders": {}
},
"noChatsFoundHere": "Walang pang mga chat na nahanap dito. Magsimula ng bagong chat kasama ang isang tao sa pamamagitan ng paggamit ng button sa ibaba. ⤵️",
"@noChatsFoundHere": {},
"aboutHomeserver": "Tungkol sa {homeserver}",
"@aboutHomeserver": {
"type": "String",
"placeholders": {
"homeserver": {
"type": "String"
}
}
},
"space": "Espasyo",
"@space": {},
"countChatsAndCountParticipants": "{chats} mga chat at {participants} mga kasali",
"@countChatsAndCountParticipants": {
"type": "String",
"placeholders": {
"chats": {
"type": "int"
},
"participants": {
"type": "int"
}
}
},
"guestsAreForbidden": "Pinagbabawal ang mga bisita",
"@guestsAreForbidden": {
"type": "String",
"placeholders": {}
},
"guestsCanJoin": "Maaring sumali ang mga bisita",
"@guestsCanJoin": {
"type": "String",
"placeholders": {}
},
"forward": "I-forward",
"@forward": {
"type": "String",
"placeholders": {}
},
"fromJoining": "Mula sa pagsali",
"@fromJoining": {
"type": "String",
"placeholders": {}
},
"fromTheInvitation": "Mula sa imbitasyon",
"@fromTheInvitation": {
"type": "String",
"placeholders": {}
},
"goToTheNewRoom": "Pumunta sa bagong room",
"@goToTheNewRoom": {
"type": "String",
"placeholders": {}
},
"group": "Grupo",
"@group": {
"type": "String",
"placeholders": {}
},
"swipeRightToLeftToReply": "Mag-swipe pakaliwa o kanan para tumugon",
"@swipeRightToLeftToReply": {},
"noMoreChatsFound": "Wala nang mga chat na nahanap…",
"@noMoreChatsFound": {},
"joinedChats": "Mga nasaling chat",
"@joinedChats": {},
"unread": "Hindi nabasa",
"@unread": {},
"spaces": "Mga Espasyo",
"@spaces": {},
"groupIsPublic": "Pampubliko ang grupo",
"@groupIsPublic": {
"type": "String",
"placeholders": {}
},
"groups": "Mga grupo",
"@groups": {
"type": "String",
"placeholders": {}
},
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"chatDescription": "Paglalarawan ng chat",
"@chatDescription": {},
"chatDescriptionHasBeenChanged": "Nabago ang paglalarawan ng chat",
"@chatDescriptionHasBeenChanged": {},
"groupWith": "Grupo kasama kay/sa {displayname}",
"@groupWith": {
"type": "String",
"placeholders": {
"displayname": {
"type": "String"
}
}
}
}

View file

@ -3091,5 +3091,7 @@
"type": "String"
}
}
}
},
"loginWithMatrixId": "Connexion avec l'identifiant Matrix",
"@loginWithMatrixId": {}
}

View file

@ -3346,5 +3346,9 @@
"notSupportedOnThisDevice": "Ní thacaítear leis ar an ngléas seo",
"@notSupportedOnThisDevice": {},
"optionalMessage": "Teachtaireacht (Roghnach)…",
"@optionalMessage": {}
"@optionalMessage": {},
"enterNewChat": "Cuir isteach comhrá nua",
"@enterNewChat": {},
"commandHint_roomupgrade": "Uasghrádaigh an seomra seo go dtí an leagan seomra a thugtar",
"@commandHint_roomupgrade": {}
}

View file

@ -3341,5 +3341,9 @@
"takeAPhoto": "Facer foto",
"@takeAPhoto": {},
"recordAVideo": "Gravar vídeo",
"@recordAVideo": {}
"@recordAVideo": {},
"enterNewChat": "Entrar na nova conversa",
"@enterNewChat": {},
"commandHint_roomupgrade": "Actualizar esta sala á versión de sala indicada",
"@commandHint_roomupgrade": {}
}

View file

@ -3332,5 +3332,17 @@
"appWantsToUseForLoginDescription": "Anda memperbolehkan aplikasi dan situs web membagikan informasi tentang Anda.",
"@appWantsToUseForLoginDescription": {},
"open": "Buka",
"@open": {}
"@open": {},
"takeAPhoto": "Ambil foto",
"@takeAPhoto": {},
"recordAVideo": "Rekam video",
"@recordAVideo": {},
"optionalMessage": "Pesan (opsional)...",
"@optionalMessage": {},
"notSupportedOnThisDevice": "Tidak didukung pada perangkat ini",
"@notSupportedOnThisDevice": {},
"enterNewChat": "Masuk ke obrolan baru",
"@enterNewChat": {},
"commandHint_roomupgrade": "Tingkatkan ruangan ini ke versi ruangan yang ditentukan",
"@commandHint_roomupgrade": {}
}

View file

@ -186,7 +186,7 @@
}
}
},
"changedTheChatDescriptionTo": "{username} ha cambiato la descrizione della chat in: «{description}»",
"changedTheChatDescriptionTo": "{username} ha cambiato la descrizione della chat in: '{description}'",
"@changedTheChatDescriptionTo": {
"type": "String",
"placeholders": {
@ -198,7 +198,7 @@
}
}
},
"changedTheChatNameTo": "{username} ha cambiato il nome della discussione in: «{chatname}»",
"changedTheChatNameTo": "{username} ha cambiato il nome della discussione in: '{chatname}'",
"@changedTheChatNameTo": {
"type": "String",
"placeholders": {
@ -303,7 +303,7 @@
}
}
},
"changedTheRoomAliases": "{username} ha cambiato il nome delle stanze",
"changedTheRoomAliases": "{username} ha modificato gli alias della stanza",
"@changedTheRoomAliases": {
"type": "String",
"placeholders": {
@ -1629,7 +1629,7 @@
"type": "String",
"placeholders": {}
},
"unknownEvent": "Evento sconosciuto «{type}»",
"unknownEvent": "Evento sconosciuto '{type}'",
"@unknownEvent": {
"type": "String",
"placeholders": {
@ -3185,5 +3185,152 @@
}
},
"compress": "Comprimere",
"@compress": {}
"@compress": {},
"contentNotificationSettings": "Impostazioni del contenuto di notifica",
"@contentNotificationSettings": {},
"generalNotificationSettings": "Impostazioni di notifica generale",
"@generalNotificationSettings": {},
"roomNotificationSettings": "Impostazioni di notifica della stanza",
"@roomNotificationSettings": {},
"userSpecificNotificationSettings": "Impostazioni di notifica specifiche dell'utente",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "Altre impostazioni di notifica",
"@otherNotificationSettings": {},
"notificationRuleContainsUserName": "Contiene il nome utente",
"@notificationRuleContainsUserName": {},
"notificationRuleContainsUserNameDescription": "Notifica l'utente quando un messaggio contiene il proprio nome utente.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMaster": "Silenzia tutte le notifiche",
"@notificationRuleMaster": {},
"notificationRuleMasterDescription": "Sovrascive tutte le altre regole e disabilita tutte le notifiche.",
"@notificationRuleMasterDescription": {},
"notificationRuleSuppressNotices": "Silenziare i messaggi automatizzati",
"@notificationRuleSuppressNotices": {},
"notificationRuleSuppressNoticesDescription": "Silenzia le notifiche da client automatizzati come i bot.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleInviteForMeDescription": "Notifica l'utente quando è invitato in una stanza.",
"@notificationRuleInviteForMeDescription": {},
"notificationRuleMemberEvent": "Eventi per i membri",
"@notificationRuleMemberEvent": {},
"notificationRuleInviteForMe": "Inviti per me",
"@notificationRuleInviteForMe": {},
"notificationRuleIsUserMentionDescription": "Notifica l'utente quando viene menzionato direttamente in un messaggio.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayNameDescription": "Notifica l'utente quando un messaggio contiene il proprio nome visualizzato.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsRoomMention": "Menzioni della stanza",
"@notificationRuleIsRoomMention": {},
"notificationRuleIsUserMention": "Menzioni dell'utente",
"@notificationRuleIsUserMention": {},
"notificationRuleRoomnotif": "Notifiche della stanza",
"@notificationRuleRoomnotif": {},
"notificationRuleRoomnotifDescription": "Notifica l'utente quando un messaggio contiene '@room'.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleTombstone": "Tombstone",
"@notificationRuleTombstone": {},
"notificationRuleTombstoneDescription": "Notifica all'utente i messaggi di disattivazione della stanza.",
"@notificationRuleTombstoneDescription": {},
"notificationRuleReaction": "Reazioni",
"@notificationRuleReaction": {},
"notificationRuleReactionDescription": "Silenzia le notifiche per le reazioni.",
"@notificationRuleReactionDescription": {},
"notificationRuleRoomServerAcl": "ACL del server della stanza",
"@notificationRuleRoomServerAcl": {},
"notificationRuleRoomServerAclDescription": "Silenzia le notifiche per gli elenchi di controllo degli accessi del server della stanza (ACL).",
"@notificationRuleRoomServerAclDescription": {},
"notificationRuleSuppressEdits": "Silenzia le modifiche",
"@notificationRuleSuppressEdits": {},
"notificationRuleSuppressEditsDescription": "Silenzia le notifiche per i messaggi modificati.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleCallDescription": "Notifica all'utente le chiamate.",
"@notificationRuleCallDescription": {},
"notificationRuleEncryptedRoomOneToOne": "Stanze crittografate One-to-One",
"@notificationRuleEncryptedRoomOneToOne": {},
"notificationRuleRoomOneToOne": "Stanze One-to-One",
"@notificationRuleRoomOneToOne": {},
"notificationRuleRoomOneToOneDescription": "Notifica all'utente i messaggi nelle stanze one-to-one.",
"@notificationRuleRoomOneToOneDescription": {},
"notificationRuleMessage": "Messaggi",
"@notificationRuleMessage": {},
"notificationRuleMessageDescription": "Notifica all'utente i messaggi generali.",
"@notificationRuleMessageDescription": {},
"notificationRuleEncryptedDescription": "Notifica all'utente i messaggi nelle stanze crittografate.",
"@notificationRuleEncryptedDescription": {},
"notificationRuleEncrypted": "Crittografate",
"@notificationRuleEncrypted": {},
"notificationRuleJitsi": "Jitsi",
"@notificationRuleJitsi": {},
"notificationRuleJitsiDescription": "Notifica all'utente gli eventi del widget Jitsi.",
"@notificationRuleJitsiDescription": {},
"notificationRuleServerAcl": "Silenziare gli eventi ACL del server",
"@notificationRuleServerAcl": {},
"notificationRuleServerAclDescription": "Silenzia le notifiche per gli eventi ACL del server.",
"@notificationRuleServerAclDescription": {},
"unknownPushRule": "Regola push '{rule}' sconosciuta",
"@unknownPushRule": {
"type": "String",
"placeholders": {
"rule": {
"type": "String"
}
}
},
"deletePushRuleCanNotBeUndone": "Se si elimina questa impostazione di notifica, questo non può essere annullato.",
"@deletePushRuleCanNotBeUndone": {},
"more": "Di più",
"@more": {},
"newChatRequest": "📩 Nuova richiesta di chat",
"@newChatRequest": {},
"shareKeysWith": "Condividi le chiavi con...",
"@shareKeysWith": {},
"shareKeysWithDescription": "Quali dispositivi dovrebbero essere fidati in modo che possano leggere i tuoi messaggi in chat crittografate?",
"@shareKeysWithDescription": {},
"allDevices": "Tutti i dispositivi",
"@allDevices": {},
"crossVerifiedDevicesIfEnabled": "Verifica incrociata dei dispositivi, se abilitata",
"@crossVerifiedDevicesIfEnabled": {},
"crossVerifiedDevices": "Dispositivi con verifica incrociata",
"@crossVerifiedDevices": {},
"verifiedDevicesOnly": "Solo dispositivi verificati",
"@verifiedDevicesOnly": {},
"appWantsToUseForLogin": "Usa '{server}' per accedere",
"@appWantsToUseForLogin": {
"type": "String",
"placeholders": {
"server": {
"type": "String"
}
}
},
"open": "Apri",
"@open": {},
"appWantsToUseForLoginDescription": "Con la presente consenti all'app e al sito web di condividere informazioni su di te.",
"@appWantsToUseForLoginDescription": {},
"appIntroduction": "FluffyChat ti consente di chattare con i tuoi amici attraverso diverse app di messaggistica. Ulteriori informazioni su https://matrix.org o semplicemente tocca *Continua*.",
"@appIntroduction": {},
"waitingForServer": "In attesa del server...",
"@waitingForServer": {},
"synchronizingPleaseWaitCounter": " Sincronizzazione… ({percentage}%)",
"@synchronizingPleaseWaitCounter": {
"type": "String",
"placeholders": {
"percentage": {
"type": "String"
}
}
},
"notificationRuleMemberEventDescription": "Silenzia le notifiche per gli eventi dei membri.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleContainsDisplayName": "Contiene nome visualizzato",
"@notificationRuleContainsDisplayName": {},
"notificationRuleIsRoomMentionDescription": "Notifica l'utente quando c'è una menzione della stanza.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleCall": "Chiamate",
"@notificationRuleCall": {},
"notificationRuleEncryptedRoomOneToOneDescription": "Notifica all'utente i messaggi in stanze crittografate one-to-one.",
"@notificationRuleEncryptedRoomOneToOneDescription": {},
"previous": "Precedente",
"@previous": {},
"otherPartyNotLoggedIn": "L'altra parte non è attualmente connessa e quindi non può ricevere messaggi!",
"@otherPartyNotLoggedIn": {}
}

View file

@ -108,7 +108,7 @@
"type": "String",
"placeholders": {}
},
"groupIsPublic": "그룹 채팅 공개",
"groupIsPublic": "그룹 채팅 공개",
"@groupIsPublic": {
"type": "String",
"placeholders": {}
@ -204,7 +204,7 @@
"type": "String",
"placeholders": {}
},
"enableEncryption": "암호화 켜기",
"enableEncryption": "암호화 사용",
"@enableEncryption": {
"type": "String",
"placeholders": {}
@ -1501,7 +1501,7 @@
"type": "String",
"placeholders": {}
},
"spaceIsPublic": "스페이스 공개",
"spaceIsPublic": "스페이스 공개",
"@spaceIsPublic": {
"type": "String",
"placeholders": {}
@ -2395,7 +2395,7 @@
}
}
},
"fileIsTooBigForServer": "전송에 실패했습니다. 서버는 {max}가 넘는 파일을 지원하지 않습니다.",
"fileIsTooBigForServer": "전송에 실패했습니다. 서버는 {max}가 넘는 파일을 지원하지 않습니다.",
"@fileIsTooBigForServer": {},
"callingPermissions": "통화 권한",
"@callingPermissions": {},
@ -2628,7 +2628,7 @@
"@hydrateTorLong": {},
"custom": "커스텀",
"@custom": {},
"noBackupWarning": "경고! 채팅 백업을 켜지 않을경우, 당신은 암호화된 메시지에대한 접근권한을 잃을것입니다. 로그아웃 하기 전에 채팅을 백업하는것이 강력히 권장됩니다.",
"noBackupWarning": "경고! 채팅 백업을 켜지 않을경우, 당신은 암호화된 메시지에 대한 접근권한을 잃을것 입니다. 로그아웃 하기 전에 채팅을 백업하는것이 강력히 권장됩니다.",
"@noBackupWarning": {},
"storeInSecureStorageDescription": "이 기기의 보안 스토리지에 복구키를 저장합니다.",
"@storeInSecureStorageDescription": {},
@ -2976,7 +2976,7 @@
"@knockRestricted": {},
"swipeRightToLeftToReply": "오른쪽에서 왼쪽으로 스와이프해서 답장",
"@swipeRightToLeftToReply": {},
"alwaysUse24HourFormat": "아니요",
"alwaysUse24HourFormat": "false",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
@ -3046,13 +3046,13 @@
},
"noChatsFoundHere": "대화가 발견되지 않았습니다. 아래 버튼을 사용하여 새 대화를 시작해 보세요. ⤵️",
"@noChatsFoundHere": {},
"changeTheVisibilityOfChatHistory": "대화 기록 표시 여부 바꾸기",
"changeTheVisibilityOfChatHistory": "채팅 기록 표시 여부 바꾸기",
"@changeTheVisibilityOfChatHistory": {},
"changeTheCanonicalRoomAlias": "메인 공개 대화 주소 변경",
"changeTheCanonicalRoomAlias": "메인 공개 채팅 주소 바꾸기",
"@changeTheCanonicalRoomAlias": {},
"sendCanceled": "전송 최소됨",
"@sendCanceled": {},
"homeserverDescription": "모든 데이터는 이메일 제공자와 마찬가지로 Homeserver(이) 에 저장됩니다. 모든 사람과 여전히 소통할 수 있는 동안 사용하고 싶은 Homeserver(이) 를 선택할 수 있습니다. https://matrix.org에서 자세히 알아보세요.",
"homeserverDescription": "당신의 모든 데이터는 이메일과 흡사하게 당신의 홈서버에 저장됩니다. 당신이 소통하고 싶은 사람들과 다른 서버를 사용해도 무관하니 당신이 원하는 홈서버를 선택해도 됩니다. https://matrix.org에서 자세히 알아보세요.",
"@homeserverDescription": {},
"sendingAttachmentCountOfCount": "첨부파일 {length}개중 {index}번째 전송 중...",
"@sendingAttachmentCountOfCount": {
@ -3075,27 +3075,27 @@
}
}
},
"noContactInformationProvided": "서버가 유효한 연락처 정보를 제공하지 않습니다",
"noContactInformationProvided": "서버가 유효한 연락처 정보를 제공하지 않",
"@noContactInformationProvided": {},
"welcomeText": "안녕하세요 👋 제 이름은 FluffyChat이에요. 당신은 Htpps://matrix.org에 호환되는 어떤 Homeserver에도 가입할 수 있어요. 그리고 아무나 대화하세요! 저는 큰 대화 네트워크랍니다! 😄",
"welcomeText": "안녕하세요 👋 FluffyChat이에요. 당신은 htpps://matrix.org와 호환되는 모든 홈서버를 사용할 수 있어요. 그리고 모두와 대화해보세요. 거대한 분산 대화망이니까요!",
"@welcomeText": {},
"changeGeneralChatSettings": "일반 대화 설정 번경하기",
"changeGeneralChatSettings": "일반 채팅 설정 번경하기",
"@changeGeneralChatSettings": {},
"inviteOtherUsers": "다른 사용자를 이 대화에 초대하기",
"inviteOtherUsers": "다른 사용자를 이 채팅에 초대하기",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "대화 권한 변경하기",
"changeTheChatPermissions": "채팅 권한 바꾸기",
"@changeTheChatPermissions": {},
"calculatingFileSize": "파일 크기 계산 중...",
"@calculatingFileSize": {},
"prepareSendingAttachment": "첨부된 파일 전송 준비 중...",
"@prepareSendingAttachment": {},
"oneOfYourDevicesIsNotVerified": "당신의 기기 중 하나가 인증되지 않았습니다",
"oneOfYourDevicesIsNotVerified": "당신의 기기 중 하나가 인증되지 않았",
"@oneOfYourDevicesIsNotVerified": {},
"noticeChatBackupDeviceVerification": "참고: 모든 기기를 대화 백업에 연결하면 자동으로 인증됩니다.",
"noticeChatBackupDeviceVerification": "참고: 모든 기기에 채팅 백업을 설정하면 자동으로 서로 인증됩니다.",
"@noticeChatBackupDeviceVerification": {},
"opacity": "불투명:",
"@opacity": {},
"setWallpaper": "배경화면 정하기",
"setWallpaper": "배경화면 정하기",
"@setWallpaper": {},
"manageAccount": "계정 관리하기",
"@manageAccount": {},
@ -3108,11 +3108,11 @@
}
}
},
"contactServerAdmin": "서버 관리자 연락하기",
"contactServerAdmin": "서버 관리자에게 연락하기",
"@contactServerAdmin": {},
"contactServerSecurity": "서버 보안 연락하기",
"contactServerSecurity": "서버 보안 관리자에게 연락하기",
"@contactServerSecurity": {},
"supportPage": "페이지 돕기",
"supportPage": "지원 페이지",
"@supportPage": {},
"name": "이름",
"@name": {},
@ -3122,23 +3122,23 @@
"@version": {},
"website": "웹사이트",
"@website": {},
"changeTheDescriptionOfTheGroup": "대화의 설명 바꾸기",
"changeTheDescriptionOfTheGroup": "채팅 설명 바꾸기",
"@changeTheDescriptionOfTheGroup": {},
"sendRoomNotifications": "@room 알림 보내기",
"@sendRoomNotifications": {},
"chatPermissionsDescription": "이 대화에서 특정 작업에 필요한 파워 레벨을 정의합니다. 파워 레벨 0, 50, 100은 일반적으로 사용자, 관리자, 관리자를 나타내지만, 모든 등급이 가능합니다.",
"chatPermissionsDescription": "이 채팅에서 특정 작업에 요구할 권한 레벨을 정의합니다. 권한 레벨 0, 50, 100은 일반적으로 유저, 관리자, 운영자를 나타내지만, 모든 숫자가 가능합니다.",
"@chatPermissionsDescription": {},
"loginWithMatrixId": "Matrix-ID로 로그인",
"@loginWithMatrixId": {},
"discoverHomeservers": "Homeserver 찾아보기",
"discoverHomeservers": "홈서버 찾아보기",
"@discoverHomeservers": {},
"whatIsAHomeserver": "Homeserver(이) 가 무엇인가요?",
"whatIsAHomeserver": "홈서버가 무엇인가요?",
"@whatIsAHomeserver": {},
"doesNotSeemToBeAValidHomeserver": "호환되는 Homeserver(이) 가 아닌 것 같습니다. URL이 올바르게 입력됐나요?",
"doesNotSeemToBeAValidHomeserver": "호환되는 홈서버가 아닌 것 같습니다. URL을 올바르게 입력됐나요?",
"@doesNotSeemToBeAValidHomeserver": {},
"continueText": "계속하기",
"@continueText": {},
"updateInstalled": "🎉 {version} 가 설치되었습니다!",
"updateInstalled": "🎉 {version} 업데이트가 설치되었습니다!",
"@updateInstalled": {
"type": "String",
"placeholders": {
@ -3213,5 +3213,67 @@
}
},
"waitingForServer": "서버를 기다리는중...",
"@waitingForServer": {}
"@waitingForServer": {},
"contentNotificationSettings": "콘텐츠 알림 설정",
"@contentNotificationSettings": {},
"generalNotificationSettings": "일반 알림 설정",
"@generalNotificationSettings": {},
"roomNotificationSettings": "채팅방 알림 설정",
"@roomNotificationSettings": {},
"otherNotificationSettings": "기타 알림 설정",
"@otherNotificationSettings": {},
"notificationRuleContainsUserName": "유저 이름을 포함함",
"@notificationRuleContainsUserName": {},
"notificationRuleMaster": "모든 알림 음소거",
"@notificationRuleMaster": {},
"notificationRuleContainsUserNameDescription": "메시지가 유저의 이름을 포함할때 알림합니다.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMasterDescription": "모든 규칙을 무시하고 모든 알림을 비활성화합니다.",
"@notificationRuleMasterDescription": {},
"notificationRuleSuppressNotices": "자동화된 메시지 무시",
"@notificationRuleSuppressNotices": {},
"notificationRuleSuppressNoticesDescription": "봇을 비롯한 자동화된 메시지로부터 발생하는 알림을 무시합니다.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleInviteForMe": "초대를 받음",
"@notificationRuleInviteForMe": {},
"notificationRuleInviteForMeDescription": "채팅방에 초대받았을 때 알림합니다.",
"@notificationRuleInviteForMeDescription": {},
"notificationRuleMemberEvent": "멤버 이벤트",
"@notificationRuleMemberEvent": {},
"notificationRuleMemberEventDescription": "멤버 이벤트로 발생하는 알림을 무시합니다.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleIsUserMentionDescription": "유저가 메시지에 멘션됐을 때 알림합니다.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayName": "표시 이름을 포함함",
"@notificationRuleContainsDisplayName": {},
"notificationRuleIsUserMention": "유저가 멘션됨",
"@notificationRuleIsUserMention": {},
"notificationRuleContainsDisplayNameDescription": "메시지에 표시 이름이 포함되면 알림합니다.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsRoomMention": "방 멘션",
"@notificationRuleIsRoomMention": {},
"notificationRuleIsRoomMentionDescription": "방 멘션이 있을경우 알림합니다.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleRoomnotif": "방 알림",
"@notificationRuleRoomnotif": {},
"notificationRuleRoomnotifDescription": "메시지가 '@room'을 포함하면 알림합니다.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleTombstone": "비활성화",
"@notificationRuleTombstone": {},
"notificationRuleTombstoneDescription": "채팅방 비활성화 메시지를 알림합니다.",
"@notificationRuleTombstoneDescription": {},
"notificationRuleReaction": "반응",
"@notificationRuleReaction": {},
"notificationRuleReactionDescription": "반응으로 발생하는 알림을 무시합니다.",
"@notificationRuleReactionDescription": {},
"notificationRuleRoomServerAcl": "채팅방 서버 ACL",
"@notificationRuleRoomServerAcl": {},
"notificationRuleRoomServerAclDescription": "채팅방 서버의 접근 권한(ACL)으로부터 오는 알림을 무시합니다.",
"@notificationRuleRoomServerAclDescription": {},
"notificationRuleSuppressEdits": "수정 음소거",
"@notificationRuleSuppressEdits": {},
"notificationRuleSuppressEditsDescription": "수정된 메시지로부터 오는 알림을 무시합니다.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleCall": "전화",
"@notificationRuleCall": {}
}

View file

@ -18,7 +18,7 @@
"type": "String",
"placeholders": {}
},
"passphraseOrKey": "paroles vārdkopa vai atkopšanas atslēga",
"passphraseOrKey": "paroles vārdkopa vai atkopes atslēga",
"@passphraseOrKey": {
"type": "String",
"placeholders": {}
@ -205,7 +205,7 @@
}
}
},
"verifySuccess": "Apliecināšana veiksmīga.",
"verifySuccess": "Apliecināšana bija sekmīga.",
"@verifySuccess": {
"type": "String",
"placeholders": {}
@ -385,7 +385,7 @@
"type": "String",
"placeholders": {}
},
"askSSSSSign": "Lai varētu parakstīt otru cilvēku, lūgums ievadīt savu drošo krātuves paroles vārdkopu vai atkopšanas atslēgu.",
"askSSSSSign": "Lai varētu parakstīt otru cilvēku, lūgums ievadīt savu drošo krātuves paroles vārdkopu vai atkopes atslēgu.",
"@askSSSSSign": {
"type": "String",
"placeholders": {}
@ -624,7 +624,7 @@
"type": "String",
"placeholders": {}
},
"incorrectPassphraseOrKey": "Nepareiza paroles vārdkopa vai atkopšanas atslēga",
"incorrectPassphraseOrKey": "Nepareiza paroles vārdkopa vai atkopes atslēga",
"@incorrectPassphraseOrKey": {
"type": "String",
"placeholders": {}
@ -636,7 +636,7 @@
},
"reopenChat": "Atkārtoti atvērt tērzēšanu",
"@reopenChat": {},
"pleaseEnterRecoveryKey": "Lūgums ievadīt savu atkopšanas atslēgu:",
"pleaseEnterRecoveryKey": "Lūgums ievadīt savu atkopes atslēgu:",
"@pleaseEnterRecoveryKey": {},
"create": "Izveidot",
"@create": {
@ -707,7 +707,7 @@
}
}
},
"noKeyForThisMessage": "Tā var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņo no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanu rezerves kopija.",
"noKeyForThisMessage": "Tā var notikt, ja ziņa tika nosūtīta, pirms pieteicies savā kontā šajā ierīcē.\n\nIr arī iespējams, ka sūtītājs noliedza Tavu ierīci vai kaut kas nogāja greizi ar interneta savienojumu.\n\nVai ziņas ir lasāmas citā sesijā? Tad Tu vari pārsūtīt ziņu no tās. Jādodas uz Iestatījumi > Ierīces un jāpārliecinās, ka ierīces viena otru ir apliecinājušas. Kad nākamreiz atvērsi istabu un abas sesijas būs priekšplānā, atslēgas tiks automātiski pārsūtītas.\n\nVai nevēlies zaudēt atslēgas, kad atsakies vai maini ierīces? Jāpārliecinās, ka iestatījumos ir iespējota tērzēšanu rezerves kopija.",
"@noKeyForThisMessage": {},
"enableEncryptionWarning": "Vairs nebūs iespējams atspējot šifrēšanu. Vai tiešām to darīt?",
"@enableEncryptionWarning": {
@ -751,7 +751,7 @@
"@hydrateTor": {},
"pushNotificationsNotAvailable": "Pašpiegādes paziņojumi nav pieejami",
"@pushNotificationsNotAvailable": {},
"passwordRecovery": "Paroles atjaunošana",
"passwordRecovery": "Paroles atkope",
"@passwordRecovery": {
"type": "String",
"placeholders": {}
@ -786,7 +786,7 @@
}
}
},
"wipeChatBackup": "Notīrīt tērzēšanu rezerves kopiju, lai izveidotu jaunu atkopšanas atslēgu?",
"wipeChatBackup": "Notīrīt tērzēšanu rezerves kopiju, lai izveidotu jaunu atkopes atslēgu?",
"@wipeChatBackup": {
"type": "String",
"placeholders": {}
@ -816,7 +816,7 @@
},
"signInWithPassword": "Pieteikties ar paroli",
"@signInWithPassword": {},
"lastActiveAgo": "Pēdējoreiz redzēts: {localizedTimeShort}",
"lastActiveAgo": "Pēdējoreiz tiešsaistē: {localizedTimeShort}",
"@lastActiveAgo": {
"type": "String",
"placeholders": {
@ -864,7 +864,7 @@
"type": "String",
"placeholders": {}
},
"noEmotesFound": "Netika atrastas emocijas. 😕",
"noEmotesFound": "Netika atrasta neviena emocija. 😕",
"@noEmotesFound": {
"type": "String",
"placeholders": {}
@ -1429,7 +1429,7 @@
"type": "String",
"placeholders": {}
},
"pleaseEnterRecoveryKeyDescription": "Lai atslēgtu savas vecās ziņas, lūgums ievadīt savu atkopšanas atslēgu, kas tika izveidota iepriekšējā sesijā. Atkopšanas atslēga NAV parole.",
"pleaseEnterRecoveryKeyDescription": "Lai atslēgtu savas vecās ziņas, lūgums ievadīt savu atkopes atslēgu, kas tika izveidota iepriekšējā sesijā. Atkopes atslēga NAV parole.",
"@pleaseEnterRecoveryKeyDescription": {},
"guestsAreForbidden": "Viesi nav ļauti",
"@guestsAreForbidden": {
@ -1656,7 +1656,7 @@
"type": "String",
"placeholders": {}
},
"recoveryKey": "Atkopšanas atslēga",
"recoveryKey": "Atkopes atslēga",
"@recoveryKey": {},
"redactMessage": "Labot ziņu",
"@redactMessage": {
@ -1680,7 +1680,7 @@
"type": "String",
"placeholders": {}
},
"chooseAStrongPassword": "Jāizvēlas spēcīga parole",
"chooseAStrongPassword": "Jāizvēlas droša parole",
"@chooseAStrongPassword": {
"type": "String",
"placeholders": {}
@ -2007,7 +2007,7 @@
}
}
},
"noChatDescriptionYet": "Vēl nav izveidots tērzēšanas apraksts.",
"noChatDescriptionYet": "Tērzēšanas apraksts vēl nav izveidots.",
"@noChatDescriptionYet": {},
"defaultPermissionLevel": "Noklusējuma atļauju līmenis jauniem lietotājiem",
"@defaultPermissionLevel": {
@ -2082,7 +2082,7 @@
}
}
},
"commandHint_op": "Iestatīt norādītā lietotāja spēka līmeni (noklusējums: 50)",
"commandHint_op": "Iestatīt norādītā lietotāja pilnvaru līmeni (noklusējums: 50)",
"@commandHint_op": {
"type": "String",
"description": "Usage hint for the command /op"
@ -2214,7 +2214,7 @@
"@shareInviteLink": {},
"commandHint_markasdm": "Atzīmēt kā tiešo ziņu istabu norādītajam Matrix Id",
"@commandHint_markasdm": {},
"recoveryKeyLost": "Pazaudēta atkopšanas atslēga?",
"recoveryKeyLost": "Pazaudēta atkopes atslēga?",
"@recoveryKeyLost": {},
"cuddleContent": "{senderName} samīļo Tevi",
"@cuddleContent": {
@ -2348,7 +2348,7 @@
}
}
},
"chatBackupDescription": "Iepriekšējās ziņas ir aizsargātas ar atkopšanas atslēgu. Lūgums nodrošināt, ka tā netiek pazaudēta.",
"chatBackupDescription": "Iepriekšējās ziņas ir aizsargātas ar atkopes atslēgu. Lūgums nodrošināt, ka tā netiek pazaudēta.",
"@chatBackupDescription": {
"type": "String",
"placeholders": {}
@ -2412,7 +2412,7 @@
"type": "String",
"placeholders": {}
},
"editBlockedServers": "Labot liegtos serveros",
"editBlockedServers": "Labot liegtos serverus",
"@editBlockedServers": {
"type": "String",
"placeholders": {}
@ -2561,7 +2561,7 @@
"type": "String",
"placeholders": {}
},
"storeInSecureStorageDescription": "Glabāt atkopšanas atslēgu šīs ierīces drošajā krātuvē.",
"storeInSecureStorageDescription": "Glabāt atkopes atslēgu šīs ierīces drošajā krātuvē.",
"@storeInSecureStorageDescription": {},
"openChat": "Atvērt tērzēšanu",
"@openChat": {},
@ -2623,7 +2623,7 @@
}
}
},
"appLockDescription": "Aizslēgt lietotni ar PIN kodu, kad tā netiek izmantota",
"appLockDescription": "Aizslēgt lietotni, kad tā netiek izmantota, ar PIN kodu",
"@appLockDescription": {},
"globalChatId": "Vispārējais tērzēšanas Id",
"@globalChatId": {},
@ -2653,13 +2653,13 @@
"@overview": {},
"notifyMeFor": "Paziņot man par",
"@notifyMeFor": {},
"wrongRecoveryKey": "Atvaino... Nešķiet, ka šī būtu pareiza atkopšanas atslēga.",
"wrongRecoveryKey": "Atvaino... Nešķiet, ka šī būtu pareiza atkopes atslēga.",
"@wrongRecoveryKey": {},
"block": "Izslēgt",
"@block": {},
"hideMemberChangesInPublicChats": "Paslēpt dalībnieku izmaiņas publiskajās tērzēšanās",
"@hideMemberChangesInPublicChats": {},
"passwordRecoverySettings": "Paroles atjaunošanas iestatījumi",
"passwordRecoverySettings": "Paroles atkopes iestatījumi",
"@passwordRecoverySettings": {},
"blockedUsers": "Atslēgtie lietotāji",
"@blockedUsers": {},
@ -2932,9 +2932,9 @@
"type": "String",
"count": {}
},
"verifyOtherDeviceDescription": "Kad apliecini citu ierīci, šīs ierīces var apmainīt atslēgas, palielinot vispārējo drošību. 💪 Kad uzsāc apliecināšanu, abās ierīcēs lietotnē parādīsies uznirstošais logs. Tajā būs redzamas dažādas emocijzīmes vai skaitļi, kas jāsalīdzina abās ierīcēs. Vislabāk, ja abas ierīces ir pieejams, pirms tiek uzsākta apliecināšana. 🤳",
"verifyOtherDeviceDescription": "Kad apliecini citu ierīci, šīs ierīces var apmainīt atslēgas, palielinot vispārējo drošību. 💪 Pēc apliecināšanas uzsākšanas abās ierīcēs lietotnē parādīsies uznirstošais logs. Tajā būs redzamas dažādas emocijzīmes vai skaitļi, kas jāsalīdzina abās ierīcēs. Vislabāk, ja abas ierīces ir pieejamas, pirms tiek uzsākta apliecināšana. 🤳",
"@verifyOtherDeviceDescription": {},
"swipeRightToLeftToReply": "Pavilkt pa labi, lai atbildētu",
"swipeRightToLeftToReply": "Pavilkt no labās puses uz kreiso, lai atbildētu",
"@swipeRightToLeftToReply": {},
"searchIn": "Meklēt tērzēšanā \"{chat}\"...",
"@searchIn": {
@ -3052,7 +3052,7 @@
}
}
},
"noMoreChatsFound": "Vairs nav tērzēšanu...",
"noMoreChatsFound": "Vairs netika atrasta neviena tērzēšana...",
"@noMoreChatsFound": {},
"joinedChats": "Tērzēšanas, kurās piedalos",
"@joinedChats": {},
@ -3311,5 +3311,17 @@
"crossVerifiedDevicesIfEnabled": "Savstarpēji apliecinātas ierīces, ja iespējots",
"@crossVerifiedDevicesIfEnabled": {},
"verifiedDevicesOnly": "Tikai apliecinātas ierīces",
"@verifiedDevicesOnly": {}
"@verifiedDevicesOnly": {},
"optionalMessage": "(Pēc izvēles) Ziņojums...",
"@optionalMessage": {},
"takeAPhoto": "Uzņemt attēlu",
"@takeAPhoto": {},
"recordAVideo": "Ierakstīt video",
"@recordAVideo": {},
"notSupportedOnThisDevice": "Šajā ierīcē nav atbalstīts",
"@notSupportedOnThisDevice": {},
"enterNewChat": "Ieiet jaunajā tērzēšanā",
"@enterNewChat": {},
"commandHint_roomupgrade": "Uzlabot šo istabu uz norādīto istabas versiju",
"@commandHint_roomupgrade": {}
}

View file

@ -69,7 +69,7 @@
}
}
},
"anyoneCanJoin": "Iedereen kan deelnemen",
"anyoneCanJoin": "Iedereen kan toetreden",
"@anyoneCanJoin": {
"type": "String",
"placeholders": {}
@ -294,7 +294,7 @@
}
}
},
"changedTheJoinRules": "{username} heeft de deelnameregels gewijzigd",
"changedTheJoinRules": "{username} heeft de toetredingsregels gewijzigd",
"@changedTheJoinRules": {
"type": "String",
"placeholders": {
@ -303,7 +303,7 @@
}
}
},
"changedTheJoinRulesTo": "{username} heeft de deelnameregels gewijzigd in: {joinRules}",
"changedTheJoinRulesTo": "{username} heeft de toetredingsregels gewijzigd in: {joinRules}",
"@changedTheJoinRulesTo": {
"type": "String",
"placeholders": {
@ -352,7 +352,7 @@
"type": "String",
"placeholders": {}
},
"changeTheme": "Stijl veranderen",
"changeTheme": "Je stijl veranderen",
"@changeTheme": {
"type": "String",
"placeholders": {}
@ -426,7 +426,7 @@
"type": "String",
"description": "Usage hint for the command /invite"
},
"commandHint_join": "Deelnemen aan de kamer",
"commandHint_join": "Toetreden tot de vermelde kamer",
"@commandHint_join": {
"type": "String",
"description": "Usage hint for the command /join"
@ -530,7 +530,7 @@
"type": "String",
"placeholders": {}
},
"containsUserName": "Bevat gebruikersnaam",
"containsUserName": "Bevat inlognaam",
"@containsUserName": {
"type": "String",
"placeholders": {}
@ -587,7 +587,7 @@
}
}
},
"createNewSpace": "Nieuwe space",
"createNewSpace": "Maak nieuwe space aan",
"@createNewSpace": {
"type": "String",
"placeholders": {}
@ -844,7 +844,7 @@
"type": "String",
"placeholders": {}
},
"fromJoining": "Vanaf deelname",
"fromJoining": "Vanaf toetreden",
"@fromJoining": {
"type": "String",
"placeholders": {}
@ -1027,7 +1027,7 @@
}
}
},
"joinRoom": "Deelnemen",
"joinRoom": "Toetreden tot de kamer",
"@joinRoom": {
"type": "String",
"placeholders": {}
@ -1208,7 +1208,7 @@
"type": "String",
"placeholders": {}
},
"noGoogleServicesWarning": "Firebase Cloud Messaging lijkt niet beschikbaar op je apparaat. Om nog steeds meldingen te krijgen, adviseren we om ntfy te installeren. Met ntfy of een andere Unified Push provider kun je meldingen ontvangen op een veilige manier. Je kunt ntfy downloaden van de PlayStore of van F-Droid.",
"noGoogleServicesWarning": "Firebase Cloud Messaging lijkt niet beschikbaar op je apparaat. Om nog steeds pushmeldingen te krijgen, adviseren we om ntfy te installeren. Met ntfy of een andere Unified Push provider kun je pushmeldingen ontvangen op een veilige manier. Je kunt ntfy downloaden van de PlayStore of van F-Droid.",
"@noGoogleServicesWarning": {
"type": "String",
"placeholders": {}
@ -1245,7 +1245,7 @@
"type": "String",
"placeholders": {}
},
"notifications": "Notificaties",
"notifications": "Meldingen",
"@notifications": {
"type": "String",
"placeholders": {}
@ -1423,7 +1423,7 @@
"type": "String",
"placeholders": {}
},
"publicRooms": "Publieke Kamers",
"publicRooms": "Openbare kamers",
"@publicRooms": {
"type": "String",
"placeholders": {}
@ -1938,7 +1938,7 @@
}
}
},
"username": "Gebruikersnaam",
"username": "Inlognaam",
"@username": {
"type": "String",
"placeholders": {}
@ -2010,7 +2010,7 @@
"type": "String",
"placeholders": {}
},
"waitingPartnerEmoji": "Wachten tot partner de emoji accepteert …",
"waitingPartnerEmoji": "Wachten tot je partner de emoji accepteert…",
"@waitingPartnerEmoji": {
"type": "String",
"placeholders": {}
@ -2055,7 +2055,7 @@
"type": "String",
"placeholders": {}
},
"withTheseAddressesRecoveryDescription": "Met deze adressen kan je je wachtwoord herstellen.",
"withTheseAddressesRecoveryDescription": "Met deze adressen kun je je wachtwoord herstellen.",
"@withTheseAddressesRecoveryDescription": {
"type": "String",
"placeholders": {}
@ -2243,7 +2243,7 @@
"@widgetVideo": {},
"widgetEtherpad": "Tekstnotitie",
"@widgetEtherpad": {},
"separateChatTypes": "Gescheiden directe chats en groepen",
"separateChatTypes": "Directe chats en groepen los weergeven",
"@separateChatTypes": {
"type": "String",
"placeholders": {}
@ -2405,7 +2405,7 @@
"@otherCallingPermissions": {},
"newGroup": "Nieuwe groep",
"@newGroup": {},
"newSpace": "Nieuwe space",
"newSpace": "Space aanmaken",
"@newSpace": {},
"enterRoom": "Kamer betreden",
"@enterRoom": {},
@ -2431,7 +2431,7 @@
}
}
},
"commandHint_googly": "Wat wiebelogen versturen",
"commandHint_googly": "Wiebel-ogen versturen",
"@commandHint_googly": {},
"commandHint_cuddle": "Een knuffel versturen",
"@commandHint_cuddle": {},
@ -2527,7 +2527,7 @@
"@exportEmotePack": {},
"replace": "Vervang",
"@replace": {},
"report": "rapporteer",
"report": "Rapporteer",
"@report": {},
"reportErrorDescription": "😭 Oh nee. Er is iets misgegaan. Probeer het later nog eens. Als je wilt, kun je de bug rapporteren aan de ontwikkelaars.",
"@reportErrorDescription": {},
@ -2539,11 +2539,11 @@
"@signInWithPassword": {},
"chatPermissions": "Chat toestemmingen",
"@chatPermissions": {},
"chatDescription": "Chatbeschrijving",
"chatDescription": "Chatomschrijving",
"@chatDescription": {},
"chatDescriptionHasBeenChanged": "Chatbeschrijving gewijzigd",
"chatDescriptionHasBeenChanged": "Chatomschrijving gewijzigd",
"@chatDescriptionHasBeenChanged": {},
"noChatDescriptionYet": "Nog geen chatbeschrijving gemaakt.",
"noChatDescriptionYet": "Nog geen chatomschrijving gemaakt.",
"@noChatDescriptionYet": {},
"tryAgain": "Opnieuw proberen",
"@tryAgain": {},
@ -2569,7 +2569,7 @@
"@inviteContactToGroupQuestion": {},
"optionalRedactReason": "(Optioneel) Reden voor aanpassing van dit bericht...",
"@optionalRedactReason": {},
"addChatDescription": "Voeg een chatbeschrijving toe...",
"addChatDescription": "Voeg een chatomschrijving toe...",
"@addChatDescription": {},
"invalidServerName": "Foute servernaam",
"@invalidServerName": {},
@ -2588,7 +2588,7 @@
},
"directChat": "Directe chat",
"@directChat": {},
"setChatDescription": "Chatbeschrijving instellen",
"setChatDescription": "Chatomschrijving instellen",
"@setChatDescription": {},
"setTheme": "Thema instellen:",
"@setTheme": {},
@ -2600,7 +2600,7 @@
"@inviteGroupChat": {},
"invitePrivateChat": "📨 Privé-chat uitnodiging",
"@invitePrivateChat": {},
"emoteKeyboardNoRecents": "Recent-gebruikte emoticons zullen hier verschijnen...",
"emoteKeyboardNoRecents": "Recent gebruikte emoticons zullen hier verschijnen...",
"@emoteKeyboardNoRecents": {
"type": "String",
"placeholders": {}
@ -2622,7 +2622,7 @@
"@removeDevicesDescription": {},
"unbanUserDescription": "De persoon zal weer in staat zijn om de chat te betreden als ze het proberen.",
"@unbanUserDescription": {},
"pushNotificationsNotAvailable": "Meldingen zijn niet beschikbaar",
"pushNotificationsNotAvailable": "Pushmeldingen zijn niet beschikbaar",
"@pushNotificationsNotAvailable": {},
"makeAdminDescription": "Wanneer je deze persoon beheerder maakt kun je dit niet ongedaan maken als jullie dezelfde rechten hebben.",
"@makeAdminDescription": {},
@ -2642,13 +2642,13 @@
"@roomUpgradeDescription": {},
"pleaseEnterANumber": "Vul een getal in groter dan 0",
"@pleaseEnterANumber": {},
"kickUserDescription": "De persoon is verwijderd uit de chat, maar is niet verbannen. In publieke chats kan de persoon op elk moment opnieuw deelnemen.",
"kickUserDescription": "De persoon is verwijderd uit de chat, maar is niet verbannen. In openbare chats kan de persoon op elk moment opnieuw deelnemen.",
"@kickUserDescription": {},
"alwaysUse24HourFormat": "true",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"joinSpace": "Deelname aan space",
"joinSpace": "Toetreden tot de space",
"@joinSpace": {},
"block": "Blokkeren",
"@block": {},
@ -2665,11 +2665,11 @@
"@swipeRightToLeftToReply": {},
"calls": "Gesprekken",
"@calls": {},
"customEmojisAndStickers": "Aangepaste emojis and stickers",
"customEmojisAndStickers": "Aangepaste emoticons en stickers",
"@customEmojisAndStickers": {},
"accessAndVisibilityDescription": "Wie mag meedoen in deze chat en hoe de chat ontdekt kan worden.",
"accessAndVisibilityDescription": "Wie mag toetreden tot deze chat en hoe de chat ontdekt kan worden.",
"@accessAndVisibilityDescription": {},
"customEmojisAndStickersBody": "Voeg toe of deel aangepaste emojis of stickers die gebruikt kunnen worden in elke chat.",
"customEmojisAndStickersBody": "Voeg toe of deel aangepaste emoji's of stickers die gebruikt kunnen worden in elke chat.",
"@customEmojisAndStickersBody": {},
"hideRedactedMessages": "Verberg verwijderde berichten",
"@hideRedactedMessages": {},
@ -2677,7 +2677,7 @@
"@hideRedactedMessagesBody": {},
"hideInvalidOrUnknownMessageFormats": "Verberg ongeldige of onbekende berichtformaten",
"@hideInvalidOrUnknownMessageFormats": {},
"passwordRecoverySettings": "Wachtwoord herstel instellingen",
"passwordRecoverySettings": "Wachtwoordherstel-instellingen",
"@passwordRecoverySettings": {},
"youInvitedToBy": "📩 Je bent uitgenodigd via een link voor:\n{alias}",
"@youInvitedToBy": {
@ -2687,15 +2687,15 @@
}
}
},
"knock": "Kloppen",
"knock": "Klop",
"@knock": {},
"overview": "Overzicht",
"@overview": {},
"hidePresences": "Verberg Status Lijst?",
"hidePresences": "Verberg statuslijst?",
"@hidePresences": {},
"noOneCanJoin": "Niemand kan deelnemen",
"@noOneCanJoin": {},
"yourGlobalUserIdIs": "Je globale gebruikers-ID is: ",
"yourGlobalUserIdIs": "Je Matrix ID is: ",
"@yourGlobalUserIdIs": {},
"appLockDescription": "Vergendel de app wanneer het niet gebruikt wordt met een pincode",
"@appLockDescription": {},
@ -2711,11 +2711,11 @@
}
}
},
"publicSpaces": "Publieke spaces",
"publicSpaces": "Openbare spaces",
"@publicSpaces": {},
"blockUsername": "Negeer gebruikersnaam",
"blockUsername": "Negeer inlognaam",
"@blockUsername": {},
"publicChatAddresses": "Publieke chat adressen",
"publicChatAddresses": "Openbare chat adressen",
"@publicChatAddresses": {},
"createNewAddress": "Creëer nieuw adres",
"@createNewAddress": {},
@ -2733,7 +2733,7 @@
},
"noMoreChatsFound": "Geen chats gevonden...",
"@noMoreChatsFound": {},
"joinedChats": "Deelnemende chats",
"joinedChats": "Chats waaraan je deelneemt",
"@joinedChats": {},
"knocking": "Kloppen",
"@knocking": {},
@ -2741,7 +2741,7 @@
"@space": {},
"spaces": "Spaces",
"@spaces": {},
"unread": "Zet als ongelezen",
"unread": "Ongelezen",
"@unread": {},
"databaseBuildErrorBody": "Het aanmaken van de SQlite database is mislukt. De app probeert nu een traditionele database te gebruiken. Meldt alsjeblieft deze fout aan de ontwikkelaars via deze {url}. De foutmelding is: {error}",
"@databaseBuildErrorBody": {
@ -2757,7 +2757,7 @@
},
"groupName": "Groepsnaam",
"@groupName": {},
"changeGeneralChatSettings": "Wijzig algemene chat instellingen",
"changeGeneralChatSettings": "Algemene chat instellingen wijzigen",
"@changeGeneralChatSettings": {},
"restricted": "Beperkt",
"@restricted": {},
@ -2765,7 +2765,7 @@
"@searchForUsers": {},
"searchMore": "Zoek meer...",
"@searchMore": {},
"noPublicLinkHasBeenCreatedYet": "Publieke link is nog niet gecreëerd",
"noPublicLinkHasBeenCreatedYet": "Openbare link is nog niet gecreëerd",
"@noPublicLinkHasBeenCreatedYet": {},
"groupCanBeFoundViaSearch": "Groep kan gevonden worden via zoeken",
"@groupCanBeFoundViaSearch": {},
@ -2794,12 +2794,12 @@
},
"noDatabaseEncryption": "Database versleuteling is niet ondersteund op dit platform",
"@noDatabaseEncryption": {},
"thereAreCountUsersBlocked": "Nu zijn er {count} personen geblokkeerd.",
"thereAreCountUsersBlocked": "Momenteel zijn er {count} personen geblokkeerd.",
"@thereAreCountUsersBlocked": {
"type": "String",
"count": {}
},
"markAsUnread": "Markeer als ongelezen",
"markAsUnread": "Als ongelezen markeren",
"@markAsUnread": {},
"userLevel": "{level} - Persoon",
"@userLevel": {
@ -2819,7 +2819,7 @@
}
}
},
"adminLevel": "{level} - Administrator",
"adminLevel": "{level} - Beheerder",
"@adminLevel": {
"type": "String",
"placeholders": {
@ -2832,7 +2832,7 @@
"@stickers": {},
"nothingFound": "Niets gevonden...",
"@nothingFound": {},
"gallery": "Gallerij",
"gallery": "Galerij",
"@gallery": {},
"transparent": "Transparant",
"@transparent": {},
@ -2840,11 +2840,11 @@
"@incomingMessages": {},
"discover": "Ontdek",
"@discover": {},
"commandHint_ignore": "Negeer de gegeven matrix ID",
"commandHint_ignore": "Negeer de gegeven Matrix ID",
"@commandHint_ignore": {},
"noChatsFoundHere": "Hier zijn nog geen chats. Begin een nieuwe chat met iemand door op de onderstaande knop te klikken. ⤵️",
"@noChatsFoundHere": {},
"unableToJoinChat": "Kan niet deelnemen aan chat. Misschien heeft de andere partij het gesprek al afgesloten.",
"unableToJoinChat": "Kan niet toetreden tot de chat. Misschien heeft de andere partij het gesprek al afgesloten.",
"@unableToJoinChat": {},
"aboutHomeserver": "Over {homeserver}",
"@aboutHomeserver": {
@ -2893,8 +2893,452 @@
},
"website": "Website",
"@website": {},
"hideMemberChangesInPublicChats": "Verberg persoon veranderingen in publieke chats",
"hideMemberChangesInPublicChats": "Verberg persoon veranderingen in openbare chats",
"@hideMemberChangesInPublicChats": {},
"hideMemberChangesInPublicChatsBody": "Verberg in de tijdlijn van de chat als iemand zich aanmeldt bij een openbare chat of deze verlaat om de leesbaarheid te verbeteren.",
"@hideMemberChangesInPublicChatsBody": {}
"@hideMemberChangesInPublicChatsBody": {},
"startConversation": "Start gesprek",
"@startConversation": {},
"usersMustKnock": "Personen moeten kloppen",
"@usersMustKnock": {},
"noUsersFoundWithQuery": "Helaas kan er geen persoon gevonden worden met \"{query}\". Controleer of je een typfout hebt gemaakt.",
"@noUsersFoundWithQuery": {
"type": "String",
"placeholders": {
"query": {
"type": "String"
}
}
},
"createGroupAndInviteUsers": "Maak groep en nodig personen uit",
"@createGroupAndInviteUsers": {},
"userWouldLikeToChangeTheChat": "{user} wil graag deelnemen aan de chat.",
"@userWouldLikeToChangeTheChat": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"chatCanBeDiscoveredViaSearchOnServer": "Chat kan worden gevonden via een zoekopdracht op {server}",
"@chatCanBeDiscoveredViaSearchOnServer": {
"type": "String",
"placeholders": {
"server": {
"type": "String"
}
}
},
"wrongRecoveryKey": "Helaas... dit lijkt niet de correcte herstelsleutel.",
"@wrongRecoveryKey": {},
"synchronizingPleaseWaitCounter": " Synchroniseren… ({percentage}%)",
"@synchronizingPleaseWaitCounter": {
"type": "String",
"placeholders": {
"percentage": {
"type": "String"
}
}
},
"databaseMigrationTitle": "Database is geoptimaliseerd",
"@databaseMigrationTitle": {},
"databaseMigrationBody": "Een ogenblik. Dit kan even duren.",
"@databaseMigrationBody": {},
"commandHint_sendraw": "Stuur kale json",
"@commandHint_sendraw": {},
"passwordIsWrong": "Je ingevoerde wachtwoord is fout",
"@passwordIsWrong": {},
"newPassword": "Nieuw wachtwoord",
"@newPassword": {},
"pleaseChooseAStrongPassword": "Kies a.j.b. een sterk wachtwoord",
"@pleaseChooseAStrongPassword": {},
"publicLink": "Openbare link",
"@publicLink": {},
"select": "Selecteer",
"@select": {},
"leaveEmptyToClearStatus": "Laat leeg om je status te resetten.",
"@leaveEmptyToClearStatus": {},
"addChatOrSubSpace": "Voeg chat of subspace toe",
"@addChatOrSubSpace": {},
"subspace": "Subspace",
"@subspace": {},
"pleaseEnterYourCurrentPassword": "Vul je huidige wachtwoord in",
"@pleaseEnterYourCurrentPassword": {},
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen",
"@passwordsDoNotMatch": {},
"decline": "Weiger",
"@decline": {},
"thisDevice": "Dit apparaat:",
"@thisDevice": {},
"contentNotificationSettings": "Contentmelding instellingen",
"@contentNotificationSettings": {},
"roomNotificationSettings": "Kamermelding instellingen",
"@roomNotificationSettings": {},
"userSpecificNotificationSettings": "Persoon specifieke melding instellingen",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "Andere melding instellingen",
"@otherNotificationSettings": {},
"notificationRuleContainsUserName": "Bevat naam van persoon",
"@notificationRuleContainsUserName": {},
"notificationRuleContainsUserNameDescription": "Stuurt een melding als een bericht de persoon vermeld.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMaster": "Alle meldingen uitschakelen",
"@notificationRuleMaster": {},
"notificationRuleMasterDescription": "Overschrijf alle andere regels en meldingen uitschakelen.",
"@notificationRuleMasterDescription": {},
"notificationRuleMemberEventDescription": "Meldingen voor kamer-gebeurtenissen uitschakelen.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleIsUserMention": "Persoonvermelding",
"@notificationRuleIsUserMention": {},
"initAppError": "Er is een fout opgetreden bij het laden van de app",
"@initAppError": {},
"requestedKeyVerification": "{sender} vraagt een sleutelverificatie",
"@requestedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"sessionLostBody": "Je sessie is verlopen. Meldt alsjeblieft deze fout aan de ontwikkelaars via deze link {url}. De foutmelding is: {error}",
"@sessionLostBody": {
"type": "String",
"placeholders": {
"url": {
"type": "String"
},
"error": {
"type": "String"
}
}
},
"sendTypingNotificationsDescription": "Andere deelnemers in de chat kunnen zien wanneer je een bericht aan het typen bent.",
"@sendTypingNotificationsDescription": {},
"sendCanceled": "Versturen geannuleerd",
"@sendCanceled": {},
"opacity": "Doorzichtigheid:",
"@opacity": {},
"verifyOtherUserDescription": "Als je een persoon verifieert ben je er zeker van dat je echt met haar contact hebt. 💪\n\nWanneer je een verificatie start ziet de persoon een popup in de app. Hier staat een serie van emoji's of getallen die je met elkaar moet vergelijken.\n\nDe beste manier om dit te doen is in persoon of met een videogesprek. 👭",
"@verifyOtherUserDescription": {},
"changeTheVisibilityOfChatHistory": "Zichtbaarheid van de chat-geschiedenis wijzigen",
"@changeTheVisibilityOfChatHistory": {},
"whatIsAHomeserver": "Wat is een server?",
"@whatIsAHomeserver": {},
"sendRoomNotifications": "@room-meldingen versturen",
"@sendRoomNotifications": {},
"noticeChatBackupDeviceVerification": "Opmerking: Als al je apparaten zijn verbonden met de chat back-up worden ze automatisch geverifieerd.",
"@noticeChatBackupDeviceVerification": {},
"notificationRuleMemberEvent": "Kamer-gebeurtenis",
"@notificationRuleMemberEvent": {},
"notificationRuleSuppressNotices": "Automatische berichten uitschakelen",
"@notificationRuleSuppressNotices": {},
"setWallpaper": "Wallpaper instellen",
"@setWallpaper": {},
"oneOfYourDevicesIsNotVerified": "Een van jouw apparaten is niet geverifieerd",
"@oneOfYourDevicesIsNotVerified": {},
"contactServerAdmin": "Contact opnemen met serverbeheerder",
"@contactServerAdmin": {},
"manageAccount": "Account beheren",
"@manageAccount": {},
"noContactInformationProvided": "Server geeft geen geldige contactinformatie",
"@noContactInformationProvided": {},
"waitingForServer": "Wachten op server...",
"@waitingForServer": {},
"generalNotificationSettings": "Algemene melding instellingen",
"@generalNotificationSettings": {},
"notificationRuleInviteForMeDescription": "Stuur een melding wanneer een persoon wordt uitgenodigd voor een kamer.",
"@notificationRuleInviteForMeDescription": {},
"notificationRuleSuppressNoticesDescription": "Meldingen van automatische accounts zoals bots uitschakelen.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleInviteForMe": "Uitnodiging voor mij",
"@notificationRuleInviteForMe": {},
"inviteOtherUsers": "Personen voor deze chat uitnodigen",
"@inviteOtherUsers": {},
"changeTheChatPermissions": "Chat-rechten wijzigen",
"@changeTheChatPermissions": {},
"changeTheCanonicalRoomAlias": "Standaard openbaar chat-adres wijzigen",
"@changeTheCanonicalRoomAlias": {},
"blur": "Vervaag:",
"@blur": {},
"isReadyForKeyVerification": "{sender} is klaar voor de sleutelverificatie",
"@isReadyForKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"startedKeyVerification": "{sender} start een sleutelverificatie",
"@startedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"acceptedKeyVerification": "{sender} accepteerde de sleutelverificatie",
"@acceptedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"canceledKeyVerification": "{sender} annuleerde de sleutelverificatie",
"@canceledKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"knockRestricted": "Kloppen is beperkt",
"@knockRestricted": {},
"goToSpace": "Ga naar space: {space}",
"@goToSpace": {
"type": "String",
"space": {}
},
"contactServerSecurity": "Contact opnemen met serverbeveiliger",
"@contactServerSecurity": {},
"newChatRequest": "📩 Nieuw chat verzoek",
"@newChatRequest": {},
"updateInstalled": "🎉 Update {version} geïnstalleerd!",
"@updateInstalled": {
"type": "String",
"placeholders": {
"version": {
"type": "String"
}
}
},
"discoverHomeservers": "Ontdek servers",
"@discoverHomeservers": {},
"changelog": "Wijzigingengeschiedenis",
"@changelog": {},
"loginWithMatrixId": "Inloggen met Matrix ID",
"@loginWithMatrixId": {},
"calculatingFileSize": "Bestandsgrootte berekenen...",
"@calculatingFileSize": {},
"sendingAttachment": "Bijlage versturen...",
"@sendingAttachment": {},
"generatingVideoThumbnail": "Video-voorbeeld genereren...",
"@generatingVideoThumbnail": {},
"prepareSendingAttachment": "Bijlage versturen voorbereiden...",
"@prepareSendingAttachment": {},
"compressVideo": "Video comprimeren...",
"@compressVideo": {},
"serverLimitReached": "Server limiet bereikt! Wacht {seconds} seconden...",
"@serverLimitReached": {
"type": "integer",
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"version": "Versie",
"@version": {},
"supportPage": "Supportpagina",
"@supportPage": {},
"serverInformation": "Server-informatie:",
"@serverInformation": {},
"name": "Naam",
"@name": {},
"verifyOtherDeviceDescription": "Een geverifieerd ander apparaat zorgt ervoor dat de apparaten sleutels uitwisselen, wat je beveiliging versterkt. 💪 Als je de verificatie start verschijnt er een popup op beide apparaten. Hier staat een reeks emoji's of getallen die je met elkaar moet vergelijken. Het is handig om beide apparaten bij de hand te hebben voordat je de verificatie start. 🤳",
"@verifyOtherDeviceDescription": {},
"commandHint_unignore": "Herstel de negeerde Matrix ID",
"@commandHint_unignore": {},
"forwardMessageTo": "Bericht doorsturen naar {roomName}?",
"@forwardMessageTo": {
"type": "String",
"placeholders": {
"roomName": {
"type": "String"
}
}
},
"restoreSessionBody": "De app probeert nu je sessie te herstellen van een back-up. Meldt alsjeblieft deze fout aan de ontwikkelaars via deze link {url}. De foutmelding is: {error}",
"@restoreSessionBody": {
"type": "String",
"placeholders": {
"url": {
"type": "String"
},
"error": {
"type": "String"
}
}
},
"sendReadReceipts": "Leesbevestigingen versturen",
"@sendReadReceipts": {},
"formattedMessages": "Opgemaakte berichten",
"@formattedMessages": {},
"chatPermissionsDescription": "Stel het gewenste rechten-niveau in voor bepaalde acties in deze chat. Het rechten-niveau 0, 50 en 100 zijn gebruikelijk voor deelnemer, moderator en beheerder, maar elke verdeling is mogelijk.",
"@chatPermissionsDescription": {},
"changeTheDescriptionOfTheGroup": "Chatomschrijving wijzigen",
"@changeTheDescriptionOfTheGroup": {},
"userRole": "Rol",
"@userRole": {},
"minimumPowerLevel": "{level} is het minimale rechten-niveau.",
"@minimumPowerLevel": {
"type": "String",
"placeholders": {
"level": {
"type": "String"
}
}
},
"sendReadReceiptsDescription": "Andere deelnemers van de chat kunnen zien of je een bericht hebt gelezen.",
"@sendReadReceiptsDescription": {},
"formattedMessagesDescription": "Geef rijke berichtinhoud weer zoals vetgedrukte tekst met markdown.",
"@formattedMessagesDescription": {},
"verifyOtherUser": "🔐 Persoon verifiëren",
"@verifyOtherUser": {},
"verifyOtherDevice": "🔐 Ander apparaat verifiëren",
"@verifyOtherDevice": {},
"doesNotSeemToBeAValidHomeserver": "Dit lijkt geen ondersteunde server. Verkeerde URL?",
"@doesNotSeemToBeAValidHomeserver": {},
"sendingAttachmentCountOfCount": "Bijlage versturen {index} van {length}...",
"@sendingAttachmentCountOfCount": {
"type": "integer",
"placeholders": {
"index": {
"type": "int"
},
"length": {
"type": "int"
}
}
},
"continueText": "Doorgaan",
"@continueText": {},
"welcomeText": "Hallo hallo 👋 Dit is FluffyChat. Je kan inloggen op elke server die werkt met https://matrix.org. En dan chat je met iedereen. Het is een groot decentraal chat-netwerk!",
"@welcomeText": {},
"appWantsToUseForLogin": "Inloggen met '{server}'",
"@appWantsToUseForLogin": {
"type": "String",
"placeholders": {
"server": {
"type": "String"
}
}
},
"appWantsToUseForLoginDescription": "Hierbij sta je toe dat de app en website informatie over je delen.",
"@appWantsToUseForLoginDescription": {},
"open": "Open",
"@open": {},
"appIntroduction": "FluffyChat laat je chatten met je vrienden tussen verschillende chat-netwerken. Lees meer op https://matrix.org of tik *Continue*.",
"@appIntroduction": {},
"completedKeyVerification": "{sender} ronde de sleutelverificatie af",
"@completedKeyVerification": {
"type": "String",
"placeholders": {
"sender": {
"type": "String"
}
}
},
"homeserverDescription": "Al je data is opgeslagen op de server, net als bij een email-leverancier. Je kan kiezen welke server je gebruikt en toch communiceren met iedereen. Lees meer op https://matrix.org.",
"@homeserverDescription": {},
"notificationRuleContainsDisplayName": "Bevat de naam",
"@notificationRuleContainsDisplayName": {},
"notificationRuleIsUserMentionDescription": "Stuur een melding als de persoon direct genoemd wordt in een bericht.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayNameDescription": "Stuur een melding als de persoon genoemd wordt in het bericht.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsRoomMention": "Kamervermelding",
"@notificationRuleIsRoomMention": {},
"notificationRuleIsRoomMentionDescription": "Stuur een melding naar de persoon als er in een kamervermelding is.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleRoomnotif": "Kamermelding",
"@notificationRuleRoomnotif": {},
"notificationRuleRoomnotifDescription": "Stuur een melding naar de persoon wanneer een bericht '@room' bevat.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleTombstone": "Sleutingsbericht",
"@notificationRuleTombstone": {},
"notificationRuleReaction": "Reactie",
"@notificationRuleReaction": {},
"notificationRuleRoomServerAcl": "Kamer Server ACL",
"@notificationRuleRoomServerAcl": {},
"notificationRuleTombstoneDescription": "Stuur een melding naar de persoon over kamersluitingsberichten.",
"@notificationRuleTombstoneDescription": {},
"notificationRuleReactionDescription": "Meldingen voor reacties uitschakelen.",
"@notificationRuleReactionDescription": {},
"notificationRuleRoomServerAclDescription": "Meldingen voor kamer server toegangscontrolelijst (ACL) uitschakelen.",
"@notificationRuleRoomServerAclDescription": {},
"notificationRuleSuppressEdits": "Bewerkingen uitschakelen",
"@notificationRuleSuppressEdits": {},
"notificationRuleCall": "Oproep",
"@notificationRuleCall": {},
"notificationRuleSuppressEditsDescription": "Meldingen voor bewerkte berichten uitschakelen.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleEncryptedRoomOneToOneDescription": "Stuur een melding naar de persoon over berichten in versleutelde een-op-een kamers.",
"@notificationRuleEncryptedRoomOneToOneDescription": {},
"notificationRuleEncryptedRoomOneToOne": "Versleutelde een-op-een kamer",
"@notificationRuleEncryptedRoomOneToOne": {},
"notificationRuleRoomOneToOne": "Een-op-een kamer",
"@notificationRuleRoomOneToOne": {},
"notificationRuleMessage": "Bericht",
"@notificationRuleMessage": {},
"notificationRuleEncrypted": "Versleuteld",
"@notificationRuleEncrypted": {},
"notificationRuleRoomOneToOneDescription": "Stuur een melding naar de persoon over berichten in een-op-een kamers.",
"@notificationRuleRoomOneToOneDescription": {},
"notificationRuleMessageDescription": "Stuur een melding naar de persoon over algemene berichten.",
"@notificationRuleMessageDescription": {},
"notificationRuleJitsi": "Jitsi",
"@notificationRuleJitsi": {},
"notificationRuleEncryptedDescription": "Stuur een melding naar de persoon over berichten in versleutelde kamers.",
"@notificationRuleEncryptedDescription": {},
"notificationRuleJitsiDescription": "Stuur een melding naar de persoon over Jitsi widget gebeurtenissen.",
"@notificationRuleJitsiDescription": {},
"unknownPushRule": "Onbekende notificatieregel '{rule}'",
"@unknownPushRule": {
"type": "String",
"placeholders": {
"rule": {
"type": "String"
}
}
},
"notificationRuleServerAcl": "Server ACL gebeurtenissen uitschakelen",
"@notificationRuleServerAcl": {},
"notificationRuleServerAclDescription": "Meldingen over server ACL gebeurtenissen uitschakelen.",
"@notificationRuleServerAclDescription": {},
"more": "Meer",
"@more": {},
"enterNewChat": "Nieuwe chat openen",
"@enterNewChat": {},
"crossVerifiedDevices": "Kruislings geverifieerde apparaten",
"@crossVerifiedDevices": {},
"allDevices": "Alle apparaten",
"@allDevices": {},
"shareKeysWithDescription": "Welke apparaten moeten vertrouwd worden zodat ze je berichten kunnen lezen in versleutelde chats?",
"@shareKeysWithDescription": {},
"verifiedDevicesOnly": "Alleen geverifieerde apparaten",
"@verifiedDevicesOnly": {},
"crossVerifiedDevicesIfEnabled": "Kruislings geverifieerde apparaten als ingeschakeld",
"@crossVerifiedDevicesIfEnabled": {},
"shareKeysWith": "Deel sleutels met...",
"@shareKeysWith": {},
"notificationRuleCallDescription": "Stuur een melding naar de persoon over oproepen.",
"@notificationRuleCallDescription": {},
"deletePushRuleCanNotBeUndone": "Als je deze melding-instelling verwijderd, kan dit niet ongedaan gemaakt worden.",
"@deletePushRuleCanNotBeUndone": {},
"takeAPhoto": "Neem een foto",
"@takeAPhoto": {},
"recordAVideo": "Neem een video",
"@recordAVideo": {},
"optionalMessage": "(Optioneel) bericht...",
"@optionalMessage": {},
"notSupportedOnThisDevice": "Niet ondersteund op dit apparaat",
"@notSupportedOnThisDevice": {},
"commandHint_roomupgrade": "Upgradeer deze kamer naar de aangegeven kamerversie",
"@commandHint_roomupgrade": {}
}

View file

@ -3258,15 +3258,15 @@
"@notificationRuleCall": {},
"notificationRuleRoomServerAclDescription": "Wyłącza powiadomienia dla list kontroli dostępu (ACL) serwerów pokojów.",
"@notificationRuleRoomServerAclDescription": {},
"notificationRuleRoomServerAcl": "Listo kontroli dostępu serwerów pokojów",
"notificationRuleRoomServerAcl": "Lista kontroli dostępu serwerów pokojów",
"@notificationRuleRoomServerAcl": {},
"notificationRuleEncryptedRoomOneToOne": "Szyfrowane pokoje jeden na jeden",
"notificationRuleEncryptedRoomOneToOne": "Szyfrowane pokoje jeden na jeden",
"@notificationRuleEncryptedRoomOneToOne": {},
"notificationRuleCallDescription": "Powiadamia o przychodzących połączeniach.",
"@notificationRuleCallDescription": {},
"notificationRuleRoomOneToOne": "Pokoje jeden na jeden",
"notificationRuleRoomOneToOne": "Pokoje jeden na jeden",
"@notificationRuleRoomOneToOne": {},
"notificationRuleRoomOneToOneDescription": "Powiadamia o wiadomościach w pokojach jeden na jeden (one-to-one).",
"notificationRuleRoomOneToOneDescription": "Powiadamia o wiadomościach w pokojach jeden na jeden (one-to-one).",
"@notificationRuleRoomOneToOneDescription": {},
"notificationRuleMessage": "Wiadomości",
"@notificationRuleMessage": {},
@ -3279,7 +3279,7 @@
}
}
},
"notificationRuleEncryptedRoomOneToOneDescription": "Powiadamia o wiadomościach w szyfrowanych pokojach jeden na jeden (one-to-one).",
"notificationRuleEncryptedRoomOneToOneDescription": "Powiadamia o wiadomościach w szyfrowanych pokojach jeden na jeden (one-to-one).",
"@notificationRuleEncryptedRoomOneToOneDescription": {},
"notificationRuleEncrypted": "Zaszyfrowane pokoje",
"@notificationRuleEncrypted": {},

View file

@ -2476,7 +2476,7 @@
"@noOtherDevicesFound": {},
"reportErrorDescription": "😭 О, нет. Что-то пошло не так. При желании вы можете сообщить об этой ошибке разработчикам.",
"@reportErrorDescription": {},
"report": "сообщить",
"report": "пожаловаться",
"@report": {},
"allRooms": "Все группы",
"@allRooms": {
@ -3184,5 +3184,91 @@
}
},
"compress": "Сжатие",
"@compress": {}
"@compress": {},
"notificationRuleMessage": "Сообщение",
"@notificationRuleMessage": {},
"appWantsToUseForLoginDescription": "Вы позволяете приложению и веб-сайту делиться информацией о вас.",
"@appWantsToUseForLoginDescription": {},
"newChatRequest": "📩 Запрос нового чата",
"@newChatRequest": {},
"allDevices": "Все устройства",
"@allDevices": {},
"roomNotificationSettings": "Настройки уведомлений комнаты",
"@roomNotificationSettings": {},
"notificationRuleContainsUserName": "Содержит имя пользователя",
"@notificationRuleContainsUserName": {},
"notificationRuleMaster": "Отключить все уведомления",
"@notificationRuleMaster": {},
"notificationRuleSuppressNoticesDescription": "Отключить уведомления от автоматизированных клиентов, таких как боты.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleInviteForMe": "Приглашение для меня",
"@notificationRuleInviteForMe": {},
"notificationRuleMemberEventDescription": "Отключить уведомления о событиях о членстве.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleIsRoomMention": "Упоминание комнаты",
"@notificationRuleIsRoomMention": {},
"notificationRuleReactionDescription": "Отключить уведомления о реакциях.",
"@notificationRuleReactionDescription": {},
"notificationRuleCall": "Звонок",
"@notificationRuleCall": {},
"notificationRuleSuppressEditsDescription": "Отключить уведомления о отредактированных сообщениях.",
"@notificationRuleSuppressEditsDescription": {},
"notificationRuleEncrypted": "Зашифровано",
"@notificationRuleEncrypted": {},
"more": "Больше",
"@more": {},
"synchronizingPleaseWaitCounter": " Синхронизация… ({percentage}%)",
"@synchronizingPleaseWaitCounter": {
"type": "String",
"placeholders": {
"percentage": {
"type": "String"
}
}
},
"appWantsToUseForLogin": "Использовать '{server}' для входа",
"@appWantsToUseForLogin": {
"type": "String",
"placeholders": {
"server": {
"type": "String"
}
}
},
"contentNotificationSettings": "Настройки уведомлений по тексту",
"@contentNotificationSettings": {},
"generalNotificationSettings": "Общие настройки уведомлений",
"@generalNotificationSettings": {},
"userSpecificNotificationSettings": "Настроки уведомлений пользователя",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "Другие настройки уведомлений",
"@otherNotificationSettings": {},
"notificationRuleContainsUserNameDescription": "Уведомляет пользователя когда сообщение содержит его имя пользователя.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMasterDescription": "Перекрывает все другие правила и отключает все уведомления.",
"@notificationRuleMasterDescription": {},
"notificationRuleSuppressNotices": "Отключить автоматические сообщения",
"@notificationRuleSuppressNotices": {},
"notificationRuleIsUserMention": "Упоминание пользователя",
"@notificationRuleIsUserMention": {},
"notificationRuleContainsDisplayName": "Содержит отображаемое имя",
"@notificationRuleContainsDisplayName": {},
"notificationRuleReaction": "Реакция",
"@notificationRuleReaction": {},
"takeAPhoto": "Снять фото",
"@takeAPhoto": {},
"recordAVideo": "Записать видео",
"@recordAVideo": {},
"enterNewChat": "Введите новый чат",
"@enterNewChat": {},
"otherPartyNotLoggedIn": "Другая сторона в настоящее время не вошла в систему и поэтому не может получать сообщения!",
"@otherPartyNotLoggedIn": {},
"open": "Открыть",
"@open": {},
"waitingForServer": "Ожидание сервера...",
"@waitingForServer": {},
"appIntroduction": "FluffyChat позволяет вам общаться с друзьями с разными мессенджерами. Узнайте больше на https://matrix.org или просто нажмите *Продолжить*.",
"@appIntroduction": {},
"previous": "Предыдущий",
"@previous": {}
}

15
assets/l10n/intl_te.arb Normal file
View file

@ -0,0 +1,15 @@
{
"alwaysUse24HourFormat": "తప్పుడు",
"@alwaysUse24HourFormat": {
"description": "Set to true to always display time of day in 24 hour format."
},
"notAnImage": "ఇమేజ్ ఫైల్ కాదు.",
"@notAnImage": {},
"repeatPassword": "పాస్‌వర్డ్‌ను పునరావృతం చేయండి",
"@repeatPassword": {},
"remove": "తొలగించు",
"@remove": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3232,5 +3232,49 @@
"contentNotificationSettings": "Налаштування сповіщень про вміст",
"@contentNotificationSettings": {},
"generalNotificationSettings": "Загальні налаштування сповіщень",
"@generalNotificationSettings": {}
"@generalNotificationSettings": {},
"roomNotificationSettings": "Налаштування сповіщень кімнати",
"@roomNotificationSettings": {},
"userSpecificNotificationSettings": "Налаштування сповіщень для користувача",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "Інші налаштування сповіщень",
"@otherNotificationSettings": {},
"notificationRuleContainsUserName": "Містить ім'я користувача",
"@notificationRuleContainsUserName": {},
"notificationRuleContainsUserNameDescription": "Сповіщає користувача, коли повідомлення містить його ім'я користувача.",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMaster": "Вимкнути всі сповіщення",
"@notificationRuleMaster": {},
"notificationRuleMasterDescription": "Перевизначає всі інші правила і вимикає всі сповіщення.",
"@notificationRuleMasterDescription": {},
"notificationRuleSuppressNotices": "Заборонити автоматичні повідомлення",
"@notificationRuleSuppressNotices": {},
"notificationRuleInviteForMe": "Запрошення мене",
"@notificationRuleInviteForMe": {},
"notificationRuleInviteForMeDescription": "Сповіщає користувача, коли його запрошують до кімнати.",
"@notificationRuleInviteForMeDescription": {},
"notificationRuleMemberEvent": "Події участі",
"@notificationRuleMemberEvent": {},
"notificationRuleMemberEventDescription": "Забороняє сповіщення про події учасників.",
"@notificationRuleMemberEventDescription": {},
"notificationRuleSuppressNoticesDescription": "Забороняє сповіщення від автоматизованих клієнтів, таких як боти.",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleIsUserMention": "Згадки користувачів",
"@notificationRuleIsUserMention": {},
"commandHint_roomupgrade": "Оновити цю кімнату до версії даної кімнати",
"@commandHint_roomupgrade": {},
"notificationRuleIsUserMentionDescription": "Сповіщає користувачів, коли безпосередньо їх згадують у повідомленні.",
"@notificationRuleIsUserMentionDescription": {},
"notificationRuleContainsDisplayName": "Містить показуване ім’я",
"@notificationRuleContainsDisplayName": {},
"notificationRuleIsRoomMentionDescription": "Сповіщає користувача, коли є згадка всієї кімнати.",
"@notificationRuleIsRoomMentionDescription": {},
"notificationRuleRoomnotif": "Сповіщення кімнати",
"@notificationRuleRoomnotif": {},
"notificationRuleRoomnotifDescription": "Сповіщає користувача, коли повідомлення містить '@room'.",
"@notificationRuleRoomnotifDescription": {},
"notificationRuleContainsDisplayNameDescription": "Сповіщає користувача, коли повідомлення містить показуване ім'я.",
"@notificationRuleContainsDisplayNameDescription": {},
"notificationRuleIsRoomMention": "Згадки кімнати",
"@notificationRuleIsRoomMention": {}
}

View file

@ -1,5 +1,5 @@
{
"@@last_modified": "2025-03-10 10:26:28.474786",
"@@last_modified": "2025-05-13 11:20:01.172393",
"about": "Giới thiệu",
"@about": {
"type": "String",
@ -3800,5 +3800,476 @@
"type": "int"
}
}
},
"setCustomPermissionLevel": "Đặt cấp độ quyền tùy chỉnh",
"setPermissionsLevelDescription": "Vui lòng chọn một vai trò đã định nghĩa sẵn bên dưới hoặc nhập một cấp độ quyền tùy chỉnh từ 0 đến 100.",
"ignoreUser": "Bỏ qua người dùng",
"normalUser": "Người dùng bình thường",
"commandHint_roomupgrade": "Nâng cấp phòng này lên phiên bản phòng đã cho",
"enterNewChat": "Tham gia trò chuyện mới",
"completeActivitiesToUnlock": "Hoàn thành ít nhất một hoạt động để mở khóa bản dịch!",
"constructUseGaDesc": "Hỗ trợ ngữ pháp",
"constructUseTaDesc": "Hỗ trợ dịch thuật",
"chooseLemmaMeaningInstructionsBody": "Khớp nghĩa với các từ trong tin nhắn!",
"analyticsVocabListBody": "Đây là toàn bộ từ vựng của bạn! Khi bạn kiếm XP cho mỗi từ, chúng sẽ từ hạt giống phát triển thành nở rộ. Nhấp vào bất kỳ từ nào để xem thêm chi tiết.",
"morphAnalyticsListBody": "Đây là tất cả các khái niệm ngữ pháp trong ngôn ngữ bạn đang học! Bạn sẽ mở khóa chúng khi gặp phải trong khi trò chuyện. Nhấp để xem chi tiết.",
"chooseWordAudioInstructionsBody": "Nghe toàn bộ tin nhắn. Sau đó, khớp các âm thanh với các từ.",
"chooseMorphsInstructionsBody": "Nhấp vào các mảnh ghép cho các câu hỏi ngữ pháp!",
"inviteAndLaunch": "Khởi động và mời",
"createOwnChat": "Tạo trò chuyện của riêng bạn",
"pleaseEnterInt": "Vui lòng nhập một số",
"home": "Trang chủ",
"join": "Tham gia",
"readingAssistanceOverviewBody": "Nhấp vào các nút bên dưới để chơi mini-game về việc khớp emoji, âm thanh, nghĩa từ và khái niệm ngữ pháp. Hoặc nhấp vào bất kỳ từ nào để xem chi tiết.",
"learnByTexting": "Học qua nhắn tin",
"referFriends": "Giới thiệu bạn bè",
"referFriendDialogTitle": "Mời một người bạn tham gia cuộc trò chuyện của bạn",
"referFriendDialogDesc": "Bạn có một người bạn hào hứng học một ngôn ngữ mới cùng bạn không? Hãy sao chép và gửi liên kết mời này để tham gia và bắt đầu trò chuyện với bạn hôm nay.",
"youUnlocked": "Bạn đã mở khóa",
"resetInstructionTooltipsTitle": "Đặt lại hướng dẫn công cụ",
"resetInstructionTooltipsDesc": "Nhấp để hiển thị hướng dẫn công cụ như cho một người dùng hoàn toàn mới.",
"selectForGrammar": "Chọn một biểu tượng ngữ pháp cho các hoạt động và chi tiết.",
"newChatActivityTitle": "Thêm một hoạt động thú vị?",
"newChatActivityDesc": "Biến mỗi cuộc trò chuyện nhóm thành một cuộc phiêu lưu với Kế hoạch Hoạt động! Đặt các chủ đề và mục tiêu hấp dẫn cho nhóm, và mang cuộc trò chuyện đến cuộc sống với những hình ảnh tuyệt đẹp. Khơi dậy những cuộc thảo luận sáng tạo và giữ cho niềm vui chảy trôi một cách dễ dàng!",
"exploreMore": "Khám phá thêm",
"randomize": "Ngẫu nhiên hóa",
"clear": "Xóa",
"makeYourOwnActivity": "Tạo hoạt động của riêng bạn",
"featuredActivities": "Nổi bật",
"yourBookmarks": "Đã đánh dấu",
"goToChat": "Đi đến trò chuyện",
"save": "Lưu",
"selectActivity": "Chọn hoạt động",
"wordFocusListeningMultipleChoice": "Âm thanh nào phù hợp với từ?",
"createActivity": "Tạo hoạt động",
"startChat": "Bắt đầu trò chuyện",
"translationProblem": "Vấn đề dịch thuật",
"perfectTranslation": "Dịch thuật hoàn hảo!",
"greatJobTranslation": "Làm tốt công việc với bản dịch này!",
"goodJobTranslation": "Làm tốt công việc với bản dịch này.",
"makingProgress": "Bạn đang tiến bộ!",
"keepPracticing": "Tiếp tục luyện tập!",
"niceJob": "Làm tốt lắm!",
"publicSpacesTitle": "Cộng đồng học tập",
"askToJoin": "Yêu cầu tham gia",
"emptyChatWarningTitle": "Trò chuyện trống",
"emptyChatWarningDesc": "Bạn chưa mời ai vào trò chuyện của mình. Đi đến cài đặt trò chuyện để mời danh bạ hoặc Bot của bạn. Bạn cũng có thể làm điều này sau.",
"areYouLikeMe": "Bạn có giống tôi không?",
"tryAgainLater": "Đã thực hiện quá nhiều lần. Vui lòng thử lại sau 5 phút.",
"enterSpaceCode": "Nhập mã không gian",
"shareSpaceLink": "Chia sẻ liên kết",
"byUsingPangeaChat": "Bằng cách sử dụng Pangea Chat, tôi đồng ý với ",
"details": "Chi tiết",
"languageLevelPreA1Desc": "Tôi chưa bao giờ học hoặc sử dụng ngôn ngữ này.",
"languageLevelA1Desc": "Tôi có thể hiểu và sử dụng một số biểu thức quen thuộc hàng ngày và các cụm từ rất cơ bản.",
"languageLevelA2Desc": "Tôi có thể hiểu các câu và các biểu thức thường được sử dụng liên quan đến các lĩnh vực có liên quan ngay lập tức.",
"languageLevelB1Desc": "Tôi có thể xử lý hầu hết các tình huống quen thuộc và có thể tạo ra văn bản đơn giản liên kết về các chủ đề quen thuộc.",
"languageLevelB2Desc": "Tôi có thể hiểu các ý chính của các văn bản phức tạp và tương tác với một mức độ lưu loát và tự phát.",
"languageLevelC1Desc": "Tôi có thể diễn đạt ý tưởng một cách lưu loát và tự phát mà không gặp nhiều khó khăn và hiểu một loạt các văn bản đòi hỏi.",
"languageLevelC2Desc": "Tôi có thể hiểu hầu như mọi thứ nghe hoặc đọc và diễn đạt bản thân một cách lưu loát và chính xác.",
"newVocab": "Từ vựng mới",
"newGrammar": "Khái niệm ngữ pháp mới",
"congratulationsOnReaching": "Bạn đã đạt đến Cấp độ {level}!",
"seeDetails": "Xem chi tiết",
"choosePracticeMode": "Nhấp vào một trong các nút ở trên để bắt đầu một hoạt động thực hành",
"userWouldLikeToChangeTheSpace": "{user} muốn tham gia không gian.",
"ban": "Cấm",
"unban": "Bỏ cấm",
"kick": "Đá",
"approve": "Chấp thuận",
"youHaveKnocked": "Bạn đã gõ cửa",
"pleaseWaitUntilInvited": "Vui lòng chờ cho đến khi ai đó trong phòng mời bạn.",
"lemma": "Lemma",
"grammarFeature": "Tính năng ngữ pháp",
"grammarTag": "Thẻ ngữ pháp",
"forms": "Hình thức",
"exampleMessages": "Tin nhắn ví dụ",
"timesUsedIndependently": "Số lần sử dụng độc lập",
"timesUsedWithAssistance": "Số lần sử dụng với sự trợ giúp",
"goToSpaceButton": "Đi đến không gian",
"shareInviteCode": "Chia sẻ mã mời: {code}",
"leaderboard": "Bảng xếp hạng",
"welcomeUser": "Chào mừng {user}",
"joinSpaceOnboardingDesc": "Bạn có mã mời hoặc liên kết đến một cộng đồng học tập không?",
"skipForNow": "Bỏ qua tạm thời",
"@setCustomPermissionLevel": {
"type": "String",
"placeholders": {}
},
"@setPermissionsLevelDescription": {
"type": "String",
"placeholders": {}
},
"@ignoreUser": {
"type": "String",
"placeholders": {}
},
"@normalUser": {
"type": "String",
"placeholders": {}
},
"@commandHint_roomupgrade": {
"type": "String",
"placeholders": {}
},
"@enterNewChat": {
"type": "String",
"placeholders": {}
},
"@completeActivitiesToUnlock": {
"type": "String",
"placeholders": {}
},
"@constructUseGaDesc": {
"type": "String",
"placeholders": {}
},
"@constructUseTaDesc": {
"type": "String",
"placeholders": {}
},
"@chooseLemmaMeaningInstructionsBody": {
"type": "String",
"placeholders": {}
},
"@analyticsVocabListBody": {
"type": "String",
"placeholders": {}
},
"@morphAnalyticsListBody": {
"type": "String",
"placeholders": {}
},
"@chooseWordAudioInstructionsBody": {
"type": "String",
"placeholders": {}
},
"@chooseMorphsInstructionsBody": {
"type": "String",
"placeholders": {}
},
"@inviteAndLaunch": {
"type": "String",
"placeholders": {}
},
"@createOwnChat": {
"type": "String",
"placeholders": {}
},
"@pleaseEnterInt": {
"type": "String",
"placeholders": {}
},
"@home": {
"type": "String",
"placeholders": {}
},
"@join": {
"type": "String",
"placeholders": {}
},
"@readingAssistanceOverviewBody": {
"type": "String",
"placeholders": {}
},
"@learnByTexting": {
"type": "String",
"placeholders": {}
},
"@referFriends": {
"type": "String",
"placeholders": {}
},
"@referFriendDialogTitle": {
"type": "String",
"placeholders": {}
},
"@referFriendDialogDesc": {
"type": "String",
"placeholders": {}
},
"@youUnlocked": {
"type": "String",
"placeholders": {}
},
"@resetInstructionTooltipsTitle": {
"type": "String",
"placeholders": {}
},
"@resetInstructionTooltipsDesc": {
"type": "String",
"placeholders": {}
},
"@selectForGrammar": {
"type": "String",
"placeholders": {}
},
"@newChatActivityTitle": {
"type": "String",
"placeholders": {}
},
"@newChatActivityDesc": {
"type": "String",
"placeholders": {}
},
"@exploreMore": {
"type": "String",
"placeholders": {}
},
"@randomize": {
"type": "String",
"placeholders": {}
},
"@clear": {
"type": "String",
"placeholders": {}
},
"@makeYourOwnActivity": {
"type": "String",
"placeholders": {}
},
"@featuredActivities": {
"type": "String",
"placeholders": {}
},
"@yourBookmarks": {
"type": "String",
"placeholders": {}
},
"@goToChat": {
"type": "String",
"placeholders": {}
},
"@save": {
"type": "String",
"placeholders": {}
},
"@selectActivity": {
"type": "String",
"placeholders": {}
},
"@wordFocusListeningMultipleChoice": {
"type": "String",
"placeholders": {}
},
"@createActivity": {
"type": "String",
"placeholders": {}
},
"@startChat": {
"type": "String",
"placeholders": {}
},
"@translationProblem": {
"type": "String",
"placeholders": {}
},
"@perfectTranslation": {
"type": "String",
"placeholders": {}
},
"@greatJobTranslation": {
"type": "String",
"placeholders": {}
},
"@goodJobTranslation": {
"type": "String",
"placeholders": {}
},
"@makingProgress": {
"type": "String",
"placeholders": {}
},
"@keepPracticing": {
"type": "String",
"placeholders": {}
},
"@niceJob": {
"type": "String",
"placeholders": {}
},
"@publicSpacesTitle": {
"type": "String",
"placeholders": {}
},
"@askToJoin": {
"type": "String",
"placeholders": {}
},
"@emptyChatWarningTitle": {
"type": "String",
"placeholders": {}
},
"@emptyChatWarningDesc": {
"type": "String",
"placeholders": {}
},
"@areYouLikeMe": {
"type": "String",
"placeholders": {}
},
"@tryAgainLater": {
"type": "String",
"placeholders": {}
},
"@enterSpaceCode": {
"type": "String",
"placeholders": {}
},
"@shareSpaceLink": {
"type": "String",
"placeholders": {}
},
"@byUsingPangeaChat": {
"type": "String",
"placeholders": {}
},
"@details": {
"type": "String",
"placeholders": {}
},
"@languageLevelPreA1Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelA1Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelA2Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelB1Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelB2Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelC1Desc": {
"type": "String",
"placeholders": {}
},
"@languageLevelC2Desc": {
"type": "String",
"placeholders": {}
},
"@newVocab": {
"type": "String",
"placeholders": {}
},
"@newGrammar": {
"type": "String",
"placeholders": {}
},
"@congratulationsOnReaching": {
"type": "String",
"placeholders": {
"level": {
"type": "int"
}
}
},
"@seeDetails": {
"type": "String",
"placeholders": {}
},
"@choosePracticeMode": {
"type": "String",
"placeholders": {}
},
"@userWouldLikeToChangeTheSpace": {
"type": "String",
"placeholders": {
"user": {
"type": "String"
}
}
},
"@ban": {
"type": "String",
"placeholders": {}
},
"@unban": {
"type": "String",
"placeholders": {}
},
"@kick": {
"type": "String",
"placeholders": {}
},
"@approve": {
"type": "String",
"placeholders": {}
},
"@youHaveKnocked": {
"type": "String",
"placeholders": {}
},
"@pleaseWaitUntilInvited": {
"type": "String",
"placeholders": {}
},
"@lemma": {
"type": "String",
"placeholders": {}
},
"@grammarFeature": {
"type": "String",
"placeholders": {}
},
"@grammarTag": {
"type": "String",
"placeholders": {}
},
"@forms": {
"type": "String",
"placeholders": {}
},
"@exampleMessages": {
"type": "String",
"placeholders": {}
},
"@timesUsedIndependently": {
"type": "String",
"placeholders": {}
},
"@timesUsedWithAssistance": {
"type": "String",
"placeholders": {}
},
"@goToSpaceButton": {
"type": "String",
"placeholders": {}
},
"@shareInviteCode": {
"type": "String",
"placeholders": {
"code": {
"type": "String"
}
}
},
"@leaderboard": {
"type": "String",
"placeholders": {}
},
"@welcomeUser": {
"type": "String",
"placeholders": {
"user": {
"type": "String"
}
}
},
"@joinSpaceOnboardingDesc": {
"type": "String",
"placeholders": {}
},
"@skipForNow": {
"type": "String",
"placeholders": {}
}
}

View file

@ -3333,5 +3333,17 @@
"crossVerifiedDevicesIfEnabled": "交叉验证设备(如启用)",
"@crossVerifiedDevicesIfEnabled": {},
"verifiedDevicesOnly": "仅已验证设备",
"@verifiedDevicesOnly": {}
"@verifiedDevicesOnly": {},
"optionalMessage": "(可选)消息…",
"@optionalMessage": {},
"takeAPhoto": "拍照",
"@takeAPhoto": {},
"recordAVideo": "录像",
"@recordAVideo": {},
"notSupportedOnThisDevice": "此设备上不受支持",
"@notSupportedOnThisDevice": {},
"enterNewChat": "进入新聊天",
"@enterNewChat": {},
"commandHint_roomupgrade": "将此聊天室升级到给定的聊天室版本",
"@commandHint_roomupgrade": {}
}

View file

@ -186,7 +186,7 @@
}
}
},
"changedTheChatDescriptionTo": "{username} 變更了聊天室介紹為:「{description}」",
"changedTheChatDescriptionTo": "{username} 把聊天室介紹變更為:「{description}」",
"@changedTheChatDescriptionTo": {
"type": "String",
"placeholders": {
@ -198,7 +198,7 @@
}
}
},
"changedTheChatNameTo": "{username} 變更了聊天室名稱為:「{chatname}」",
"changedTheChatNameTo": "{username} 把聊天室名稱變更為:「{chatname}」",
"@changedTheChatNameTo": {
"type": "String",
"placeholders": {
@ -1403,7 +1403,7 @@
}
}
},
"sentAnAudio": "{username} 傳送了一個音訊",
"sentAnAudio": "🎤{username} 傳送了一個音訊",
"@sentAnAudio": {
"type": "String",
"placeholders": {
@ -1412,7 +1412,7 @@
}
}
},
"sentAPicture": "{username} 傳送了一張圖片",
"sentAPicture": "🖼️{username} 傳送了一張圖片",
"@sentAPicture": {
"type": "String",
"placeholders": {
@ -1421,7 +1421,7 @@
}
}
},
"sentASticker": "{username} 傳送了貼圖",
"sentASticker": "😊{username} 傳送了貼圖",
"@sentASticker": {
"type": "String",
"placeholders": {
@ -1430,7 +1430,7 @@
}
}
},
"sentAVideo": "{username} 傳送了影片",
"sentAVideo": "🎥{username} 傳送了影片",
"@sentAVideo": {
"type": "String",
"placeholders": {
@ -2750,7 +2750,7 @@
"@disableEncryptionWarning": {},
"deviceKeys": "裝置密鑰:",
"@deviceKeys": {},
"fileIsTooBigForServer": "伺服器報告該文件太大,無法傳送。",
"fileIsTooBigForServer": "無法發送!該伺服器僅支援最大到 {max} 的附件。",
"@fileIsTooBigForServer": {},
"fileHasBeenSavedAt": "文件已保存在 {path}",
"@fileHasBeenSavedAt": {
@ -3043,5 +3043,211 @@
"type": "String"
}
}
}
},
"doesNotSeemToBeAValidHomeserver": "似乎不是能匹配的歸屬伺服器。伺服器域名打錯了嗎?",
"@doesNotSeemToBeAValidHomeserver": {},
"noticeChatBackupDeviceVerification": "注意:當您將所有裝置連線到聊天備份時,它們會自動驗證。",
"@noticeChatBackupDeviceVerification": {},
"sendCanceled": "傳送取消",
"@sendCanceled": {},
"changelog": "變更日誌",
"@changelog": {},
"changeTheCanonicalRoomAlias": "變更公開聊天室的主要地址",
"@changeTheCanonicalRoomAlias": {},
"sendImages": "傳送{count}張圖片",
"@sendImages": {
"type": "String",
"placeholders": {
"count": {
"type": "int"
}
}
},
"loginWithMatrixId": "以Matrix-ID登入",
"@loginWithMatrixId": {},
"inviteOtherUsers": "邀請其他用戶進入本聊天",
"@inviteOtherUsers": {},
"sendRoomNotifications": "傳送一條 @room 群提醒",
"@sendRoomNotifications": {},
"updateInstalled": "🎉已成功安裝{version}版本!",
"@updateInstalled": {
"type": "String",
"placeholders": {
"version": {
"type": "String"
}
}
},
"oneOfYourDevicesIsNotVerified": "你的其中一個裝置尚未驗證",
"@oneOfYourDevicesIsNotVerified": {},
"chatPermissionsDescription": "定義此聊天中某些操作需要哪個權限等級。 權限等級0、50和100通常代表使用者、版主和管理員但任何分級都是可能的。",
"@chatPermissionsDescription": {},
"changeGeneralChatSettings": "變更一般聊天設定",
"@changeGeneralChatSettings": {},
"manageAccount": "帳號管理",
"@manageAccount": {},
"changeTheChatPermissions": "變更聊天室權限",
"@changeTheChatPermissions": {},
"changeTheVisibilityOfChatHistory": "變更過往聊天記錄可見度",
"@changeTheVisibilityOfChatHistory": {},
"homeserverDescription": "您的所有資料都儲存在歸屬伺服器上,就像電子郵件提供商一樣。 您可以選擇要使用的歸屬伺服器,同時您仍然可以與每個人溝通。 請訪問https://matrix.org瞭解更多資訊。",
"@homeserverDescription": {},
"sendingAttachment": "附件傳送中…",
"@sendingAttachment": {},
"compressVideo": "影片壓縮中…",
"@compressVideo": {},
"opacity": "不透明度:",
"@opacity": {},
"aboutHomeserver": "關於{homeserver}",
"@aboutHomeserver": {
"type": "String",
"placeholders": {
"homeserver": {
"type": "String"
}
}
},
"noChatsFoundHere": "還沒開始聊天嗎?點擊下方按鈕找個人聊聊吧⤵",
"@noChatsFoundHere": {},
"changeTheDescriptionOfTheGroup": "變更聊天室說明",
"@changeTheDescriptionOfTheGroup": {},
"discoverHomeservers": "探索歸屬伺服器",
"@discoverHomeservers": {},
"whatIsAHomeserver": "什麼是歸屬伺服器?",
"@whatIsAHomeserver": {},
"calculatingFileSize": "正在計算檔案大小…",
"@calculatingFileSize": {},
"prepareSendingAttachment": "準備傳送附件…",
"@prepareSendingAttachment": {},
"generatingVideoThumbnail": "生成影片縮圖中…",
"@generatingVideoThumbnail": {},
"sendingAttachmentCountOfCount": "附件傳送中 {index}/{length}…",
"@sendingAttachmentCountOfCount": {
"type": "integer",
"placeholders": {
"index": {
"type": "int"
},
"length": {
"type": "int"
}
}
},
"serverLimitReached": "已達伺服器上限! 請稍等{seconds}秒…",
"@serverLimitReached": {
"type": "integer",
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"welcomeText": "嘿嘿👋這是FluffyChat。 您可以登入任何與https://matrix.org相容的歸屬伺服器後和任何人聊天。 這是一個巨大的去中心化訊息網路!",
"@welcomeText": {},
"setWallpaper": "設定背景樣式",
"@setWallpaper": {},
"noContactInformationProvided": "伺服器沒有提供任何有效的聯絡資訊",
"@noContactInformationProvided": {},
"contactServerAdmin": "聯繫伺服器管理員",
"@contactServerAdmin": {},
"contactServerSecurity": "聯繫伺服器安管",
"@contactServerSecurity": {},
"continueText": "繼續",
"@continueText": {},
"blur": "模糊:",
"@blur": {},
"synchronizingPleaseWaitCounter": " 同步中… ({percentage}%)",
"@synchronizingPleaseWaitCounter": {
"type": "String",
"placeholders": {
"percentage": {
"type": "String"
}
}
},
"contentNotificationSettings": "內容通知設定",
"@contentNotificationSettings": {},
"generalNotificationSettings": "常規通知設定",
"@generalNotificationSettings": {},
"roomNotificationSettings": "聊天室通知設定",
"@roomNotificationSettings": {},
"userSpecificNotificationSettings": "用戶特定通知設定",
"@userSpecificNotificationSettings": {},
"otherNotificationSettings": "其他通知設定",
"@otherNotificationSettings": {},
"notificationRuleContainsUserName": "包含用户名稱",
"@notificationRuleContainsUserName": {},
"notificationRuleContainsUserNameDescription": "當訊息帶有用户名稱時通知用戶。",
"@notificationRuleContainsUserNameDescription": {},
"notificationRuleMaster": "靜音所有通知",
"@notificationRuleMaster": {},
"notificationRuleMasterDescription": "覆蓋所有其他規則並禁止所有通知。",
"@notificationRuleMasterDescription": {},
"notificationRuleInviteForMe": "邀請我",
"@notificationRuleInviteForMe": {},
"notificationRuleSuppressNoticesDescription": "隱藏來自bot等的自動化消息。",
"@notificationRuleSuppressNoticesDescription": {},
"notificationRuleSuppressNotices": "隱藏自動化消息",
"@notificationRuleSuppressNotices": {},
"notificationRuleMemberEvent": "成員事件",
"@notificationRuleMemberEvent": {},
"notificationRuleMemberEventDescription": "隱藏成員事件的通知。",
"@notificationRuleMemberEventDescription": {},
"notificationRuleIsUserMention": "用户提及",
"@notificationRuleIsUserMention": {},
"notificationRuleInviteForMeDescription": "當用户被邀請到聊天室時,通知他們。",
"@notificationRuleInviteForMeDescription": {},
"commandHint_roomupgrade": "升級此聊天室至指定版本",
"@commandHint_roomupgrade": {},
"serverInformation": "伺服器資訊 ",
"@serverInformation": {},
"name": "名稱",
"@name": {},
"website": "網站",
"@website": {},
"compress": "壓縮",
"@compress": {},
"newChatRequest": "📩 新的聊天邀請",
"@newChatRequest": {},
"enterNewChat": "進入新聊天室",
"@enterNewChat": {},
"version": "版本",
"@version": {},
"unableToJoinChat": "無法加入聊天室。對話可能以被其他方結束。",
"@unableToJoinChat": {},
"appWantsToUseForLogin": "使用伺服器「{server} 」登入",
"@appWantsToUseForLogin": {
"type": "String",
"placeholders": {
"server": {
"type": "String"
}
}
},
"italicText": "斜體",
"@italicText": {},
"boldText": "粗體",
"@boldText": {},
"strikeThrough": "刪除線",
"@strikeThrough": {},
"pleaseFillOut": "請填充",
"@pleaseFillOut": {},
"invalidUrl": "無效 url",
"@invalidUrl": {},
"appWantsToUseForLoginDescription": "你特此允許該應用程式和網站分享關於你的信息。",
"@appWantsToUseForLoginDescription": {},
"open": "打開",
"@open": {},
"waitingForServer": "等待伺服器中...",
"@waitingForServer": {},
"appIntroduction": "FluffyChat 讓你和你的朋友跨越工具聊天。在 https://matrix.org 了解更多或*繼續*。",
"@appIntroduction": {},
"previous": "上一個",
"@previous": {},
"otherPartyNotLoggedIn": "對方現未登入,未能接收訊息 !",
"@otherPartyNotLoggedIn": {},
"supportPage": "幫助頁面",
"@supportPage": {},
"addLink": "插入連結",
"@addLink": {}
}

Binary file not shown.

View file

@ -1,96 +0,0 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

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

@ -11,10 +11,11 @@ abstract class AppConfig {
// #Pangea
static String get applicationName => _applicationName;
static String? _applicationWelcomeMessage;
static String? get applicationWelcomeMessage => _applicationWelcomeMessage;
// #Pangea
// static String _defaultHomeserver = 'matrix.org';
static String _defaultHomeserver = Environment.synapseURL;
static String get _defaultHomeserver => Environment.synapseURL;
// #Pangea
static String get defaultHomeserver => _defaultHomeserver;
static double fontSizeFactor = 1;
@ -120,7 +121,10 @@ abstract class AppConfig {
'https://8591d0d863b646feb4f3dda7e5dcab38@o256755.ingest.sentry.io/5243143';
// Pangea#
static bool renderHtml = true;
static bool hideRedactedEvents = false;
// #Pangea
// static bool hideRedactedEvents = false;
static bool hideRedactedEvents = true;
// Pangea#
static bool hideUnknownEvents = true;
static bool hideUnimportantStateEvents = true;
static bool separateChatTypes = false;
@ -154,9 +158,6 @@ abstract class AppConfig {
'https://sygnal.pangea.chat/_matrix/push/v1/notify';
static const String? pushNotificationsPusherFormat = null;
// Pangea#
static const String emojiFontName = 'Noto Emoji';
static const String emojiFontUrl =
'https://github.com/googlefonts/noto-emoji/';
static const double borderRadius = 18.0;
static const double columnWidth = 360.0;
static final Uri homeserverList = Uri(
@ -208,9 +209,11 @@ abstract class AppConfig {
if (json['application_welcome_message'] is String) {
_applicationWelcomeMessage = json['application_welcome_message'];
}
if (json['default_homeserver'] is String) {
_defaultHomeserver = json['default_homeserver'];
}
// #Pangea
// if (json['default_homeserver'] is String) {
// _defaultHomeserver = json['default_homeserver'];
// }
// Pangea#
if (json['privacy_url'] is String) {
_privacyUrl = json['privacy_url'];
}

View file

@ -37,11 +37,14 @@ import 'package:fluffychat/pangea/layouts/bottom_nav_layout.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart';
import 'package:fluffychat/pangea/login/pages/signup.dart';
import 'package:fluffychat/pangea/login/pages/space_code_onboarding.dart';
import 'package:fluffychat/pangea/login/pages/user_settings.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_alias.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart';
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
import 'package:fluffychat/pangea/user/pages/find_partner.dart';
import 'package:fluffychat/widgets/config_viewer.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
import 'package:fluffychat/widgets/log_view.dart';
@ -130,23 +133,37 @@ abstract class AppRoutes {
const LogViewer(),
),
),
GoRoute(
path: '/configs',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ConfigViewer(),
),
),
// #Pangea
GoRoute(
path: '/join_with_link',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const JoinClassWithLink(),
JoinClassWithLink(
classCode: state.uri.queryParameters[SpaceConstants.classCode],
),
),
),
GoRoute(
path: '/join_with_alias',
pageBuilder: (context, state) => Matrix.of(context).client.isLogged()
? chatListShellRouteBuilder(context, state, const JoinWithAlias())
? chatListShellRouteBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
)
: defaultPageBuilder(
context,
state,
const JoinWithAlias(),
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
),
GoRoute(
@ -157,30 +174,17 @@ abstract class AppRoutes {
const UserSettingsPage(),
),
redirect: loggedOutRedirect,
),
ShellRoute(
pageBuilder: chatListShellRouteBuilder,
routes: [
GoRoute(
path: '/homepage',
path: 'join_space',
pageBuilder: (context, state) {
return defaultPageBuilder(
context,
state,
const SpaceCodeOnboarding(),
);
},
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SuggestionsPage(),
),
routes: [
...newRoomRoutes,
GoRoute(
path: '/planner',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityGenerator(),
),
),
],
),
],
),
@ -197,6 +201,10 @@ abstract class AppRoutes {
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),
@ -215,7 +223,45 @@ abstract class AppRoutes {
routes: [
GoRoute(
path: '/rooms',
redirect: loggedOutRedirect,
// #Pangea
// redirect: loggedOutRedirect,
redirect: (context, state) async {
final resp = await loggedOutRedirect(context, state);
if (resp != null) return resp;
final isColumnMode = FluffyThemes.isColumnMode(context);
final roomId = state.pathParameters['roomid'];
final room = roomId != null
? Matrix.of(context).client.getRoomById(roomId)
: null;
if (room != null && room.isSpace) {
// If a user is on mobile and they end up on the space
// page, redirect them and set the activeSpaceId
if (!isColumnMode &&
(state.fullPath?.endsWith(':roomid') ?? false)) {
return '/rooms?spaceId=${room.id}';
}
}
if (state.uri.queryParameters.containsKey('spaceId')) {
final spaceId = state.uri.queryParameters['spaceId'];
if (spaceId == null || spaceId == 'clear') {
// Have to load chat list to clear the spaceId, so don't redirect
return null;
}
// If spaceId is not null, and on web, and not on the space page,
// redirect to the space page
if (isColumnMode &&
!(state.fullPath?.endsWith(':roomid') ?? false)) {
return '/rooms/$spaceId?spaceId=$spaceId';
}
}
return null;
},
// Pangea#
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
@ -226,6 +272,10 @@ abstract class AppRoutes {
// Pangea#
: ChatList(
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
),
),
routes: [
@ -315,6 +365,40 @@ abstract class AppRoutes {
: child,
),
routes: [
// #Pangea
GoRoute(
path: '/homepage',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const SuggestionsPage(),
),
routes: [
...newRoomRoutes,
GoRoute(
path: '/planner',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityPlannerPage(),
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: '/generator',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const ActivityGenerator(),
),
),
],
),
],
),
// Pangea#
GoRoute(
path: 'settings',
pageBuilder: (context, state) => defaultPageBuilder(
@ -718,6 +802,10 @@ abstract class AppRoutes {
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
// #Pangea
activeSpaceId: state.uri.queryParameters['spaceId'],
activeFilter: state.uri.queryParameters['filter'],
// Pangea#
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),

View file

@ -1,3 +1,5 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class SettingKeys {
static const String renderHtml = 'chat.fluffy.renderHtml';
static const String hideRedactedEvents = 'chat.fluffy.hideRedactedEvents';
@ -30,9 +32,68 @@ abstract class SettingKeys {
'chat.fluffy.swipeRightToLeftToReply';
static const String experimentalVoip = 'chat.fluffy.experimental_voip';
static const String showPresences = 'chat.fluffy.show_presences';
static const String displayChatDetailsColumn =
'chat.fluffy.display_chat_details_column';
static const String noEncryptionWarningShown =
'chat.fluffy.no_encryption_warning_shown';
static const String shareKeysWith = 'chat.fluffy.share_keys_with';
}
enum AppSettings<T> {
audioRecordingNumChannels<int>('audioRecordingNumChannels', 1),
audioRecordingAutoGain<bool>('audioRecordingAutoGain', true),
audioRecordingEchoCancel<bool>('audioRecordingEchoCancel', false),
audioRecordingNoiseSuppress<bool>('audioRecordingNoiseSuppress', true),
audioRecordingBitRate<int>('audioRecordingBitRate', 64000),
// #Pangea
// audioRecordingSamplingRate<int>('audioRecordingSamplingRate', 44100),
audioRecordingSamplingRate<int>('audioRecordingSamplingRate', 22050),
// Pangea#
pushNotificationsGatewayUrl<String>(
'pushNotificationsGatewayUrl',
'https://push.fluffychat.im/_matrix/push/v1/notify',
),
pushNotificationsPusherFormat<String>(
'pushNotificationsPusherFormat',
'event_id_only',
),
shareKeysWith<String>('chat.fluffy.share_keys_with_2', 'all'),
noEncryptionWarningShown<bool>(
'chat.fluffy.no_encryption_warning_shown',
false,
),
displayChatDetailsColumn(
'chat.fluffy.display_chat_details_column',
false,
);
final String key;
final T defaultValue;
const AppSettings(this.key, this.defaultValue);
}
extension AppSettingsBoolExtension on AppSettings<bool> {
bool getItem(SharedPreferences store) => store.getBool(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, bool value) =>
store.setBool(key, value);
}
extension AppSettingsStringExtension on AppSettings<String> {
String getItem(SharedPreferences store) =>
store.getString(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, String value) =>
store.setString(key, value);
}
extension AppSettingsIntExtension on AppSettings<int> {
int getItem(SharedPreferences store) => store.getInt(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, int value) =>
store.setInt(key, value);
}
extension AppSettingsDoubleExtension on AppSettings<double> {
double getItem(SharedPreferences store) =>
store.getDouble(key) ?? defaultValue;
Future<void> setItem(SharedPreferences store, double value) =>
store.setDouble(key, value);
}

View file

@ -18,27 +18,6 @@ abstract class FluffyThemes {
static bool isThreeColumnMode(BuildContext context) =>
MediaQuery.of(context).size.width > FluffyThemes.columnWidth * 3.5;
static const fallbackTextStyle = TextStyle(
fontFamily: 'Ubuntu',
fontFamilyFallback: ['NotoEmoji'],
);
static var fallbackTextTheme = const TextTheme(
bodyLarge: fallbackTextStyle,
bodyMedium: fallbackTextStyle,
labelLarge: fallbackTextStyle,
bodySmall: fallbackTextStyle,
labelSmall: fallbackTextStyle,
displayLarge: fallbackTextStyle,
displayMedium: fallbackTextStyle,
displaySmall: fallbackTextStyle,
headlineMedium: fallbackTextStyle,
headlineSmall: fallbackTextStyle,
titleLarge: fallbackTextStyle,
titleMedium: fallbackTextStyle,
titleSmall: fallbackTextStyle,
);
static LinearGradient backgroundGradient(
BuildContext context,
int alpha,
@ -77,11 +56,6 @@ abstract class FluffyThemes {
useMaterial3: true,
brightness: brightness,
colorScheme: colorScheme,
// #Pangea
// causes memory leak on iOS
// textTheme: fallbackTextTheme,
// textTheme: scaleTextTheme(Theme.of(context).textTheme, MediaQuery.of(context).size),
// Pangea#
dividerColor: brightness == Brightness.dark
? colorScheme.surfaceContainerHighest
: colorScheme.surfaceContainer,
@ -105,7 +79,14 @@ abstract class FluffyThemes {
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
contentPadding: const EdgeInsets.all(12),
filled: false,
),
chipTheme: ChipThemeData(
showCheckmark: false,
backgroundColor: colorScheme.surfaceContainer,
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
),
appBarTheme: AppBarTheme(
toolbarHeight: isColumnMode ? 72 : 56,
@ -197,6 +178,7 @@ extension BubbleColorTheme on ThemeData {
Color get bubbleColor => brightness == Brightness.light
? colorScheme.primary
: colorScheme.primaryContainer;
Color get onBubbleColor => brightness == Brightness.light
? colorScheme.onPrimary
: colorScheme.onPrimaryContainer;

View file

@ -110,7 +110,7 @@ Future<void> startGui(List<Client> clients, SharedPreferences store) async {
// staging or vice versa, logout.
if (firstClient?.userID?.domain != null) {
final isStagingUser = firstClient!.userID!.domain!.contains("staging");
final isStagingServer = Environment.isStaging;
final isStagingServer = Environment.synapseURL.contains("staging");
if (isStagingServer != isStagingUser) {
await firstClient.logout();
}

View file

@ -18,6 +18,7 @@ import '../key_verification/key_verification_dialog.dart';
class BootstrapDialog extends StatefulWidget {
final bool wipe;
final Client client;
const BootstrapDialog({
super.key,
this.wipe = false,
@ -132,7 +133,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
minLines: 2,
maxLines: 4,
readOnly: true,
style: const TextStyle(fontFamily: 'UbuntuMono'),
style: const TextStyle(fontFamily: 'RobotoMono'),
controller: TextEditingController(text: key),
decoration: const InputDecoration(
contentPadding: EdgeInsets.all(16),
@ -257,7 +258,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
? null
: [AutofillHints.password],
controller: _recoveryKeyTextEditingController,
style: const TextStyle(fontFamily: 'UbuntuMono'),
style: const TextStyle(fontFamily: 'RobotoMono'),
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle(

View file

@ -50,6 +50,7 @@ import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dialog.dart';
import 'package:fluffychat/pangea/spaces/pages/pangea_space_page.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/error_reporter.dart';
@ -101,6 +102,12 @@ class ChatPage extends StatelessWidget {
);
}
// #Pangea
if (room.isSpace) {
return PangeaSpacePage(space: room);
}
// Pangea#
return ChatPageWithRoom(
key: Key('chat_page_${roomId}_$eventId'),
room: room,
@ -148,7 +155,7 @@ class ChatController extends State<ChatPageWithRoom>
final AutoScrollController scrollController = AutoScrollController();
FocusNode inputFocus = FocusNode();
late final FocusNode inputFocus;
StreamSubscription<html.Event>? onFocusSub;
Timer? typingCoolDown;
@ -323,8 +330,29 @@ class ChatController extends State<ChatPageWithRoom>
);
}
KeyEventResult _shiftEnterKeyHandling(FocusNode node, KeyEvent evt) {
if (!HardwareKeyboard.instance.isShiftPressed &&
evt.logicalKey.keyLabel == 'Enter') {
if (evt is KeyDownEvent) {
// #Pangea
// send();
choreographer.send(context);
// Pangea#
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
}
@override
void initState() {
inputFocus = FocusNode(
onKeyEvent: (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile)
? _shiftEnterKeyHandling
: null,
);
scrollController.addListener(_updateScrollController);
inputFocus.addListener(_inputFocusListener);
@ -332,8 +360,7 @@ class ChatController extends State<ChatPageWithRoom>
WidgetsBinding.instance.addPostFrameCallback(_shareItems);
super.initState();
_displayChatDetailsColumn = ValueNotifier(
Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
false,
AppSettings.displayChatDetailsColumn.getItem(Matrix.of(context).store),
);
sendingClient = Matrix.of(context).client;
@ -341,14 +368,6 @@ class ChatController extends State<ChatPageWithRoom>
WidgetsBinding.instance.addObserver(this);
// #Pangea
if (!mounted) return;
if (room.isSpace) {
ErrorHandler.logError(
e: "Space chat opened",
s: StackTrace.current,
data: {"roomId": roomId},
);
context.go("/rooms");
}
Future.delayed(const Duration(seconds: 1), () async {
if (!mounted) return;
debugPrint(
@ -531,7 +550,8 @@ class ChatController extends State<ChatPageWithRoom>
var prevNumEvents = timeline!.events.length;
await requestHistory();
var numRequests = 0;
while (timeline!.events.length > prevNumEvents &&
while (timeline != null &&
timeline!.events.length > prevNumEvents &&
visibleEvents.length < 10 &&
numRequests <= 5) {
prevNumEvents = timeline!.events.length;
@ -569,8 +589,8 @@ class ChatController extends State<ChatPageWithRoom>
if (state == AppLifecycleState.paused) {
clearSelectedEvents();
}
if (state == AppLifecycleState.hidden && !stopAudioStream.isClosed) {
stopAudioStream.add(null);
if (state == AppLifecycleState.hidden && !stopMediaStream.isClosed) {
stopMediaStream.add(null);
}
// Pangea#
if (state != AppLifecycleState.resumed) return;
@ -662,7 +682,7 @@ class ChatController extends State<ChatPageWithRoom>
choreographer.dispose();
MatrixState.pAnyState.closeAllOverlays(force: true);
showToolbarStream.close();
stopAudioStream.close();
stopMediaStream.close();
hideTextController.dispose();
_levelSubscription?.cancel();
_analyticsSubscription?.cancel();
@ -677,10 +697,20 @@ class ChatController extends State<ChatPageWithRoom>
super.didChangeDependencies();
_router = GoRouter.of(context);
_router.routeInformationProvider.addListener(_onRouteChanged);
if (room.isSpace && _router.state.path == ":roomid") {
ErrorHandler.logError(
e: "Space chat opened",
s: StackTrace.current,
data: {"roomId": roomId},
);
context.go("/rooms");
}
}
void _onRouteChanged() {
stopAudioStream.add(null);
if (!stopMediaStream.isClosed) {
stopMediaStream.add(null);
}
MatrixState.pAnyState.closeAllOverlays();
}
@ -867,10 +897,13 @@ class ChatController extends State<ChatPageWithRoom>
pangeaEditingEvent = previousEdit;
}
GoogleAnalytics.sendMessage(
room.id,
room.classCode(context),
);
final spaceCode = room.classCode(context);
if (spaceCode != null) {
GoogleAnalytics.sendMessage(
room.id,
spaceCode,
);
}
if (msgEventId == null) {
ErrorHandler.logError(
@ -924,8 +957,12 @@ class ChatController extends State<ChatPageWithRoom>
});
}
void sendFileAction() async {
final files = await selectFiles(context, allowMultiple: true);
void sendFileAction({FileSelectorType type = FileSelectorType.any}) async {
final files = await selectFiles(
context,
allowMultiple: true,
type: type,
);
if (files.isEmpty) return;
await showAdaptiveDialog(
context: context,
@ -949,27 +986,6 @@ class ChatController extends State<ChatPageWithRoom>
);
}
void sendImageAction() async {
final files = await selectFiles(
context,
allowMultiple: true,
// #Pangea
// type: FileSelectorType.images,
type: FileSelectorType.media,
// Pangea#
);
if (files.isEmpty) return;
await showAdaptiveDialog(
context: context,
builder: (c) => SendFileDialog(
files: files,
room: room,
outerContext: context,
),
);
}
void openCameraAction() async {
// Make sure the textfield is unfocused before opening the camera
FocusScope.of(context).requestFocus(FocusNode());
@ -1007,7 +1023,7 @@ class ChatController extends State<ChatPageWithRoom>
void voiceMessageAction() async {
// #Pangea
stopAudioStream.add(null);
stopMediaStream.add(null);
// Pangea#
final scaffoldMessenger = ScaffoldMessenger.of(context);
if (PlatformInfos.isAndroid) {
@ -1235,12 +1251,13 @@ class ChatController extends State<ChatPageWithRoom>
// Pangea#
)
: null;
// #Pangea
// if (reasonInput == null) return;
if (reasonInput == null) {
// #Pangea
clearSelectedEvents();
// Pangea#
return;
}
// Pangea#
final reason = reasonInput.isEmpty ? null : reasonInput;
for (final event in selectedEvents) {
await showFutureLoadingDialog(
@ -1572,35 +1589,23 @@ class ChatController extends State<ChatPageWithRoom>
}
void goToNewRoomAction() async {
if (OkCancelResult.ok !=
await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).goToTheNewRoom,
message: room
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.body,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
)) {
return;
}
final result = await showFutureLoadingDialog(
context: context,
future: () async {
final roomId = room.client.joinRoom(
room
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.replacementRoom,
);
await room.leave();
return roomId;
},
future: () => room.client.joinRoomById(
room
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.replacementRoom,
),
);
if (result.error != null) return;
if (!mounted) return;
context.go('/rooms/${result.result!}');
await showFutureLoadingDialog(
context: context,
future: room.leave,
);
if (result.error == null) {
context.go('/rooms/${result.result!}');
}
}
void onSelectMessage(Event event) {
@ -1671,7 +1676,10 @@ class ChatController extends State<ChatPageWithRoom>
sendFileAction();
}
if (choice == 'image') {
sendImageAction();
sendFileAction(type: FileSelectorType.images);
}
if (choice == 'video') {
sendFileAction(type: FileSelectorType.videos);
}
if (choice == 'camera') {
openCameraAction();
@ -1850,7 +1858,7 @@ class ChatController extends State<ChatPageWithRoom>
final StreamController<String> showToolbarStream =
StreamController.broadcast();
final StreamController<void> stopAudioStream = StreamController.broadcast();
final StreamController<void> stopMediaStream = StreamController.broadcast();
void showToolbar(
Event event, {
@ -1910,7 +1918,7 @@ class ChatController extends State<ChatPageWithRoom>
HapticFeedback.mediumImpact();
}
stopAudioStream.add(null);
stopMediaStream.add(null);
Future.delayed(
Duration(milliseconds: buttonEventID == event.eventId ? 200 : 0), () {
@ -1954,10 +1962,10 @@ class ChatController extends State<ChatPageWithRoom>
late final ValueNotifier<bool> _displayChatDetailsColumn;
void toggleDisplayChatDetailsColumn() async {
await Matrix.of(context).store.setBool(
SettingKeys.displayChatDetailsColumn,
!_displayChatDetailsColumn.value,
);
await AppSettings.displayChatDetailsColumn.setItem(
Matrix.of(context).store,
!_displayChatDetailsColumn.value,
);
_displayChatDetailsColumn.value = !_displayChatDetailsColumn.value;
}

View file

@ -37,6 +37,7 @@ class ChatAppBarListTile extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Linkify(
text: title,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
maxLines: 1,
overflow: TextOverflow.ellipsis,

View file

@ -1,6 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:fluffychat/config/app_config.dart';
@ -9,16 +9,15 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message.dart';
import 'package:fluffychat/pages/chat/seen_by_row.dart';
import 'package:fluffychat/pages/chat/typing_indicators.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
class ChatEventList extends StatelessWidget {
final ChatController controller;
const ChatEventList({
super.key,
required this.controller,
@ -27,14 +26,10 @@ class ChatEventList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final timeline = controller.timeline;
if (timeline == null) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
if (timeline == null) {
return const Center(child: CupertinoActivityIndicator());
}
final theme = Theme.of(context);
final colors = [
@ -99,6 +94,7 @@ class ChatEventList extends StatelessWidget {
// Request history button or progress indicator:
if (i == events.length + 1) {
if (timeline.isRequestingHistory) {
// #Pangea
// return const Center(
// child: CircularProgressIndicator.adaptive(strokeWidth: 2),
// );
@ -185,15 +181,8 @@ class ChatEventList extends StatelessWidget {
onInfoTab: (_) => {},
// onInfoTab: controller.showEventInfo,
// Pangea#
onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback,
outerContext: context,
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
),
),
onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ',
highlightMarker:
controller.scrollToEventIdMarker == event.eventId,
// #Pangea

View file

@ -112,88 +112,120 @@ class ChatInputRow extends StatelessWidget {
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
width: controller.sendController.text.isEmpty ? height : 0,
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton<String>(
icon: const Icon(Icons.add_outlined),
icon: const Icon(Icons.add_circle_outline),
iconColor: theme.colorScheme.onPrimaryContainer,
onSelected: controller.onAddPopupMenuButtonSelected,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
child: Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
child: Icon(Icons.image_outlined),
),
title: Text(L10n.of(context).sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
child: Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).openCamera),
contentPadding: const EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'camera-video',
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).openVideoCamera),
contentPadding: const EdgeInsets.all(0),
),
),
if (PlatformInfos.isMobile)
PopupMenuItem<String>(
value: 'location',
child: ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.brown,
foregroundColor: Colors.white,
child: Icon(Icons.gps_fixed_outlined),
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context).shareLocation),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'image',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.photo_outlined),
),
title: Text(L10n.of(context).sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'video',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.video_camera_back_outlined),
),
title: Text(L10n.of(context).sendVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'file',
child: ListTile(
leading: CircleAvatar(
backgroundColor: theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
if (PlatformInfos.isMobile)
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.sendController.text.isNotEmpty ? 0 : height,
height: height,
alignment: Alignment.center,
decoration: const BoxDecoration(),
clipBehavior: Clip.hardEdge,
child: PopupMenuButton(
icon: const Icon(Icons.camera_alt_outlined),
onSelected: controller.onAddPopupMenuButtonSelected,
iconColor: theme.colorScheme.onPrimaryContainer,
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera-video',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).recordAVideo),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<String>(
value: 'camera',
child: ListTile(
leading: CircleAvatar(
backgroundColor:
theme.colorScheme.onPrimaryContainer,
foregroundColor: theme.colorScheme.primaryContainer,
child: const Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).takeAPhoto),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
),
Container(
height: height,
width: height,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).emojis,
color: theme.colorScheme.onPrimaryContainer,
icon: PageTransitionSwitcher(
transitionBuilder: (
Widget child,

View file

@ -190,11 +190,6 @@ class ChatView extends StatelessWidget {
if (scrollUpBannerEventId != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
final tombstoneEvent =
controller.room.getState(EventTypes.RoomTombstone);
if (tombstoneEvent != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
return Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
@ -233,18 +228,6 @@ class ChatView extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
PinnedEvents(controller),
if (tombstoneEvent != null)
ChatAppBarListTile(
title: tombstoneEvent.parsedTombstoneContent.body,
leading: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.upgrade_outlined),
),
trailing: TextButton(
onPressed: controller.goToNewRoomAction,
child: Text(L10n.of(context).goToTheNewRoom),
),
),
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
leading: IconButton(
@ -329,7 +312,21 @@ class ChatView extends StatelessWidget {
child: ChatEventList(controller: controller),
),
),
if (controller.room.canSendDefaultMessages &&
if (controller.room.isExtinct)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.chevron_right),
label: Text(L10n.of(context).enterNewChat),
onPressed: controller.goToNewRoomAction,
),
)
else if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Container(
margin: EdgeInsets.only(
@ -409,7 +406,9 @@ class ChatView extends StatelessWidget {
),
// #Pangea
ChatViewBackground(controller.choreographer),
if (!controller.room.isAbandonedDMRoom)
if (!controller.room.isAbandonedDMRoom &&
controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
Positioned(
left: 0,
right: 0,

View file

@ -78,6 +78,8 @@ String commandHint(L10n l10n, String command) {
return l10n.commandHint_ignore;
case 'unignore':
return l10n.commandHint_unignore;
case 'roomupgrade':
return l10n.commandHint_roomupgrade;
default:
return "";
}

View file

@ -22,6 +22,7 @@ extension EventInfoDialogExtension on Event {
class EventInfoDialog extends StatelessWidget {
final Event event;
final L10n l10n;
const EventInfoDialog({
required this.event,
required this.l10n,
@ -42,10 +43,8 @@ class EventInfoDialog extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).messageInfo),
leading: IconButton(
icon: const Icon(Icons.arrow_downward_outlined),
leading: CloseButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
tooltip: L10n.of(context).close,
),
),
body: ListView(

View file

@ -173,8 +173,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// #Pangea
// If there's another audio playing, stop it. Wait for this to come through
// the stream so that the listener doesn't stop the audio that just started
final future = widget.chatController.stopAudioStream.stream.first;
widget.chatController.stopAudioStream.add(null);
final future = widget.chatController.stopMediaStream.stream.first;
widget.chatController.stopMediaStream.add(null);
await future;
// if (AudioPlayerWidget.currentId != widget.event.eventId) {
@ -366,7 +366,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
: _downloadAction();
}
_onShowToolbar = widget.chatController.stopAudioStream.stream.listen((_) {
_onShowToolbar = widget.chatController.stopMediaStream.stream.listen((_) {
audioPlayer?.pause();
audioPlayer?.seek(Duration.zero);
});
@ -561,20 +561,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
// Pangea#
) ...[
const SizedBox(height: 8),
Linkify(
text: fileDescription,
style: TextStyle(
color: widget.color,
fontSize: widget.fontSize,
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: widget.linkColor,
fontSize: widget.fontSize,
decoration: TextDecoration.underline,
decorationColor: widget.linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: widget.color,
fontSize: widget.fontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: widget.linkColor,
fontSize: widget.fontSize,
decoration: TextDecoration.underline,
decorationColor: widget.linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
],
],
@ -586,6 +593,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
/// To use a MatrixFile as an AudioSource for the just_audio package
class MatrixFileAudioSource extends StreamAudioSource {
final MatrixFile file;
MatrixFileAudioSource(this.file);
@override

View file

@ -70,6 +70,7 @@ class HtmlMessage extends StatelessWidget {
static const Set<String> allowedHtmlTags = {
'font',
'del',
's',
'h1',
'h2',
'h3',
@ -289,7 +290,6 @@ class HtmlMessage extends StatelessWidget {
);
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: CompositedTransformTarget(
link: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
@ -332,12 +332,12 @@ class HtmlMessage extends StatelessWidget {
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onClick != null && token != null
? () => onClick?.call(token)
: null,
child: Text.rich(
textScaler: TextScaler.noScaling,
TextSpan(
child: RichText(
text: TextSpan(
children: [
LinkifySpan(
text: node.innerHtml,
@ -445,7 +445,7 @@ class HtmlMessage extends StatelessWidget {
if (node.parent?.localName == 'ol')
TextSpan(
text:
'${(node.parent?.nodes.indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ',
'${(node.parent?.nodes.whereType<dom.Element>().toList().indexOf(node) ?? 0) + (int.tryParse(node.parent?.attributes['start'] ?? '1') ?? 1)}. ',
),
..._renderWithLineBreaks(
node.nodes,
@ -466,7 +466,7 @@ class HtmlMessage extends StatelessWidget {
border: Border(
left: BorderSide(
color: textColor,
width: 3,
width: 5,
),
),
),
@ -514,7 +514,7 @@ class HtmlMessage extends StatelessWidget {
),
textStyle: TextStyle(
fontSize: fontSize,
fontFamily: 'UbuntuMono',
fontFamily: 'RobotoMono',
),
),
),
@ -638,6 +638,7 @@ class HtmlMessage extends StatelessWidget {
'strong' => const TextStyle(fontWeight: FontWeight.bold),
'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic),
'del' ||
's' ||
'strikethrough' =>
const TextStyle(decoration: TextDecoration.lineThrough),
'u' => const TextStyle(decoration: TextDecoration.underline),
@ -671,6 +672,7 @@ class HtmlMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// #Pangea
// return Text.rich(
dom.Node parsed = parser.parse(html).body ?? dom.Element.html('');
if (tokens != null) {
parsed = _tokenizeHtml(parsed, html, List.from(tokens!));
@ -687,9 +689,7 @@ class HtmlMessage extends StatelessWidget {
);
}
},
// Pangea#
child: Text.rich(
// #Pangea
textScaler: TextScaler.noScaling,
// Pangea#
_renderHtml(

View file

@ -79,12 +79,19 @@ class ImageBubble extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final borderRadius =
var borderRadius =
this.borderRadius ?? BorderRadius.circular(AppConfig.borderRadius);
final fileDescription = event.fileDescription;
final textColor = this.textColor;
if (fileDescription != null) {
borderRadius = borderRadius.copyWith(
bottomLeft: Radius.zero,
bottomRight: Radius.zero,
);
}
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
@ -122,20 +129,29 @@ class ImageBubble extends StatelessWidget {
if (fileDescription != null && textColor != null)
SizedBox(
width: width,
child: Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
),
],

View file

@ -9,7 +9,6 @@ import 'package:swipe_to_action/swipe_to_action.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
import 'package:fluffychat/pangea/choreographer/enums/use_type.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
@ -17,12 +16,12 @@ import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
import '../../../config/app_config.dart';
import 'message_content.dart';
import 'message_reactions.dart';
import 'reply_content.dart';
import 'state_message.dart';
import 'verification_request_content.dart';
class Message extends StatelessWidget {
final Event event;
@ -30,10 +29,10 @@ class Message extends StatelessWidget {
final Event? previousEvent;
final bool displayReadMarker;
final void Function(Event) onSelect;
final void Function(Event) onAvatarTab;
final void Function(Event) onInfoTab;
final void Function(String) scrollToEventId;
final void Function() onSwipe;
final void Function() onMention;
final bool longPressSelect;
final bool selected;
final Timeline timeline;
@ -42,12 +41,12 @@ class Message extends StatelessWidget {
final void Function()? resetAnimateIn;
final bool wallpaperMode;
final ScrollController scrollController;
final List<Color> colors;
// #Pangea
final bool immersionMode;
final ChatController controller;
final bool isButton;
// Pangea#
final List<Color> colors;
const Message(
this.event, {
@ -57,7 +56,6 @@ class Message extends StatelessWidget {
this.longPressSelect = false,
required this.onSelect,
required this.onInfoTab,
required this.onAvatarTab,
required this.scrollToEventId,
required this.onSwipe,
this.selected = false,
@ -66,13 +64,14 @@ class Message extends StatelessWidget {
this.animateIn = false,
this.resetAnimateIn,
this.wallpaperMode = false,
required this.onMention,
required this.scrollController,
required this.colors,
// #Pangea
required this.immersionMode,
required this.controller,
this.isButton = false,
// Pangea#
required this.colors,
super.key,
});
@ -127,7 +126,7 @@ class Message extends StatelessWidget {
if (event.type == EventTypes.Message &&
event.messageType == EventTypes.KeyVerificationRequest) {
return VerificationRequestContent(event: event, timeline: timeline);
return StateMessage(event);
}
final client = Matrix.of(context).client;
@ -201,10 +200,6 @@ class Message extends StatelessWidget {
event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 3);
final noPadding = {
MessageTypes.File,
MessageTypes.Audio,
}.contains(event.messageType);
if (ownMessage) {
// #Pangea
@ -310,10 +305,14 @@ class Message extends StatelessWidget {
return Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
onTap: () => showMemberActionsPopupMenu(
context: context,
user: user,
onMention: onMention,
),
presenceUserId: user.stateKey,
presenceBackgroundColor:
wallpaperMode ? Colors.transparent : null,
onTap: () => onAvatarTab(event),
);
},
),
@ -442,12 +441,6 @@ class Message extends StatelessWidget {
AppConfig.borderRadius,
),
),
padding: noBubble || noPadding
? EdgeInsets.zero
: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
constraints: const BoxConstraints(
maxWidth:
FluffyThemes.columnWidth *
@ -497,23 +490,35 @@ class Message extends StatelessWidget {
padding:
const EdgeInsets
.only(
bottom: 4.0,
left: 16,
right: 16,
top: 8,
),
child: InkWell(
child: Material(
color: Colors
.transparent,
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
child: InkWell(
borderRadius:
ReplyContent
.borderRadius,
onTap: () =>
scrollToEventId(
replyEvent
.eventId,
),
child:
AbsorbPointer(
child:
ReplyContent(
replyEvent,
ownMessage:
ownMessage,
timeline:
timeline,
),
),
),
),
@ -537,68 +542,41 @@ class Message extends StatelessWidget {
// Pangea#
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
)
// #Pangea
||
(pangeaMessageEvent
?.showUseType ??
false)
// Pangea#
)
timeline,
RelationshipTypes.edit,
))
Padding(
padding:
const EdgeInsets.only(
top: 4.0,
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize:
MainAxisSize.min,
spacing: 4.0,
children: [
// #Pangea
if (pangeaMessageEvent
?.showUseType ??
false) ...[
pangeaMessageEvent!
.msgUseType
.iconView(
Icon(
Icons.edit_outlined,
color: textColor
.withAlpha(164),
size: 14,
),
Text(
displayEvent
.originServerTs
.localizedTimeShort(
context,
textColor.withAlpha(
164,
),
),
const SizedBox(
width: 4,
),
],
if (event
.hasAggregatedEvents(
timeline,
RelationshipTypes
.edit,
)) ...[
// Pangea#
Icon(
Icons.edit_outlined,
style: TextStyle(
color: textColor
.withAlpha(
164,
),
size: 14,
fontSize: 11,
),
Text(
' - ${displayEvent.originServerTs.localizedTimeShort(context)}',
style: TextStyle(
color: textColor
.withAlpha(
164,
),
fontSize: 12,
),
),
],
),
],
),
),

View file

@ -8,10 +8,10 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_rich_text.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -34,6 +34,7 @@ class MessageContent extends StatelessWidget {
final Color linkColor;
final void Function(Event)? onInfoTab;
final BorderRadius borderRadius;
final Timeline timeline;
// #Pangea
final PangeaMessageEvent? pangeaMessageEvent;
//question: are there any performance benefits to using booleans
@ -47,7 +48,6 @@ class MessageContent extends StatelessWidget {
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
// Pangea#
final Timeline timeline;
const MessageContent(
this.event, {
@ -55,6 +55,8 @@ class MessageContent extends StatelessWidget {
super.key,
required this.timeline,
required this.textColor,
required this.linkColor,
required this.borderRadius,
// #Pangea
this.pangeaMessageEvent,
required this.immersionMode,
@ -65,8 +67,6 @@ class MessageContent extends StatelessWidget {
this.isTransitionAnimation = false,
this.readingAssistanceMode,
// Pangea#
required this.linkColor,
required this.borderRadius,
});
// #Pangea
@ -143,7 +143,7 @@ class MessageContent extends StatelessWidget {
const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
), () {
controller.choreographer.tts.tryToSpeak(
TtsController.tryToSpeak(
token.text.content,
langCode: pangeaMessageEvent!.messageDisplayLangCode,
);
@ -245,7 +245,14 @@ class MessageContent extends StatelessWidget {
linkColor: linkColor,
);
case MessageTypes.Video:
return EventVideoPlayer(event, textColor: textColor);
// #Pangea
// return EventVideoPlayer(event, textColor: textColor);
return EventVideoPlayer(
event,
textColor: textColor,
chatController: controller,
);
// Pangea#
case MessageTypes.File:
return MessageDownloadContent(
event,
@ -263,31 +270,38 @@ class MessageContent extends StatelessWidget {
if (event.messageType == MessageTypes.Emote) {
html = '* $html';
}
return HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
// #Pangea
event: event,
overlayController: overlayController,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
isSelected: overlayController != null ? isSelected : null,
onClick: event.isActivityMessage ? null : onClick,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
// Pangea#
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
linkStyle: TextStyle(
color: linkColor,
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: HtmlMessage(
html: html,
textColor: textColor,
room: event.room,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
// #Pangea
event: event,
overlayController: overlayController,
controller: controller,
pangeaMessageEvent: pangeaMessageEvent,
nextEvent: nextEvent,
prevEvent: prevEvent,
isSelected: overlayController != null ? isSelected : null,
onClick: event.isActivityMessage ? null : onClick,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
// Pangea#
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
);
}
// else we fall through to the normal message rendering
@ -372,47 +386,41 @@ class MessageContent extends StatelessWidget {
// Pangea#
// #Pangea
final messageTextStyle =
AppConfig.messageTextStyle(event, textColor);
if (immersionMode && pangeaMessageEvent != null) {
return Flexible(
child: PangeaRichText(
style: messageTextStyle,
if (pangeaMessageEvent != null &&
pangeaMessageEvent!.shouldShowToolbar) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: MessageTokenText(
pangeaMessageEvent: pangeaMessageEvent!,
immersionMode: immersionMode,
isOverlay: overlayController != null,
controller: controller,
tokens:
pangeaMessageEvent!.messageDisplayRepresentation?.tokens,
style: messageTextStyle,
onClick: onClick,
isSelected: overlayController != null ? isSelected : null,
messageMode: overlayController?.toolbarMode,
isHighlighted: (PangeaToken token) =>
overlayController?.toolbarMode.associatedActivityType !=
null &&
overlayController?.practiceSelection
?.hasActiveActivityByToken(
overlayController!
.toolbarMode.associatedActivityType!,
token,
) ==
true,
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
),
);
}
if (pangeaMessageEvent != null &&
pangeaMessageEvent!.shouldShowToolbar) {
return MessageTokenText(
pangeaMessageEvent: pangeaMessageEvent!,
tokens:
pangeaMessageEvent!.messageDisplayRepresentation?.tokens,
style: messageTextStyle,
onClick: onClick,
isSelected: overlayController != null ? isSelected : null,
messageMode: overlayController?.toolbarMode,
isHighlighted: (PangeaToken token) =>
overlayController?.toolbarMode.associatedActivityType !=
null &&
overlayController?.practiceSelection
?.hasActiveActivityByToken(
overlayController!
.toolbarMode.associatedActivityType!,
token,
) ==
true,
overlayController: overlayController,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
);
}
// Pangea#
return
@ -426,27 +434,34 @@ class MessageContent extends StatelessWidget {
prevEvent: prevEvent,
child:
// Pangea#
Linkify(
text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
// #Pangea
// style: TextStyle(
// color: textColor,
// fontSize: bigEmotes ? fontSize * 5 : fontSize,
// decoration: event.redacted ? TextDecoration.lineThrough : null,
// ),
style: messageTextStyle,
// Pangea#
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
),
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
// #Pangea
// style: TextStyle(
// color: textColor,
// fontSize: bigEmotes ? fontSize * 5 : fontSize,
// decoration: event.redacted ? TextDecoration.lineThrough : null,
// ),
style: messageTextStyle,
// Pangea#
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
);
}
@ -504,13 +519,19 @@ class _ButtonContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onPressed,
child: Text(
'$icon $label',
style: TextStyle(
color: textColor,
fontSize: fontSize,
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: onPressed,
child: Text(
'$icon $label',
style: TextStyle(
color: textColor,
fontSize: fontSize,
),
),
),
);

View file

@ -30,83 +30,79 @@ class MessageDownloadContent extends StatelessWidget {
?.tryGet<String>('mimetype')
?.toUpperCase() ??
'UNKNOWN');
final sizeString = event.sizeString;
final sizeString = event.sizeString ?? '?MB';
final fileDescription = event.fileDescription;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
InkWell(
onTap: () => event.saveFile(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
Icons.file_download_outlined,
color: textColor,
),
const SizedBox(width: 16),
Flexible(
child: Text(
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
onTap: () => event.saveFile(context),
child: Container(
width: 400,
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
CircleAvatar(
backgroundColor: textColor.withAlpha(32),
child: Icon(Icons.file_download_outlined, color: textColor),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
filename,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const Divider(height: 1),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row(
children: [
Text(
filetype,
style: TextStyle(
color: linkColor,
),
),
const Spacer(),
if (sizeString != null)
Text(
sizeString,
style: TextStyle(
color: linkColor,
),
'$sizeString | $filetype',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: textColor, fontSize: 10),
),
],
),
],
),
],
),
],
),
),
),
if (fileDescription != null)
Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
if (fileDescription != null) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
],
],
);
}

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -111,7 +112,9 @@ class _Reaction extends StatelessWidget {
final theme = Theme.of(context);
final textColor =
theme.brightness == Brightness.dark ? Colors.white : Colors.black;
final color = theme.colorScheme.surface;
final color = reacted == true
? theme.bubbleColor
: theme.colorScheme.surfaceContainerHigh;
Widget content;
if (reactionKey.startsWith('mxc://')) {
content = Row(
@ -144,7 +147,7 @@ class _Reaction extends StatelessWidget {
content = Text(
renderKey.toString() + (count > 1 ? ' $count' : ''),
style: TextStyle(
color: textColor,
color: reacted == true ? theme.onBubbleColor : textColor,
fontSize: DefaultTextStyle.of(context).style.fontSize,
),
);
@ -156,12 +159,6 @@ class _Reaction extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
color: color,
border: Border.all(
width: 1,
color: reacted!
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),

View file

@ -0,0 +1 @@

View file

@ -10,14 +10,12 @@ class ReplyContent extends StatelessWidget {
final Event replyEvent;
final bool ownMessage;
final Timeline? timeline;
final Color? backgroundColor;
const ReplyContent(
this.replyEvent, {
this.ownMessage = false,
super.key,
this.timeline,
this.backgroundColor,
});
static const BorderRadius borderRadius = BorderRadius.only(
@ -40,16 +38,18 @@ class ReplyContent extends StatelessWidget {
: theme.colorScheme.tertiary;
return Material(
color: backgroundColor ??
theme.colorScheme.surface.withAlpha(ownMessage ? 50 : 80),
color: Colors.transparent,
borderRadius: borderRadius,
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: 3,
width: 5,
height: fontSize * 2 + 16,
color: color,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: color,
),
),
const SizedBox(width: 6),
Flexible(

View file

@ -17,6 +17,7 @@ import 'package:fluffychat/widgets/avatar.dart';
class RoomCreationStateEvent extends StatefulWidget {
// Pangea#
final Event event;
const RoomCreationStateEvent({required this.event, super.key});
// #Pangea
@ -60,56 +61,69 @@ class RoomCreationStateEventState extends State<RoomCreationStateEvent> {
final roomName = event.room.getLocalizedDisplayname(matrixLocals);
return Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Center(
child: Material(
color: theme.colorScheme.surface.withAlpha(128),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
mxContent: event.room.avatar,
name: roomName,
// #Pangea
userId: event.room.directChatMatrixID,
useRive: true,
// Pangea#
size: Avatar.defaultSize * 2,
),
Text(
roomName,
style: theme.textTheme.headlineSmall,
),
Text(
'${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}',
style: theme.textTheme.labelSmall,
),
// #Pangea
InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickMessage,
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
),
onClose: () => setState(() {}),
),
if (_members <= 1 && InstructionsEnum.clickMessage.isToggledOff)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.emptyChatWarning,
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
// #Pangea
// child: Center(
// child: ConstrainedBox(
child: Column(
children: [
ConstrainedBox(
// Pangea#
constraints: const BoxConstraints(maxWidth: 256),
child: Material(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
mxContent: event.room.avatar,
name: roomName,
size: Avatar.defaultSize * 2,
// #Pangea
userId: event.room.directChatMatrixID,
useRive: true,
// Pangea#
),
),
// Pangea#
],
Text(
roomName,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'${event.originServerTs.localizedTime(context)} | ${l10n.countParticipants((event.room.summary.mJoinedMemberCount ?? 1) + (event.room.summary.mInvitedMemberCount ?? 0))}',
style: theme.textTheme.labelSmall,
textAlign: TextAlign.center,
),
],
),
),
),
),
),
// #Pangea
const SizedBox(height: 16.0),
InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickMessage,
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
),
onClose: () => setState(() {}),
),
if (_members <= 1 && InstructionsEnum.clickMessage.isToggledOff)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.emptyChatWarning,
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
),
),
// Pangea#
],
),
);
}

View file

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../../config/app_config.dart';
class VerificationRequestContent extends StatelessWidget {
final Event event;
final Timeline timeline;
const VerificationRequestContent({
required this.event,
required this.timeline,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final events = event.aggregatedEvents(timeline, 'm.reference');
final done = events.where((e) => e.type == EventTypes.KeyVerificationDone);
final start =
events.where((e) => e.type == EventTypes.KeyVerificationStart);
final cancel =
events.where((e) => e.type == EventTypes.KeyVerificationCancel);
final fullyDone = done.length >= 2;
final started = start.isNotEmpty;
final canceled = cancel.isNotEmpty;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Center(
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor,
),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: theme.colorScheme.surface,
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(
Icons.lock_outlined,
color: canceled
? Colors.red
: (fullyDone ? Colors.green : Colors.grey),
),
const SizedBox(width: 8),
Text(
canceled
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
: (fullyDone
? L10n.of(context).verifySuccess
: (started
? L10n.of(context).loadingPleaseWait
: L10n.of(context).newVerificationRequest)),
),
],
),
),
),
);
}
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -12,6 +13,7 @@ import 'package:universal_html/html.dart' as html;
import 'package:video_player/video_player.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
import 'package:fluffychat/utils/file_description.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
@ -25,10 +27,17 @@ class EventVideoPlayer extends StatefulWidget {
final Event event;
final Color? textColor;
final Color? linkColor;
// #Pangea
final ChatController? chatController;
// Pangea#
const EventVideoPlayer(
this.event, {
this.textColor,
this.linkColor,
// #Pangea
this.chatController,
// Pangea#
super.key,
});
@ -37,22 +46,39 @@ class EventVideoPlayer extends StatefulWidget {
}
class EventVideoPlayerState extends State<EventVideoPlayer> {
ChewieController? _chewieManager;
ChewieController? _chewieController;
VideoPlayerController? _videoPlayerController;
bool _isDownloading = false;
String? _networkUri;
File? _tmpFile;
// The video_player package only doesn't support Windows and Linux.
// #Pangea
// final _supportsVideoPlayer =
// !PlatformInfos.isWindows && !PlatformInfos.isLinux;
final _supportsVideoPlayer = !PlatformInfos.isLinux;
StreamSubscription? _stopVideoSubscription;
// Pangea#
void _downloadAction() async {
if (PlatformInfos.isDesktop) {
if (!_supportsVideoPlayer) {
widget.event.saveFile(context);
return;
}
setState(() => _isDownloading = true);
try {
final videoFile = await widget.event.downloadAndDecryptAttachment();
// Dispose the controllers if we already have them.
_disposeControllers();
late VideoPlayerController videoPlayerController;
// Create the VideoPlayerController from the contents of videoFile.
if (kIsWeb) {
final blob = html.Blob([videoFile.bytes]);
_networkUri = html.Url.createObjectUrlFromBlob(blob);
final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob));
videoPlayerController = VideoPlayerController.networkUrl(networkUri);
} else {
final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent(
@ -62,25 +88,28 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
if (await file.exists() == false) {
await file.writeAsBytes(videoFile.bytes);
}
_tmpFile = file;
}
final tmpFile = _tmpFile;
final networkUri = _networkUri;
if (kIsWeb && networkUri != null && _chewieManager == null) {
_chewieManager ??= ChewieController(
videoPlayerController:
VideoPlayerController.networkUrl(Uri.parse(networkUri)),
autoPlay: true,
autoInitialize: true,
);
} else if (!kIsWeb && tmpFile != null && _chewieManager == null) {
_chewieManager ??= ChewieController(
useRootNavigator: false,
videoPlayerController: VideoPlayerController.file(tmpFile),
autoPlay: true,
autoInitialize: true,
);
videoPlayerController = VideoPlayerController.file(file);
}
_videoPlayerController = videoPlayerController;
await videoPlayerController.initialize();
// Create a ChewieController on top.
_chewieController = ChewieController(
videoPlayerController: videoPlayerController,
useRootNavigator: !kIsWeb,
autoPlay: true,
autoInitialize: true,
);
// #Pangea
_stopVideoSubscription?.cancel();
_stopVideoSubscription =
widget.chatController?.stopMediaStream.stream.listen((_) {
_videoPlayerController?.pause();
_chewieController?.pause();
});
// Pangea#
} on IOException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -90,15 +119,23 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
} catch (e, s) {
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
} finally {
// Workaround for Chewie needs time to get the aspectRatio
await Future.delayed(const Duration(milliseconds: 100));
setState(() => _isDownloading = false);
}
}
void _disposeControllers() {
_chewieController?.dispose();
_videoPlayerController?.dispose();
_chewieController = null;
_videoPlayerController = null;
// #Pangea
_stopVideoSubscription?.cancel();
// Pangea#
}
@override
void dispose() {
_chewieManager?.dispose();
_disposeControllers();
super.dispose();
}
@ -118,7 +155,7 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
const width = 300.0;
final chewieManager = _chewieManager;
final chewieController = _chewieController;
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 8,
@ -128,8 +165,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: SizedBox(
height: width,
child: chewieManager != null
? Center(child: Chewie(controller: chewieManager))
child: chewieController != null
? Center(child: Chewie(controller: chewieController))
: Stack(
children: [
if (hasThumbnail)
@ -159,7 +196,9 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
strokeWidth: 2,
),
)
: const Icon(Icons.play_circle_outlined),
: _supportsVideoPlayer
? const Icon(Icons.play_circle_outlined)
: const Icon(Icons.file_download_outlined),
tooltip: _isDownloading
? L10n.of(context).loadingPleaseWait
: L10n.of(context).videoWithSize(
@ -175,20 +214,29 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
if (fileDescription != null && textColor != null && linkColor != null)
SizedBox(
width: width,
child: Linkify(
text: fileDescription,
style: TextStyle(
color: textColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
child: Linkify(
text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle(
color: textColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: linkColor,
fontSize:
AppConfig.fontSizeFactor * AppConfig.messageFontSize,
decoration: TextDecoration.underline,
decorationColor: linkColor,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
),
],

View file

@ -6,14 +6,11 @@ import 'package:emojis/emoji.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:matrix/matrix.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:slugify/slugify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
import 'package:fluffychat/utils/markdown_context_builder.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../widgets/avatar.dart';
import '../../widgets/matrix.dart';
@ -251,7 +248,7 @@ class InputBar extends StatelessWidget {
children: [
Text(
commandExample(command),
style: const TextStyle(fontFamily: 'UbuntuMono'),
style: const TextStyle(fontFamily: 'RobotoMono'),
),
Text(
hint,
@ -271,7 +268,7 @@ class InputBar extends StatelessWidget {
waitDuration: const Duration(days: 1), // don't show on hover
child: Container(
padding: padding,
child: Text(label, style: const TextStyle(fontFamily: 'UbuntuMono')),
child: Text(label, style: const TextStyle(fontFamily: 'RobotoMono')),
),
);
}
@ -328,6 +325,9 @@ class InputBar extends StatelessWidget {
suggestion.tryGet<String>('mxid'),
size: size,
client: client,
// #Pangea
userId: suggestion.tryGet<String>('mxid'),
// Pangea#
),
const SizedBox(width: 6),
// #Pangea
@ -421,226 +421,166 @@ class InputBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final useShortCuts = (AppConfig.sendOnEnter ?? !PlatformInfos.isMobile);
// #Pangea
final enableAutocorrect = MatrixState
.pangeaController.userController.profile.toolSettings.enableAutocorrect;
// Pangea#
return Shortcuts(
shortcuts: !useShortCuts
? {}
: {
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.enter):
NewLineIntent(),
LogicalKeySet(LogicalKeyboardKey.enter): SubmitLineIntent(),
LogicalKeySet(
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyM,
): PasteLineIntent(),
},
child: Actions(
actions: !useShortCuts
? {}
: {
NewLineIntent: CallbackAction(
onInvoke: (i) {
final val = controller!.value;
final selection = val.selection.start;
final messageWithoutNewLine =
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
controller!.value = TextEditingValue(
text: messageWithoutNewLine,
selection: TextSelection.fromPosition(
TextPosition(offset: selection + 1),
),
);
return null;
},
),
SubmitLineIntent: CallbackAction(
onInvoke: (i) {
onSubmitted!(controller!.text);
return null;
},
),
PasteLineIntent: CallbackAction(
onInvoke: (i) async {
final image = await Pasteboard.image;
if (image != null) {
onSubmitImage!(image);
return null;
}
return null;
},
),
},
child: TypeAheadField<Map<String, String?>>(
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
// #Pangea
// if should obscure text (to make it looks that a message has been sent after sending fake message),
// use hideTextController
return TypeAheadField<Map<String, String?>>(
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
// #Pangea
// if should obscure text (to make it looks that a message has been sent after sending fake message),
// use hideTextController
// controller: controller,
// controller: controller,
controller:
(controller?.choreographer.chatController.obscureText) ?? false
? controller?.choreographer.chatController.hideTextController
: controller,
// Pangea#
focusNode: focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
// #Pangea
builder: (context, _, focusNode) {
final textField = TextField(
enableSuggestions: enableAutocorrect,
readOnly: controller != null &&
(controller!.choreographer.isRunningIT ||
controller!.choreographer.chatController.obscureText),
autocorrect: enableAutocorrect,
controller:
(controller?.choreographer.chatController.obscureText) ?? false
? controller?.choreographer.chatController.hideTextController
: controller,
// Pangea#
focusNode: focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
// #Pangea
builder: (context, _, focusNode) {
final textField = TextField(
enableSuggestions: enableAutocorrect,
readOnly: controller != null &&
(controller!.choreographer.isRunningIT ||
controller!.choreographer.chatController.obscureText),
autocorrect: enableAutocorrect,
controller: (controller
?.choreographer.chatController.obscureText) ??
false
? controller?.choreographer.chatController.hideTextController
: controller,
focusNode: focusNode,
contextMenuBuilder: (c, e) => markdownContextBuilder(
c,
e,
_,
),
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
final data = content.data;
if (data == null) return;
contextMenuBuilder: (c, e) => markdownContextBuilder(
c,
e,
_,
),
contentInsertionConfiguration: ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
final data = content.data;
if (data == null) return;
final file = MatrixFile(
mimeType: content.mimeType,
bytes: data,
name: content.uri.split('/').last,
);
room.sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
);
},
),
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
inputFormatters: [
//LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//setting max character count to 1000
//after max, nothing else can be typed
LengthLimitingTextInputFormatter(1000),
],
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
},
style: controller?.exceededMaxLength ?? false
? const TextStyle(color: Colors.red)
: null,
onTap: () {
controller?.onInputTap(
context,
fNode: focusNode,
);
},
decoration: decoration!,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
);
// fix for issue with typing not working sometimes on Firefox and Safari
return Stack(
alignment: Alignment.centerLeft,
children: [
if (controller != null && controller!.text.isEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ShrinkableText(
text: hintText,
maxWidth: double.infinity,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
),
),
kIsWeb ? SelectionArea(child: textField) : textField,
],
final file = MatrixFile(
mimeType: content.mimeType,
bytes: data,
name: content.uri.split('/').last,
);
room.sendFileEvent(
file,
shrinkImageMaxDimension: 1600,
);
},
),
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus!,
inputFormatters: [
//LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
//setting max character count to 1000
//after max, nothing else can be typed
LengthLimitingTextInputFormatter(1000),
],
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted!(text);
},
style: controller?.exceededMaxLength ?? false
? const TextStyle(color: Colors.red)
: null,
onTap: () {
controller?.onInputTap(
context,
fNode: focusNode,
);
},
// builder: (context, controller, focusNode) => TextField(
// controller: controller,
// focusNode: focusNode,
// contextMenuBuilder: (c, e) =>
// markdownContextBuilder(c, e, controller),
// contentInsertionConfiguration: ContentInsertionConfiguration(
// onContentInserted: (KeyboardInsertedContent content) {
// final data = content.data;
// if (data == null) return;
decoration: decoration!,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
);
// fix for issue with typing not working sometimes on Firefox and Safari
return Stack(
alignment: Alignment.centerLeft,
children: [
if (controller != null && controller!.text.isEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ShrinkableText(
text: hintText,
maxWidth: double.infinity,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,
),
),
),
kIsWeb ? SelectionArea(child: textField) : textField,
],
);
},
// builder: (context, controller, focusNode) => TextField(
// controller: controller,
// focusNode: focusNode,
// contextMenuBuilder: (c, e) => markdownContextBuilder(c, e, controller),
// contentInsertionConfiguration: ContentInsertionConfiguration(
// onContentInserted: (KeyboardInsertedContent content) {
// final data = content.data;
// if (data == null) return;
// final file = MatrixFile(
// mimeType: content.mimeType,
// bytes: data,
// name: content.uri.split('/').last,
// );
// room.sendFileEvent(
// file,
// shrinkImageMaxDimension: 1600,
// );
// },
// ),
// minLines: minLines,
// maxLines: maxLines,
// keyboardType: keyboardType!,
// textInputAction: textInputAction,
// autofocus: autofocus!,
// inputFormatters: [
// LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
// ],
// onSubmitted: (text) {
// // fix for library for now
// // it sets the types for the callback incorrectly
// onSubmitted!(text);
// },
// decoration: decoration!,
// onChanged: (text) {
// // fix for the library for now
// // it sets the types for the callback incorrectly
// onChanged!(text);
// },
// textCapitalization: TextCapitalization.sentences,
// ),
// Pangea#
suggestionsCallback: getSuggestions,
itemBuilder: (c, s) =>
buildSuggestion(c, s, Matrix.of(context).client),
onSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) =>
const SizedBox.shrink(),
loadingBuilder: (BuildContext context) => const SizedBox.shrink(),
// fix loading briefly flickering a dark box
emptyBuilder: (BuildContext context) => const SizedBox
.shrink(), // fix loading briefly showing no suggestions
),
),
// final file = MatrixFile(
// mimeType: content.mimeType,
// bytes: data,
// name: content.uri.split('/').last,
// );
// room.sendFileEvent(
// file,
// shrinkImageMaxDimension: 1600,
// );
// },
// ),
// minLines: minLines,
// maxLines: maxLines,
// keyboardType: keyboardType!,
// textInputAction: textInputAction,
// autofocus: autofocus!,
// inputFormatters: [
// LengthLimitingTextInputFormatter((maxPDUSize / 3).floor()),
// ],
// onSubmitted: (text) {
// // fix for library for now
// // it sets the types for the callback incorrectly
// onSubmitted!(text);
// },
// decoration: decoration!,
// onChanged: (text) {
// // fix for the library for now
// // it sets the types for the callback incorrectly
// onChanged!(text);
// },
// textCapitalization: TextCapitalization.sentences,
// ),
// Pangea#
suggestionsCallback: getSuggestions,
itemBuilder: (c, s) => buildSuggestion(c, s, Matrix.of(context).client),
onSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) =>
const SizedBox.shrink(),
loadingBuilder: (BuildContext context) => const SizedBox.shrink(),
// fix loading briefly flickering a dark box
emptyBuilder: (BuildContext context) =>
const SizedBox.shrink(), // fix loading briefly showing no suggestions
);
}
}
class NewLineIntent extends Intent {}
class SubmitLineIntent extends Intent {}
class PasteLineIntent extends Intent {}

View file

@ -11,8 +11,10 @@ import 'package:record/record.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/pangea/toolbar/utils/update_version_dialog.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'events/audio_player.dart';
class RecordingDialog extends StatefulWidget {
@ -35,25 +37,17 @@ class RecordingDialogState extends State<RecordingDialog> {
String? fileName;
static const int bitRate = 64000;
// #Pangea
// static const int samplingRate = 44100;
static const int samplingRate = 22050;
// Pangea#
Future<void> startRecording() async {
final store = Matrix.of(context).store;
try {
// #Pangea
// final codec = kIsWeb
// // Web seems to create webm instead of ogg when using opus encoder
// // which does not play on iOS right now. So we use wav for now:
// ? AudioEncoder.wav
// // Everywhere else we use opus if supported by the platform:
// : await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
// ? AudioEncoder.opus
// : AudioEncoder.aacLc;
const codec = AudioEncoder.wav;
// Pangea#
final codec = kIsWeb
// Web seems to create webm instead of ogg when using opus encoder
// which does not play on iOS right now. So we use wav for now:
? AudioEncoder.wav
// Everywhere else we use opus if supported by the platform:
: await _audioRecorder.isEncoderSupported(AudioEncoder.opus)
? AudioEncoder.opus
: AudioEncoder.aacLc;
fileName =
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
String? path;
@ -71,17 +65,17 @@ class RecordingDialogState extends State<RecordingDialog> {
// #Pangea
final isNotError = await showUpdateVersionDialog(
future: () =>
future: () async =>
// Pangea#
_audioRecorder.start(
const RecordConfig(
bitRate: bitRate,
sampleRate: samplingRate,
numChannels: 1,
autoGain: true,
echoCancel: true,
noiseSuppress: true,
await _audioRecorder.start(
RecordConfig(
bitRate: AppSettings.audioRecordingBitRate.getItem(store),
sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store),
numChannels: AppSettings.audioRecordingNumChannels.getItem(store),
autoGain: AppSettings.audioRecordingAutoGain.getItem(store),
echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store),
noiseSuppress:
AppSettings.audioRecordingNoiseSuppress.getItem(store),
encoder: codec,
),
path: path ?? '',

View file

@ -38,7 +38,6 @@ class ReplyDisplay extends StatelessWidget {
? ReplyContent(
controller.replyEvent!,
timeline: controller.timeline!,
backgroundColor: Colors.transparent,
)
: _EditContent(
controller.editEvent?.getDisplayEvent(controller.timeline!),

View file

@ -1,7 +1,4 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cross_file/cross_file.dart';
@ -16,6 +13,7 @@ import 'package:fluffychat/utils/other_party_can_receive.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/size_string.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/dialog_text_field.dart';
import '../../utils/resize_video.dart';
class SendFileDialog extends StatefulWidget {
@ -40,6 +38,8 @@ class SendFileDialogState extends State<SendFileDialog> {
/// Images smaller than 20kb don't need compression.
static const int minSizeToCompress = 20 * 1000;
final TextEditingController _labelTextController = TextEditingController();
Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(widget.outerContext);
final l10n = L10n.of(context);
@ -96,11 +96,14 @@ class SendFileDialogState extends State<SendFileDialog> {
scaffoldMessenger.clearSnackBars();
}
final label = _labelTextController.text.trim();
try {
await widget.room.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: compress ? 1600 : null,
extraContent: label.isEmpty ? null : {'body': label},
);
} on MatrixException catch (e) {
final retryAfterMs = e.retryAfterMs;
@ -124,7 +127,8 @@ class SendFileDialogState extends State<SendFileDialog> {
await widget.room.sendFileEvent(
file,
thumbnail: thumbnail,
shrinkImageMaxDimension: compress ? null : 1600,
shrinkImageMaxDimension: compress ? 1600 : null,
extraContent: label.isEmpty ? null : {'body': label},
);
}
}
@ -188,6 +192,9 @@ class SendFileDialogState extends State<SendFileDialog> {
sendStr = L10n.of(context).sendVideo;
}
final compressionSupported =
uniqueFileType != 'video' || PlatformInfos.isMobile;
return FutureBuilder<String>(
future: _calcCombinedFileSize(),
builder: (context, snapshot) {
@ -198,129 +205,189 @@ class SendFileDialogState extends State<SendFileDialog> {
title: Text(sendStr),
content: SizedBox(
width: 256,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
if (uniqueFileType == 'image')
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: SizedBox(
height: 256,
child: Center(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.files.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 2,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
if (uniqueFileType == 'image')
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: SizedBox(
height: 256,
child: Center(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.files.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 2,
),
color: Colors.black,
clipBehavior: Clip.hardEdge,
child: FutureBuilder(
future: widget.files[i].readAsBytes(),
builder: (context, snapshot) {
final bytes = snapshot.data;
if (bytes == null) {
return const Center(
child: CircularProgressIndicator
.adaptive(),
);
}
if (snapshot.error != null) {
Logs().w(
'Unable to preview image',
snapshot.error,
snapshot.stackTrace,
);
return const Center(
child: SizedBox(
width: 256,
height: 256,
child: Icon(
Icons.broken_image_outlined,
size: 64,
),
),
);
}
return Image.memory(
bytes,
height: 256,
width: widget.files.length == 1
? 256 - 36
: null,
fit: BoxFit.contain,
errorBuilder: (context, e, s) {
Logs()
.w('Unable to preview image', e, s);
return const Center(
child: SizedBox(
width: 256,
height: 256,
child: Icon(
Icons.broken_image_outlined,
size: 64,
),
),
);
},
);
},
),
),
clipBehavior: Clip.hardEdge,
child: kIsWeb
? Image.network(
widget.files[i].path,
height: 256,
)
: Image.file(
File(widget.files[i].path),
height: 256,
),
),
),
),
),
),
),
if (uniqueFileType != 'image')
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
if (uniqueFileType != 'image')
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
children: [
Icon(
uniqueFileType == null
? Icons.description_outlined
: uniqueFileType == 'video'
? Icons.video_file_outlined
: uniqueFileType == 'audio'
? Icons.audio_file_outlined
: Icons.description_outlined,
size: 32,
),
const SizedBox(width: 8),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'$sizeString - $fileTypes',
style: theme.textTheme.labelSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
if (widget.files.length == 1)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: DialogTextField(
controller: _labelTextController,
labelText: L10n.of(context).optionalMessage,
minLines: 1,
maxLines: 3,
maxLength: 255,
counterText: '',
),
),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
if ({'image', 'video'}.contains(uniqueFileType))
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
uniqueFileType == null
? Icons.description_outlined
: uniqueFileType == 'video'
? Icons.video_file_outlined
: uniqueFileType == 'audio'
? Icons.audio_file_outlined
: Icons.description_outlined,
size: 32,
),
const SizedBox(width: 8),
if ({TargetPlatform.iOS, TargetPlatform.macOS}
.contains(theme.platform))
CupertinoSwitch(
value: compressionSupported && compress,
onChanged: compressionSupported
? (v) => setState(() => compress = v)
: null,
)
else
Switch.adaptive(
value: compressionSupported && compress,
onChanged: compressionSupported
? (v) => setState(() => compress = v)
: null,
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'$sizeString - $fileTypes',
style: theme.textTheme.labelSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
L10n.of(context).compress,
style: theme.textTheme.titleMedium,
textAlign: TextAlign.left,
),
],
),
if (!compress)
Text(
' ($sizeString)',
style: theme.textTheme.labelSmall,
),
if (!compressionSupported)
Text(
L10n.of(context).notSupportedOnThisDevice,
style: theme.textTheme.labelSmall,
),
],
),
),
],
),
),
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
if ({'image', 'video'}.contains(uniqueFileType))
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if ({TargetPlatform.iOS, TargetPlatform.macOS}
.contains(theme.platform))
CupertinoSwitch(
value: compress,
onChanged: uniqueFileType == 'video' &&
!PlatformInfos.isMobile
? null
: (v) => setState(() => compress = v),
)
else
Switch.adaptive(
value: compress,
onChanged: uniqueFileType == 'video' &&
!PlatformInfos.isMobile
? null
: (v) => setState(() => compress = v),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
L10n.of(context).compress,
style: theme.textTheme.titleMedium,
textAlign: TextAlign.left,
),
],
),
if (!compress)
Text(
' ($sizeString)',
style: theme.textTheme.labelSmall,
),
],
),
),
],
),
],
],
),
),
),
actions: <Widget>[

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart' hide Visibility;
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_access_settings/chat_access_settings_page.dart';
@ -179,10 +180,13 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
)) {
return;
}
await showFutureLoadingDialog(
final result = await showFutureLoadingDialog(
context: context,
future: () => room.client.upgradeRoom(room.id, newVersion),
);
if (result.error != null) return;
if (!mounted) return;
context.go('/rooms/${room.id}');
}
Future<void> addAlias() async {

View file

@ -14,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/url_launcher.dart';
import '../../widgets/mxc_image_viewer.dart';
import '../../widgets/qr_code_viewer.dart';
class ChatDetailsView extends StatelessWidget {
@ -37,6 +38,9 @@ class ChatDetailsView extends StatelessWidget {
);
}
final directChatMatrixID = room.directChatMatrixID;
final roomAvatar = room.avatar;
return StreamBuilder(
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id),
@ -57,7 +61,7 @@ class ChatDetailsView extends StatelessWidget {
const Center(child: BackButton()),
elevation: theme.appBarTheme.elevation,
actions: <Widget>[
if (room.canonicalAlias.isNotEmpty) ...[
if (room.canonicalAlias.isNotEmpty)
IconButton(
tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded),
@ -65,8 +69,16 @@ class ChatDetailsView extends StatelessWidget {
context,
room.canonicalAlias,
),
)
else if (directChatMatrixID != null)
IconButton(
tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded),
onPressed: () => showQrCodeViewer(
context,
directChatMatrixID,
),
),
],
if (controller.widget.embeddedCloseButton == null)
ChatSettingsPopupMenu(room, false),
],
@ -101,6 +113,13 @@ class ChatDetailsView extends StatelessWidget {
userId: room.directChatMatrixID,
// Pangea#
size: Avatar.defaultSize * 2.5,
onTap: roomAvatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(roomAvatar),
)
: null,
),
),
if (!room.isDirectChat &&
@ -227,6 +246,8 @@ class ChatDetailsView extends StatelessWidget {
text: room.topic.isEmpty
? L10n.of(context).noChatDescriptionYet
: room.topic,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,

View file

@ -5,9 +5,8 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
import '../../widgets/avatar.dart';
import '../user_bottom_sheet/user_bottom_sheet.dart';
class ParticipantListItem extends StatelessWidget {
final User user;
@ -32,74 +31,72 @@ class ParticipantListItem extends StatelessWidget {
? L10n.of(context).moderator
: '';
return Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,
child: ListTile(
onTap: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: user,
outerContext: context,
return ListTile(
onTap: () => showMemberActionsPopupMenu(context: context, user: user),
title: Row(
children: <Widget>[
Expanded(
child: Text(
user.calcDisplayname(),
overflow: TextOverflow.ellipsis,
),
),
),
title: Row(
children: <Widget>[
Expanded(
if (permissionBatch.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: user.powerLevel >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: Text(
user.calcDisplayname(),
overflow: TextOverflow.ellipsis,
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
),
),
// #Pangea
LevelDisplayName(userId: user.id),
// Pangea#
if (permissionBatch.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: user.powerLevel >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
membershipBatch == null
? const SizedBox.shrink()
: Container(
padding:
const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
permissionBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
),
),
membershipBatch == null
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
membershipBatch,
style: theme.textTheme.labelSmall,
child: Center(
child: Text(
membershipBatch,
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSecondaryContainer,
),
),
),
],
),
subtitle: Text(
user.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: Avatar(
),
],
),
subtitle:
// #Pangea
LevelDisplayName(userId: user.id),
// Text(
// user.id,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// Pangea#
leading: Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5,
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
presenceUserId: user.stateKey,

View file

@ -170,7 +170,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context).unknownEncryptionAlgorithm,
style: TextStyle(
fontFamily: 'UbuntuMono',
fontFamily: 'RobotoMono',
color: theme.colorScheme.secondary,
),
),

View file

@ -8,7 +8,6 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart';
import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/constants/local.key.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
@ -78,11 +77,19 @@ extension LocalizedActiveFilter on ActiveFilter {
class ChatList extends StatefulWidget {
static BuildContext? contextForVoip;
final String? activeChat;
// #Pangea
final String? activeSpaceId;
final String? activeFilter;
// Pangea#
final bool displayNavigationRail;
const ChatList({
super.key,
required this.activeChat,
// #Pangea
this.activeSpaceId,
this.activeFilter,
// Pangea#
this.displayNavigationRail = false,
});
@ -110,7 +117,7 @@ class ChatListController extends State<ChatList>
// #Pangea
if (FluffyThemes.isColumnMode(context)) {
context.push("/rooms/$spaceId/details");
context.push("/rooms/$spaceId");
}
// Pangea#
@ -147,16 +154,9 @@ class ChatListController extends State<ChatList>
future: () async {
if (acceptInvite == OkCancelResult.ok) {
await room.join();
if (room.isSpace) {
setActiveSpace(room.id);
context.go(
FluffyThemes.isColumnMode(context)
? "/rooms/${room.id}/details"
: "/rooms",
);
return;
}
context.go("/rooms/${room.id}");
context.go(
room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}",
);
return;
}
await room.leave();
@ -216,24 +216,21 @@ class ChatListController extends State<ChatList>
return (room) => !room.isAnalyticsRoom && !room.isSpace;
// Pangea#
case ActiveFilter.messages:
// #Pangea
// return (room) => !room.isSpace && room.isDirectChat;
return (room) =>
!room.isSpace &&
room.isDirectChat // #Pangea
&&
!room.isAnalyticsRoom;
!room.isSpace && room.isDirectChat && !room.isAnalyticsRoom;
// Pangea#
case ActiveFilter.groups:
// #Pangea
// return (room) => !room.isSpace && !room.isDirectChat;
return (room) =>
!room.isSpace &&
!room.isDirectChat // #Pangea
&&
!room.isAnalyticsRoom;
!room.isSpace && !room.isDirectChat && !room.isAnalyticsRoom;
// Pangea#
case ActiveFilter.unread:
return (room) =>
room.isUnreadOrInvited // #Pangea
&&
!room.isAnalyticsRoom;
// #Pangea
// return (room) => room.isUnreadOrInvited;
return (room) => room.isUnreadOrInvited && !room.isAnalyticsRoom;
// Pangea#
case ActiveFilter.spaces:
return (room) => room.isSpace;
@ -473,7 +470,6 @@ class ChatListController extends State<ChatList>
}
//#Pangea
StreamSubscription? classStream;
StreamSubscription? _invitedSpaceSubscription;
StreamSubscription? _subscriptionStatusStream;
StreamSubscription? _spaceChildSubscription;
@ -510,20 +506,6 @@ class ChatListController extends State<ChatList>
_checkTorBrowser();
//#Pangea
classStream = MatrixState.pangeaController.classController.stateStream
.listen((event) {
if (!mounted || event is! Map<String, dynamic>) return;
if (event.containsKey("activeSpaceId")) {
final setSpaceID = event["activeSpaceId"];
setSpaceID != null ? setActiveSpace(setSpaceID) : clearActiveSpace();
if (setSpaceID != null) {
context.push("/rooms/$setSpaceID/details");
}
} else if (event.containsKey("activeFilter")) {
setActiveFilter(event["activeFilter"]);
}
});
_invitedSpaceSubscription = MatrixState
.pangeaController.matrixState.client.onSync.stream
.where((event) => event.rooms?.invite != null)
@ -550,11 +532,11 @@ class ChatListController extends State<ChatList>
// #Pangea
final String? justInputtedCode =
MatrixState.pangeaController.classController.chatBox.read(
PLocalKey.justInputtedCode,
);
MatrixState.pangeaController.classController.justInputtedCode();
final newSpaceCode = space?.classCode(context);
if (newSpaceCode == justInputtedCode) return;
if (newSpaceCode?.toLowerCase() == justInputtedCode?.toLowerCase()) {
return;
}
if (space != null) {
chatListHandleSpaceTap(
@ -638,6 +620,15 @@ class ChatListController extends State<ChatList>
);
}
});
_activeSpaceId =
widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId;
if (widget.activeFilter == 'groups') {
activeFilter = AppConfig.separateChatTypes
? ActiveFilter.groups
: ActiveFilter.allChats;
}
// Pangea#
super.initState();
@ -647,15 +638,21 @@ class ChatListController extends State<ChatList>
@override
void didUpdateWidget(ChatList oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback((_) {
final params = GoRouterState.of(context).uri.queryParameters;
if (!params.containsKey("filter") || params['filter'] != 'groups') return;
if (widget.activeFilter != oldWidget.activeFilter &&
widget.activeFilter == 'groups') {
setActiveFilter(
AppConfig.separateChatTypes
? ActiveFilter.groups
: ActiveFilter.allChats,
);
});
}
if (widget.activeSpaceId != oldWidget.activeSpaceId &&
widget.activeSpaceId != null) {
widget.activeSpaceId == 'clear'
? clearActiveSpace()
: setActiveSpace(widget.activeSpaceId!);
}
}
// Pangea#
@ -665,7 +662,6 @@ class ChatListController extends State<ChatList>
_intentFileStreamSubscription?.cancel();
_intentUriStreamSubscription?.cancel();
//#Pangea
classStream?.cancel();
_invitedSpaceSubscription?.cancel();
_subscriptionStatusStream?.cancel();
_spaceChildSubscription?.cancel();

View file

@ -11,13 +11,11 @@ import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/dummy_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/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/chat_list/widgets/pangea_chat_list_header.dart';
import 'package:fluffychat/pangea/public_spaces/pangea_public_room_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import '../../config/themes.dart';
import '../../widgets/matrix.dart';
@ -135,12 +133,9 @@ class ChatListViewBody extends StatelessWidget {
// .results[i].userId.localpart ??
// L10n.of(context).unknownDevice,
// avatar: userSearchResult.results[i].avatarUrl,
// onPressed: () => showAdaptiveBottomSheet(
// onPressed: () => UserDialog.show(
// context: context,
// builder: (c) => UserBottomSheet(
// profile: userSearchResult.results[i],
// outerContext: context,
// ),
// profile: userSearchResult.results[i],
// ),
// ),
// ),
@ -175,16 +170,16 @@ class ChatListViewBody extends StatelessWidget {
),
// #Pangea
// if (client.rooms.isNotEmpty && !controller.isSearchMode)
// SizedBox(
// height: 64,
// child: ListView(
// padding: const EdgeInsets.symmetric(
// horizontal: 12.0,
// vertical: 12.0,
// ),
// shrinkWrap: true,
// scrollDirection: Axis.horizontal,
if (!controller.isSearchMode)
// SizedBox(
// height: 64,
// child: ListView(
// padding: const EdgeInsets.symmetric(
// horizontal: 12.0,
// vertical: 16.0,
// ),
// shrinkWrap: true,
// scrollDirection: Axis.horizontal,
SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
@ -211,64 +206,26 @@ class ChatListViewBody extends StatelessWidget {
ActiveFilter.spaces,
]
.map(
(filter) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4),
child: HoverBuilder(
builder: (context, hovered) =>
AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: InkWell(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
onTap: () =>
controller.setActiveFilter(filter),
// #Pangea
child: UnreadRoomsBadge(
filter: (_) =>
filter == ActiveFilter.unread,
badgePosition: BadgePosition.topEnd(
top: -12,
end: -6,
),
// Pangea#
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: filter ==
controller.activeFilter
? theme.colorScheme.primary
: theme.colorScheme
.secondaryContainer,
borderRadius:
BorderRadius.circular(
AppConfig.borderRadius,
),
),
alignment: Alignment.center,
child: Text(
filter.toLocalizedString(context),
style: TextStyle(
fontWeight: filter ==
controller.activeFilter
? FontWeight.w500
: FontWeight.normal,
color: filter ==
controller.activeFilter
? theme
.colorScheme.onPrimary
: theme.colorScheme
.onSecondaryContainer,
),
),
),
),
// #Pangea
// (filter) => Padding(
(filter) => UnreadRoomsBadge(
filter: (_) => filter == ActiveFilter.unread,
badgePosition: BadgePosition.topEnd(
top: -12,
end: -6,
),
child: Padding(
// Pangea#
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected:
filter == controller.activeFilter,
onSelected: (_) =>
controller.setActiveFilter(filter),
label: Text(
filter.toLocalizedString(context),
),
),
),
@ -410,7 +367,10 @@ class PublicRoomsHorizontalListState extends State<PublicRoomsHorizontalList> {
Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child: ListView.builder(
child:
// Pangea#
ListView.builder(
// #Pangea
controller: _scrollController,
// Pangea#
scrollDirection: Axis.horizontal,
@ -423,19 +383,21 @@ class PublicRoomsHorizontalListState extends State<PublicRoomsHorizontalList> {
L10n.of(context).chat,
// Pangea#
avatar: publicRooms[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
// #Pangea
// builder: (c) => PublicRoomBottomSheet(
builder: (c) => PangeaPublicRoomBottomSheet(
// Pangea#
roomAlias: publicRooms[i].canonicalAlias ??
publicRooms[i].roomId,
outerContext: context,
chunk: publicRooms[i],
),
),
// #Pangea
onPressed: () => PublicRoomBottomSheet.show(
context: context,
roomAlias:
publicRooms[i].canonicalAlias ?? publicRooms[i].roomId,
chunk: publicRooms[i],
),
// onPressed: () => showAdaptiveDialog(
// context: context,
// builder: (c) => PublicRoomDialog(
// roomAlias: publicRooms[i].canonicalAlias ??
// publicRooms[i].roomId,
// chunk: publicRooms[i],
// ),
// ),
radius: BorderRadius.circular(
AppConfig.borderRadius / 2,
),
@ -533,12 +495,9 @@ class UserSearchResultsListState extends State<UserSearchResultsList> {
widget.userSearchResult.results[i].userId.localpart ??
L10n.of(context).unknownDevice,
avatar: widget.userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
onPressed: () => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: widget.userSearchResult.results[i],
outerContext: context,
),
profile: widget.userSearchResult.results[i],
),
),
),

View file

@ -109,351 +109,331 @@ class ChatListItem extends StatelessWidget {
: room.getState(EventTypes.RoomMember, lastEvent.senderId) == null;
final space = this.space;
return Dismissible(
key: ValueKey(room.id),
confirmDismiss: (_) => archiveAction(context),
onDismissed: (_) {
// Empty dismissed callback to trigger the dismiss animation
},
background: Material(
color: theme.colorScheme.errorContainer,
child: Icon(
Icons.archive_outlined,
color: theme.colorScheme.onErrorContainer,
),
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 1,
),
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
color: backgroundColor,
child: FutureBuilder(
future: room.loadHeroUsers(),
builder: (context, snapshot) => HoverBuilder(
builder: (context, listTileHovered) => ListTile(
visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
onLongPress: () => onLongPress?.call(context),
leading: HoverBuilder(
builder: (context, hovered) => AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: SizedBox(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: Stack(
children: [
if (space != null)
Positioned(
top: 0,
left: 0,
child: Avatar(
border: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
mxContent: space.avatar,
size: Avatar.defaultSize * 0.75,
name: space.getLocalizedDisplayname(),
// #Pangea
userId: space.directChatMatrixID,
useRive: true,
// Pangea#
onTap: () => onLongPress?.call(context),
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? room.isSpace
? BorderSide(
width: 1,
color: theme.dividerColor,
)
: null
: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: room.isSpace
? BorderRadius.circular(
AppConfig.borderRadius / 4,
)
: null,
mxContent: room.avatar,
size: space != null
? Avatar.defaultSize * 0.75
: Avatar.defaultSize,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: () => onLongPress?.call(context),
),
),
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge,
color: backgroundColor,
child: FutureBuilder(
future: room.loadHeroUsers(),
builder: (context, snapshot) => HoverBuilder(
builder: (context, listTileHovered) => ListTile(
visualDensity: const VisualDensity(vertical: -0.5),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
onLongPress: () => onLongPress?.call(context),
leading: HoverBuilder(
builder: (context, hovered) => AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: hovered ? 1.1 : 1.0,
child: SizedBox(
width: Avatar.defaultSize,
height: Avatar.defaultSize,
child: Stack(
children: [
if (space != null)
Positioned(
top: 0,
right: 0,
child: GestureDetector(
left: 0,
child: Avatar(
border: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius / 4,
),
mxContent: space.avatar,
size: Avatar.defaultSize * 0.75,
name: space.getLocalizedDisplayname(),
// #Pangea
userId: space.directChatMatrixID,
useRive: true,
// Pangea#
onTap: () => onLongPress?.call(context),
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: listTileHovered ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: const Icon(
Icons.arrow_drop_down_circle_outlined,
size: 18,
),
),
Positioned(
bottom: 0,
right: 0,
child: Avatar(
border: space == null
? room.isSpace
? BorderSide(
width: 1,
color: theme.dividerColor,
)
: null
: BorderSide(
width: 2,
color: backgroundColor ??
theme.colorScheme.surface,
),
borderRadius: room.isSpace
? BorderRadius.circular(
AppConfig.borderRadius / 4,
)
: null,
mxContent: room.avatar,
size: space != null
? Avatar.defaultSize * 0.75
: Avatar.defaultSize,
name: displayname,
presenceUserId: directChatMatrixId,
presenceBackgroundColor: backgroundColor,
onTap: () => onLongPress?.call(context),
),
),
Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: () => onLongPress?.call(context),
child: AnimatedScale(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
scale: listTileHovered ? 1.0 : 0.0,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(16),
child: const Icon(
Icons.arrow_drop_down_circle_outlined,
size: 18,
),
),
),
),
],
),
),
],
),
),
),
title: Row(
children: <Widget>[
Expanded(
child: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.w500
: null,
),
),
title: Row(
children: <Widget>[
Expanded(
child: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.w500
: null,
),
),
if (isMuted)
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.notifications_off_outlined,
size: 16,
),
),
if (room.isFavourite)
Padding(
padding: EdgeInsets.only(
right: hasNotifications ? 4.0 : 0.0,
),
child: Icon(
Icons.push_pin,
size: 16,
color: theme.colorScheme.primary,
),
),
if (!room.isSpace &&
lastEvent != null &&
room.membership != Membership.invite)
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
lastEvent.originServerTs.localizedTimeShort(context),
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
),
],
),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (typingText.isEmpty &&
ownMessage &&
room.lastEvent!.status.isSending) ...[
const SizedBox(
width: 16,
height: 16,
child:
CircularProgressIndicator.adaptive(strokeWidth: 2),
),
const SizedBox(width: 4),
],
AnimatedContainer(
width: typingText.isEmpty ? 0 : 18,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
padding: const EdgeInsets.only(right: 4),
),
if (isMuted)
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.edit_outlined,
color: theme.colorScheme.secondary,
size: 14,
Icons.notifications_off_outlined,
size: 16,
),
),
Expanded(
child: room.isSpace && room.membership == Membership.join
? Text(
L10n.of(context).countChatsAndCountParticipants(
// #Pangea
// room.spaceChildren.length,
room.spaceChildCount,
// Pangea#
(room.summary.mJoinedMemberCount ?? 1),
),
style:
TextStyle(color: theme.colorScheme.outline),
)
: typingText.isNotEmpty
? Text(
typingText,
style: TextStyle(
color: theme.colorScheme.primary,
),
maxLines: 1,
softWrap: false,
)
if (room.isFavourite)
Padding(
padding: EdgeInsets.only(
right: hasNotifications ? 4.0 : 0.0,
),
child: Icon(
Icons.push_pin,
size: 16,
color: theme.colorScheme.primary,
),
),
if (!room.isSpace &&
lastEvent != null &&
room.membership != Membership.invite)
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
lastEvent.originServerTs.localizedTimeShort(context),
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
),
],
),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (typingText.isEmpty &&
ownMessage &&
room.lastEvent!.status.isSending) ...[
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
const SizedBox(width: 4),
],
AnimatedContainer(
width: typingText.isEmpty ? 0 : 18,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
padding: const EdgeInsets.only(right: 4),
child: Icon(
Icons.edit_outlined,
color: theme.colorScheme.secondary,
size: 14,
),
),
Expanded(
child: room.isSpace && room.membership == Membership.join
? Text(
L10n.of(context).countChatsAndCountParticipants(
// #Pangea
: room.lastEvent != null
? ChatListItemSubtitle(
event: room.lastEvent,
style: TextStyle(
fontWeight:
unread || room.hasNewMessages
? FontWeight.bold
: null,
color:
theme.colorScheme.onSurfaceVariant,
),
)
// Pangea#
: FutureBuilder(
key: ValueKey(
'${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}',
),
future: needLastEventSender
? lastEvent.calcLocalizedBody(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix:
(!isDirectChat ||
directChatMatrixId !=
room.lastEvent
?.senderId),
)
// room.spaceChildren.length,
room.spaceChildCount,
// Pangea#
(room.summary.mJoinedMemberCount ?? 1),
),
style: TextStyle(color: theme.colorScheme.outline),
)
: typingText.isNotEmpty
? Text(
typingText,
style: TextStyle(
color: theme.colorScheme.primary,
),
maxLines: 1,
softWrap: false,
)
// #Pangea
: room.lastEvent != null
? ChatListItemSubtitle(
event: room.lastEvent,
style: TextStyle(
fontWeight: unread || room.hasNewMessages
? FontWeight.bold
: null,
initialData:
lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? room
.getState(
EventTypes.RoomMember,
room.client.userID!,
)
?.content
.tryGet<String>('reason') ??
(isDirectChat
? L10n.of(context)
.newChatRequest
// #Pangea
// : L10n.of(context)
// .inviteGroupChat)
: L10n.of(context)
.inviteChat)
// Pangea#
: snapshot.data ??
L10n.of(context).emptyChat,
softWrap: false,
maxLines:
room.notificationCount >= 1 ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread || room.hasNewMessages
? theme.colorScheme.onSurface
: theme.colorScheme.outline,
decoration:
room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
color: theme.colorScheme.onSurfaceVariant,
),
)
// Pangea#
: FutureBuilder(
key: ValueKey(
'${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}',
),
future: needLastEventSender
? lastEvent.calcLocalizedBody(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix:
(!isDirectChat ||
directChatMatrixId !=
room.lastEvent
?.senderId),
)
: null,
initialData:
lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? room
.getState(
EventTypes.RoomMember,
room.client.userID!,
)
?.content
.tryGet<String>('reason') ??
(isDirectChat
? L10n.of(context)
.newChatRequest
// #Pangea
// : L10n.of(context)
// .inviteGroupChat)
: L10n.of(context).inviteChat)
// Pangea#
: snapshot.data ??
L10n.of(context).emptyChat,
softWrap: false,
maxLines:
room.notificationCount >= 1 ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread || room.hasNewMessages
? theme.colorScheme.onSurface
: theme.colorScheme.outline,
decoration:
room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
),
),
),
const SizedBox(width: 8),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 7),
height: unreadBubbleSize,
width: !hasNotifications && !unread && !room.hasNewMessages
? 0
: (unreadBubbleSize - 9) *
room.notificationCount.toString().length +
9,
decoration: BoxDecoration(
color: room.highlightCount > 0 ||
room.membership == Membership.invite
? theme.colorScheme.error
: hasNotifications || room.markedUnread
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(7),
),
const SizedBox(width: 8),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 7),
height: unreadBubbleSize,
width:
!hasNotifications && !unread && !room.hasNewMessages
? 0
: (unreadBubbleSize - 9) *
room.notificationCount.toString().length +
9,
decoration: BoxDecoration(
color: room.highlightCount > 0 ||
room.membership == Membership.invite
? theme.colorScheme.onError
: hasNotifications || room.markedUnread
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(7),
),
child: hasNotifications
? Text(
room.notificationCount.toString(),
style: TextStyle(
color: room.highlightCount > 0 ||
room.membership == Membership.invite
? theme.colorScheme.onError
: hasNotifications
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
fontSize: 13,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
)
: const SizedBox.shrink(),
),
],
),
onTap: onTap,
trailing: onForget == null
? null
: IconButton(
icon: const Icon(Icons.delete_outlined),
onPressed: onForget,
),
child: hasNotifications
? Text(
room.notificationCount.toString(),
style: TextStyle(
color: room.highlightCount > 0 ||
room.membership == Membership.invite
? theme.colorScheme.onError
: hasNotifications
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
fontSize: 13,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
)
: const SizedBox.shrink(),
),
],
),
onTap: onTap,
trailing: onForget == null
? null
: IconButton(
icon: const Icon(Icons.delete_outlined),
onPressed: onForget,
),
),
),
),

View file

@ -99,44 +99,43 @@ class ClientChooserButton extends StatelessWidget {
],
),
),
...matrix.accountBundles[bundle]!.map(
(client) => PopupMenuItem(
value: client,
child: FutureBuilder<Profile?>(
// analyzer does not understand this type cast for error
// handling
//
// ignore: unnecessary_cast
future: (client!.fetchOwnProfile() as Future<Profile?>)
.onError((e, s) => null),
builder: (context, snapshot) => Row(
children: [
Avatar(
mxContent: snapshot.data?.avatarUrl,
name:
snapshot.data?.displayName ?? client.userID!.localpart,
size: 32,
...matrix.accountBundles[bundle]!
.whereType<Client>()
.where((client) => client.isLogged())
.map(
(client) => PopupMenuItem(
value: client,
child: FutureBuilder<Profile?>(
future: client.fetchOwnProfile(),
builder: (context, snapshot) => Row(
children: [
Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID!.localpart,
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Text(
snapshot.data?.displayName ??
client.userID!.localpart!,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => controller.editBundlesForAccount(
client.userID,
bundle,
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Text(
snapshot.data?.displayName ?? client.userID!.localpart!,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => controller.editBundlesForAccount(
client.userID,
bundle,
),
),
],
),
),
),
),
),
],
PopupMenuItem(
value: SettingsAction.addAccount,

View file

@ -15,10 +15,12 @@ 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/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/public_spaces/pangea_public_room_bottom_sheet.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/pangea/spaces/widgets/space_view_leaderboard.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -26,10 +28,7 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum AddRoomType {
chat,
subspace,
}
enum AddRoomType { chat, subspace }
class SpaceView extends StatefulWidget {
final String spaceId;
@ -92,7 +91,7 @@ class _SpaceViewState extends State<SpaceView> {
// and reload the hierarchy when they come through
final client = Matrix.of(context).client;
_roomSubscription ??= client.onSync.stream
.where(hasHierarchyUpdate)
.where(_hasHierarchyUpdate)
.listen((update) => loadHierarchy(hasUpdate: true));
// Pangea#
super.initState();
@ -127,6 +126,44 @@ class _SpaceViewState extends State<SpaceView> {
super.dispose();
}
Future<void> _joinDefaultChats() async {
if (_discoveredChildren == null) return;
final found = List<SpaceRoomsChunk>.from(_discoveredChildren!);
final List<Future> joinFutures = [];
for (final chunk in found) {
if (chunk.canonicalAlias == null) continue;
final alias = chunk.canonicalAlias!;
final isDefaultChat = (alias.localpart ?? '')
.startsWith(SpaceConstants.announcementsChatAlias) ||
(alias.localpart ?? '')
.startsWith(SpaceConstants.introductionChatAlias);
if (!isDefaultChat) continue;
joinFutures.add(
Matrix.of(context).client.joinRoom(alias).then((_) {
_discoveredChildren?.remove(chunk);
}).catchError((e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'alias': alias,
'spaceId': widget.spaceId,
},
);
return null;
}),
);
}
if (joinFutures.isNotEmpty) {
await Future.wait(joinFutures);
}
}
Future<void> loadHierarchy({hasUpdate = false}) async {
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
if (room == null) return;
@ -137,6 +174,7 @@ class _SpaceViewState extends State<SpaceView> {
try {
await _loadHierarchy(activeSpace: room, hasUpdate: hasUpdate);
await _joinDefaultChats();
} catch (e, s) {
Logs().w('Unable to load hierarchy', e, s);
if (!mounted) return;
@ -211,12 +249,12 @@ class _SpaceViewState extends State<SpaceView> {
// finally, set the response to the last response for this space
// and set the current next batch token
currentHierarchy = filterHierarchyResponse(activeSpace, response.rooms);
currentHierarchy = _filterHierarchyResponse(activeSpace, response.rooms);
currentNextBatch = response.nextBatch;
}
_discoveredChildren = currentHierarchy;
_discoveredChildren?.sort(sortSpaceChildren);
_discoveredChildren?.sort(_sortSpaceChildren);
_nextBatch = currentNextBatch;
}
@ -263,24 +301,34 @@ class _SpaceViewState extends State<SpaceView> {
final client = Matrix.of(context).client;
final space = client.getRoomById(widget.spaceId);
final joined = await showAdaptiveBottomSheet<bool>(
// #Pangea
// final joined = await showAdaptiveDialog<bool>(
// context: context,
// builder: (_) => PublicRoomDialog(
// chunk: item,
// via: space?.spaceChildren
// .firstWhereOrNull(
// (child) => child.roomId == item.roomId,
// )
// ?.via,
// ),
// );
final joined = await PublicRoomBottomSheet.show(
context: context,
// #Pangea
// builder: (_) => PublicRoomBottomSheet(
builder: (_) => PangeaPublicRoomBottomSheet(
// Pangea#
outerContext: context,
chunk: item,
via: space?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == item.roomId,
)
?.via,
),
chunk: item,
via: space?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == item.roomId,
)
?.via,
);
// Pangea#
if (mounted && joined == true) {
setState(() {
// #Pangea
// _discoveredChildren.remove(item);
_discoveredChildren?.remove(item);
// Pangea#
});
}
}
@ -398,7 +446,7 @@ class _SpaceViewState extends State<SpaceView> {
// Pangea#
// #Pangea
bool includeSpaceChild(
bool _includeSpaceChild(
Room space,
SpaceRoomsChunk hierarchyMember,
) {
@ -416,7 +464,7 @@ class _SpaceViewState extends State<SpaceView> {
return !isAnalyticsRoom && (isMember || isSuggested);
}
List<SpaceRoomsChunk> filterHierarchyResponse(
List<SpaceRoomsChunk> _filterHierarchyResponse(
Room space,
List<SpaceRoomsChunk> hierarchyResponse,
) {
@ -432,7 +480,7 @@ class _SpaceViewState extends State<SpaceView> {
);
if (isDuplicate) continue;
if (includeSpaceChild(space, child)) {
if (_includeSpaceChild(space, child)) {
filteredChildren.add(child);
}
}
@ -441,7 +489,7 @@ class _SpaceViewState extends State<SpaceView> {
/// Used to filter out sync updates with hierarchy updates for the active
/// space so that the view can be auto-reloaded in the room subscription
bool hasHierarchyUpdate(SyncUpdate update) {
bool _hasHierarchyUpdate(SyncUpdate update) {
final joinTimeline = update.rooms?.join?[widget.spaceId]?.timeline;
final leaveTimeline = update.rooms?.leave?[widget.spaceId]?.timeline;
if (joinTimeline == null && leaveTimeline == null) return false;
@ -456,7 +504,7 @@ class _SpaceViewState extends State<SpaceView> {
return hasJoinUpdate || hasLeaveUpdate;
}
int sortSpaceChildren(
int _sortSpaceChildren(
SpaceRoomsChunk a,
SpaceRoomsChunk b,
) {
@ -470,19 +518,6 @@ class _SpaceViewState extends State<SpaceView> {
}
return 0;
}
List<Room>? get joinedRooms {
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
if (room == null) return null;
final spaceChildIds =
room.spaceChildren.map((c) => c.roomId).whereType<String>().toSet();
return room.client.rooms
.where((room) => spaceChildIds.contains(room.id))
.where((room) => !room.isAnalyticsRoom)
.toList();
}
// Pangea#
@override
@ -636,6 +671,16 @@ class _SpaceViewState extends State<SpaceView> {
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
// #Pangea
SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return SpaceViewLeaderboard(
space: room,
);
},
),
// Pangea#
SliverAppBar(
floating: true,
toolbarHeight: 72,
@ -734,7 +779,10 @@ class _SpaceViewState extends State<SpaceView> {
},
),
SliverList.builder(
// #Pangea
// itemCount: _discoveredChildren.length + 2,
itemCount: (_discoveredChildren?.length ?? 0) + 2,
// Pangea#
itemBuilder: (context, i) {
if (i == 0) {
return SearchTitle(
@ -743,7 +791,10 @@ class _SpaceViewState extends State<SpaceView> {
);
}
i--;
// #Pangea
// if (i == _discoveredChildren.length) {
if (i == (_discoveredChildren?.length ?? 0)) {
// Pangea#
if (_noMoreRooms) {
return Padding(
padding: const EdgeInsets.all(12.0),
@ -761,7 +812,10 @@ class _SpaceViewState extends State<SpaceView> {
vertical: 2.0,
),
child: TextButton(
// #Pangea
// onPressed: _isLoading ? null : _loadHierarchy,
onPressed: _isLoading ? null : loadHierarchy,
// Pangea#
child: _isLoading
? LinearProgressIndicator(
borderRadius: BorderRadius.circular(
@ -772,7 +826,10 @@ class _SpaceViewState extends State<SpaceView> {
),
);
}
// #Pangea
// final item = _discoveredChildren[i];
final item = _discoveredChildren![i];
// Pangea#
final displayname = item.name ??
item.canonicalAlias ??
L10n.of(context).emptyChat;

View file

@ -4,15 +4,15 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class StatusMessageList extends StatelessWidget {
final void Function() onStatusEdit;
const StatusMessageList({
required this.onStatusEdit,
super.key,
@ -24,12 +24,9 @@ class StatusMessageList extends StatelessWidget {
final client = Matrix.of(context).client;
if (profile.userId == client.userID) return onStatusEdit();
showAdaptiveBottomSheet(
UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: profile,
outerContext: context,
),
profile: profile,
);
return;
}
@ -293,6 +290,7 @@ extension on CachedPresence {
(currentlyActive == true
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(0));
LinearGradient get gradient => presence.isOnline == true
? LinearGradient(
colors: [

View file

@ -4,12 +4,12 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import '../../widgets/matrix.dart';
import 'chat_members_view.dart';
class ChatMembersPage extends StatefulWidget {
final String roomId;
const ChatMembersPage({required this.roomId, super.key});
@override
@ -20,16 +20,27 @@ class ChatMembersController extends State<ChatMembersPage> {
List<User>? members;
List<User>? filteredMembers;
Object? error;
Membership membershipFilter = Membership.join;
final TextEditingController filterController = TextEditingController();
// #Pangea
StreamSubscription? _subscription;
// Pangea#
void setMembershipFilter(Membership membership) {
membershipFilter = membership;
setFilter();
}
void setFilter([_]) async {
final filter = filterController.text.toLowerCase().trim();
final members = this
.members
?.where(
(member) =>
membershipFilter == Membership.join ||
member.membership == membershipFilter,
)
.toList();
if (filter.isEmpty) {
setState(() {
filteredMembers = members
@ -49,7 +60,8 @@ class ChatMembersController extends State<ChatMembersPage> {
});
}
void refreshMembers() async {
void refreshMembers([_]) async {
Logs().d('Load room members from', widget.roomId);
try {
setState(() {
error = null;
@ -58,15 +70,16 @@ class ChatMembersController extends State<ChatMembersPage> {
.client
.getRoomById(widget.roomId)
?.requestParticipants(
// #Pangea
// without setting cache to true, each call to requestParticipants will
// result in a new entry in the roomState stream, because the member roomState is not
// stored in the database. This causes an infinite loop with the roomState listener.
[Membership.join, Membership.invite, Membership.knock],
false,
true,
// Pangea#
);
// #Pangea
// [...Membership.values]..remove(Membership.leave),
// without setting cache to true, each call to requestParticipants will
// result in a new entry in the roomState stream, because the member roomState is not
// stored in the database. This causes an infinite loop with the roomState listener.
[...Membership.values]..remove(Membership.leave),
false,
true,
// Pangea#
);
if (!mounted) return;
@ -83,29 +96,34 @@ class ChatMembersController extends State<ChatMembersPage> {
}
}
StreamSubscription? _updateSub;
@override
void initState() {
super.initState();
refreshMembers();
// #Pangea
_subscription = Matrix.of(context)
_updateSub = Matrix.of(context)
.client
.onRoomState
.onSync
.stream
.where((update) => update.roomId == widget.roomId)
.rateLimit(const Duration(seconds: 1))
.listen((_) => refreshMembers());
// Pangea#
.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[widget.roomId]?.timeline?.events
?.any((state) => state.type == EventTypes.RoomMember) ??
false,
)
.listen(refreshMembers);
}
// #Pangea
@override
void dispose() {
_subscription?.cancel();
_updateSub?.cancel();
// #Pangea
filterController.dispose();
// Pangea#
super.dispose();
}
// Pangea#
@override
Widget build(BuildContext context) => ChatMembersView(this);

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import '../../widgets/layouts/max_width_body.dart';
@ -11,6 +12,7 @@ import 'chat_members.dart';
class ChatMembersView extends StatelessWidget {
final ChatMembersController controller;
const ChatMembersView(this.controller, {super.key});
@override
@ -84,29 +86,89 @@ class ChatMembersView extends StatelessWidget {
: ListView.builder(
shrinkWrap: true,
itemCount: members.length + 1,
itemBuilder: (context, i) => i == 0
? Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: controller.filterController,
onChanged: controller.setFilter,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
itemBuilder: (context, i) {
if (i == 0) {
final availableFilters = Membership.values
.where(
(membership) =>
controller.members?.any(
(member) => member.membership == membership,
) ??
false,
)
.toList();
availableFilters
.sort((a, b) => a == Membership.join ? -1 : 1);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: controller.filterController,
onChanged: controller.setFilter,
decoration: InputDecoration(
filled: true,
fillColor:
theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).search,
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).search,
),
),
)
: ParticipantListItem(members[i - 1]),
if (availableFilters.length > 1)
SizedBox(
height: 64,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 12.0,
),
scrollDirection: Axis.horizontal,
itemCount: availableFilters.length,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
label: Text(
switch (availableFilters[i]) {
Membership.ban =>
L10n.of(context).banned,
Membership.invite =>
L10n.of(context).invited,
Membership.join =>
L10n.of(context).all,
Membership.knock =>
L10n.of(context).knocking,
Membership.leave =>
L10n.of(context).leftTheChat,
},
),
selected: controller.membershipFilter ==
availableFilters[i],
onSelected: (_) =>
controller.setMembershipFilter(
availableFilters[i],
),
),
),
),
),
],
);
}
i--;
return ParticipantListItem(members[i]);
},
),
),
);

View file

@ -53,7 +53,6 @@ class ChatPermissionsSettingsView extends StatelessWidget {
)..removeWhere(
(k, v) =>
v is! int ||
k.equals("m.space.child") ||
k.equals("pangea.usranalytics") ||
k.equals(EventTypes.RoomPowerLevels),
);

View file

@ -67,6 +67,10 @@ class PermissionsListTile extends StatelessWidget {
return L10n.of(context).enableEncryption;
case 'm.room.server_acl':
return L10n.of(context).editBlockedServers;
// #Pangea
case EventTypes.SpaceChild:
return L10n.of(context).spaceChildPermission;
// Pangea#
}
}
return permissionKey;

View file

@ -155,6 +155,7 @@ class _MessageSearchResultListTile extends StatelessWidget {
],
),
subtitle: Linkify(
textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,

View file

@ -122,6 +122,8 @@ class HomeserverPickerView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: SelectableLinkify(
text: L10n.of(context).appIntroduction,
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
textAlign: TextAlign.center,
linkStyle: TextStyle(
color: theme.colorScheme.secondary,
@ -169,6 +171,19 @@ class HomeserverPickerView extends StatelessWidget {
content: Linkify(
text: L10n.of(context)
.homeserverDescription,
textScaleFactor:
MediaQuery.textScalerOf(context)
.scale(1),
options: const LinkifyOptions(
humanize: false,
),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
decorationColor:
theme.colorScheme.primary,
),
onOpen: (link) =>
launchUrlString(link.url),
),
actions: [
AdaptiveDialogAction(

View file

@ -69,25 +69,29 @@ class ImageViewerView extends StatelessWidget {
body: HoverBuilder(
builder: (context, hovered) => Stack(
children: [
PageView.builder(
controller: controller.pageController,
itemCount: controller.allEvents.length,
itemBuilder: (context, i) => InteractiveViewer(
minScale: 1.0,
maxScale: 10.0,
onInteractionEnd: controller.onInteractionEnds,
child: Center(
child: Hero(
tag: controller.allEvents[i].eventId,
child: GestureDetector(
// Ignore taps to not go back here:
onTap: () {},
child: MxcImage(
key: ValueKey(controller.allEvents[i].eventId),
event: controller.allEvents[i],
fit: BoxFit.contain,
isThumbnail: false,
animated: true,
KeyboardListener(
focusNode: controller.focusNode,
onKeyEvent: controller.onKeyEvent,
child: PageView.builder(
controller: controller.pageController,
itemCount: controller.allEvents.length,
itemBuilder: (context, i) => InteractiveViewer(
minScale: 1.0,
maxScale: 10.0,
onInteractionEnd: controller.onInteractionEnds,
child: Center(
child: Hero(
tag: controller.allEvents[i].eventId,
child: GestureDetector(
// Ignore taps to not go back here:
onTap: () {},
child: MxcImage(
key: ValueKey(controller.allEvents[i].eventId),
event: controller.allEvents[i],
fit: BoxFit.contain,
isThumbnail: false,
animated: true,
),
),
),
),

View file

@ -35,6 +35,36 @@ class InvitationSelectionController extends State<InvitationSelection> {
String? get roomId => widget.roomId;
// #Pangea
final viewportKey = GlobalKey();
final participantListItemHeight = 72.0;
final goToChatButtonHeight = 50.0;
final shareButtonsHeight = 150.0;
final padding = 16.0 * 2;
final fixedParticipantHeight = 72.0;
double? viewportHeight;
double get availableHeight =>
(viewportHeight ?? 0) -
goToChatButtonHeight -
shareButtonsHeight -
padding;
bool showShareButtons(int numParticipants) =>
(fixedParticipantHeight * numParticipants) < availableHeight;
@override
initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = viewportKey.currentContext;
if (context == null) return;
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
setState(() => viewportHeight = size.height);
});
super.initState();
}
List<User>? get participants {
final room = Matrix.of(context).client.getRoomById(roomId!);
return room?.getParticipants();

View file

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
@ -12,18 +11,17 @@ import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/refer_friends_dialog.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/space_invite_buttons.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class InvitationSelectionView extends StatelessWidget {
final InvitationSelectionController controller;
@ -54,346 +52,249 @@ class InvitationSelectionView extends StatelessWidget {
leading: const Center(child: BackButton()),
titleSpacing: 0,
title: Text(L10n.of(context).inviteContact),
// #Pangea
actions: [
if (room.isSpace && room.classCode(context) != null)
PopupMenuButton<int>(
icon: const Icon(Icons.share_outlined),
onSelected: (value) async {
final spaceCode = room.classCode(context)!;
String toCopy = spaceCode;
if (value == 0) {
final String initialUrl =
kIsWeb ? html.window.origin! : Environment.frontendURL;
toCopy =
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}";
}
await Clipboard.setData(ClipboardData(text: toCopy));
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
L10n.of(context).copiedToClipboard,
),
),
);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
PopupMenuItem<int>(
value: 0,
child: ListTile(
leading: const Icon(Icons.share_outlined),
title: Text(L10n.of(context).shareSpaceLink),
contentPadding: const EdgeInsets.all(0),
),
),
PopupMenuItem<int>(
value: 1,
child: ListTile(
leading: const Icon(Icons.share_outlined),
title: Text(
L10n.of(context)
.shareInviteCode(room.classCode(context)!),
),
contentPadding: const EdgeInsets.all(0),
),
),
],
),
],
// Pangea#
),
body: MaxWidthBody(
innerPadding: const EdgeInsets.symmetric(vertical: 8),
// #Pangea
withScrolling: false,
// Pangea#
child: Column(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
// #Pangea
Padding(
padding: const EdgeInsets.all(16.0),
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
),
onTap: () async {
await Clipboard.setData(
ClipboardData(text: room.classCode(context)),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).copiedToClipboard)),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0,
),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(99),
),
child: Row(
spacing: 16.0,
children: [
const Icon(
Icons.copy_outlined,
size: 20.0,
),
Text(
"${L10n.of(context).copyClassCode}: ${room.classCode(context)}",
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
fontSize: 16.0,
),
),
],
child: SizedBox(
width: 450,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
),
Padding(
padding: const EdgeInsets.only(
bottom: 16.0,
left: 16.0,
right: 16.0,
),
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
),
onTap: () async {
final String initialUrl =
kIsWeb ? html.window.origin! : Environment.frontendURL;
final link =
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${room.classCode(context)}";
await Clipboard.setData(ClipboardData(text: link));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).copiedToClipboard)),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0,
),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(99),
),
child: Row(
spacing: 16.0,
children: [
const Icon(
Icons.copy_outlined,
size: 20.0,
),
Text(
L10n.of(context).copyClassLink,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
fontSize: 16.0,
),
),
],
),
),
),
),
// Pangea#
Padding(
// #Pangea
// padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.only(
bottom: 16.0,
left: 16.0,
right: 16.0,
),
// Pangea#
child: TextField(
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
Column(
children: [
Padding(
// #Pangea
hintText: L10n.of(context).inviteStudentByUserName,
// hintText: L10n.of(context).inviteContactToGroup(groupName),
// padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.only(
bottom: 16.0,
left: 16.0,
right: 16.0,
),
// Pangea#
prefixIcon: controller.loading
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: const Icon(Icons.search_outlined),
),
onChanged: controller.searchUserWithCoolDown,
),
),
// #Pangea
// StreamBuilder<Object>(
Expanded(
child: StreamBuilder<Object>(
// Pangea#
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id),
builder: (context, snapshot) {
final participants =
room.getParticipants().map((user) => user.id).toSet();
return controller.foundProfiles.isNotEmpty
? ListView.builder(
// #Pangea
// physics: const NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// Pangea#
itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
profile: controller.foundProfiles[i],
isMember: participants
.contains(controller.foundProfiles[i].userId),
onTap: () => controller.inviteAction(
context,
controller.foundProfiles[i].userId,
controller.foundProfiles[i].displayName ??
controller
.foundProfiles[i].userId.localpart ??
L10n.of(context).user,
),
),
)
: FutureBuilder<List<User>>(
future: controller.getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: TextField(
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99),
),
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
// #Pangea
hintText: L10n.of(context).inviteStudentByUserName,
// hintText: L10n.of(context).inviteContactToGroup(groupName),
// Pangea#
prefixIcon: controller.loading
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
final contacts = snapshot.data!;
return ListView.builder(
),
)
: const Icon(Icons.search_outlined),
),
onChanged: controller.searchUserWithCoolDown,
),
),
// #Pangea
// StreamBuilder<Object>(
Expanded(
key: controller.viewportKey,
child: StreamBuilder<Object>(
// stream: room.client.onRoomState.stream
// .where((update) => update.roomId == room.id),
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id)
.rateLimit(const Duration(seconds: 1)),
// Pangea#
builder: (context, snapshot) {
final participants =
room.getParticipants().map((user) => user.id).toSet();
return controller.foundProfiles.isNotEmpty
? ListView.builder(
// #Pangea
// physics: const NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// itemCount: contacts.length,
// itemBuilder: (BuildContext context, int i) =>
// _InviteContactListTile(
itemCount: contacts.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i == contacts.length) {
return room.isSpace
? const SizedBox()
: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: 450,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}",
errorWidget:
(context, url, error) =>
const SizedBox(),
placeholder: (context, url) =>
const Center(
child:
CircularProgressIndicator
.adaptive(),
),
),
),
),
);
// Pangea#
itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
profile: controller.foundProfiles[i],
isMember: participants.contains(
controller.foundProfiles[i].userId,
),
onTap: () => controller.inviteAction(
context,
controller.foundProfiles[i].userId,
controller.foundProfiles[i].displayName ??
controller
.foundProfiles[i].userId.localpart ??
L10n.of(context).user,
),
),
)
: FutureBuilder<List<User>>(
future: controller.getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
return _InviteContactListTile(
// Pangea#
user: contacts[i],
profile: Profile(
avatarUrl: contacts[i].avatarUrl,
displayName: contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
userId: contacts[i].id,
),
isMember:
participants.contains(contacts[i].id),
onTap: () => controller.inviteAction(
context,
contacts[i].id,
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
),
final contacts = snapshot.data!;
return ListView.builder(
// #Pangea
roomPowerLevel: controller.participants
?.firstWhereOrNull(
(element) =>
element.id == contacts[i].id,
)
?.powerLevel,
membership: controller.participants
?.firstWhereOrNull(
(element) =>
element.id == contacts[i].id,
)
?.membership,
// Pangea#
// physics: const NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// itemCount: contacts.length,
// itemBuilder: (BuildContext context, int i) =>
// _InviteContactListTile(
itemCount: contacts.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i == contacts.length) {
final showButtons = controller
.showShareButtons(contacts.length);
return AnimatedOpacity(
duration:
FluffyThemes.animationDuration,
opacity: showButtons ? 1.0 : 0.0,
child: SpaceInviteButtons(room: room),
);
}
return _InviteContactListTile(
// Pangea#
user: contacts[i],
profile: Profile(
avatarUrl: contacts[i].avatarUrl,
displayName: contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
userId: contacts[i].id,
),
isMember:
participants.contains(contacts[i].id),
onTap: () => controller.inviteAction(
context,
contacts[i].id,
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
),
);
},
);
},
);
},
);
},
),
},
),
),
],
),
// #Pangea
if (!room.isSpace)
Padding(
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
top: 16.0,
bottom: FluffyThemes.isColumnMode(context) ? 0 : 16.0,
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
elevation: 5.0,
),
child: Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (context) => FullWidthDialog(
dialogContent: ReferFriendsDialog(room: room),
maxWidth: 600.0,
maxHeight: 800.0,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppConfig.gold,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
Icon(
Icons.redeem_outlined,
color: Theme.of(context).brightness ==
Brightness.light
? DefaultTextStyle.of(context).style.color
: Theme.of(context).colorScheme.surface,
),
Text(
L10n.of(context).referFriends,
style: TextStyle(
color: Theme.of(context).brightness ==
Brightness.light
? null
: Theme.of(context).colorScheme.surface,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
Expanded(
child: ElevatedButton(
onPressed: () => context.go("/rooms/${room.id}"),
style: ElevatedButton.styleFrom(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
Icon(
Icons.chat_outlined,
color: DefaultTextStyle.of(context).style.color,
),
Text(
L10n.of(context).goToChat,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
Text(
room.isSpace
? L10n.of(context).goToSpaceButton
: L10n.of(context).goToChat,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
),
),
],
),
onPressed: () => context.go(
room.isSpace
? "/rooms?spaceId=${room.id}"
: "/rooms/${room.id}",
),
),
// Pangea#
),
],
),
),
@ -406,48 +307,19 @@ class _InviteContactListTile extends StatelessWidget {
final User? user;
final bool isMember;
final void Function() onTap;
// #Pangea
final int? roomPowerLevel;
final Membership? membership;
// Pangea#
const _InviteContactListTile({
required this.profile,
this.user,
required this.isMember,
required this.onTap,
// #Pangea
this.roomPowerLevel,
this.membership,
// Pangea#
});
@override
Widget build(BuildContext context) {
// #Pangea
String? permissionCopy() {
if (roomPowerLevel == null) {
return null;
}
return roomPowerLevel! >= 100
? L10n.of(context).admin
: roomPowerLevel! >= 50
? L10n.of(context).moderator
: null;
}
String? membershipCopy() => switch (membership) {
Membership.ban => L10n.of(context).banned,
Membership.invite => L10n.of(context).invited,
Membership.join => null,
Membership.knock => L10n.of(context).knocking,
Membership.leave => L10n.of(context).leftTheChat,
null => null,
};
// final theme = Theme.of(context);
// Pangea#
final theme = Theme.of(context);
final l10n = L10n.of(context);
return ListTile(
@ -455,13 +327,9 @@ class _InviteContactListTile extends StatelessWidget {
mxContent: profile.avatarUrl,
name: profile.displayName,
presenceUserId: profile.userId,
onTap: () => showAdaptiveBottomSheet(
onTap: () => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
user: user,
profile: profile,
outerContext: context,
),
profile: profile,
),
),
title: Text(
@ -469,70 +337,22 @@ class _InviteContactListTile extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
profile.userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: theme.colorScheme.secondary,
),
),
// #Pangea
// trailing: TextButton.icon(
// onPressed: isMember ? null : onTap,
// label: Text(isMember ? l10n.participant : l10n.invite),
// icon: Icon(isMember ? Icons.check : Icons.add),
// subtitle: Text(
// profile.userId,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// style: TextStyle(
// color: theme.colorScheme.secondary,
// ),
// ),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
LevelDisplayName(userId: profile.userId),
if (membershipCopy() != null)
Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
membershipCopy()!,
style: theme.textTheme.labelSmall,
),
)
else if (permissionCopy() != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: roomPowerLevel! >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: Text(
permissionCopy()!,
style: theme.textTheme.labelSmall?.copyWith(
color: roomPowerLevel! >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
),
)
else if (!isMember || roomPowerLevel == null || roomPowerLevel! < 50)
TextButton.icon(
onPressed: isMember ? null : onTap,
label: Text(isMember ? l10n.participant : l10n.invite),
icon: Icon(isMember ? Icons.check : Icons.add),
),
],
),
subtitle: LevelDisplayName(userId: profile.userId),
// Pangea#
trailing: TextButton.icon(
onPressed: isMember ? null : onTap,
label: Text(isMember ? l10n.participant : l10n.invite),
icon: Icon(isMember ? Icons.check : Icons.add),
),
);
}
}

View file

@ -56,7 +56,7 @@ class LoginView extends StatelessWidget {
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextField(
readOnly: controller.loadingSignIn,
autocorrect: false,
@ -79,7 +79,7 @@ class LoginView extends StatelessWidget {
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextField(
readOnly: controller.loadingSignIn,
autocorrect: false,
@ -110,7 +110,7 @@ class LoginView extends StatelessWidget {
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
@ -125,7 +125,7 @@ class LoginView extends StatelessWidget {
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: TextButton(
onPressed: controller.loadingSignIn
? () {}

View file

@ -10,14 +10,11 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/new_group/new_group_view.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -198,92 +195,52 @@ class NewGroupController extends State<NewGroup> {
Future<void> _createSpace() async {
if (!mounted) return;
// #Pangea
final client = Matrix.of(context).client;
final joinCode = await SpaceCodeUtil.generateSpaceCode(client);
// Pangea#
final spaceId = await Matrix.of(context).client.createRoom(
// #Pangea
// preset: publicGroup
// ? sdk.CreateRoomPreset.publicChat
// : sdk.CreateRoomPreset.privateChat,
// Pangea#
creationContent: {'type': RoomCreationTypes.mSpace},
// #Pangea
// visibility: publicGroup ? sdk.Visibility.public : null,
// final spaceId = await Matrix.of(context).client.createRoom(
// preset: publicGroup
// ? sdk.CreateRoomPreset.publicChat
// : sdk.CreateRoomPreset.privateChat,
// creationContent: {'type': RoomCreationTypes.mSpace},
// visibility: publicGroup ? sdk.Visibility.public : null,
// roomAliasName: publicGroup
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
// : null,
// name: nameController.text.trim(),
// powerLevelContentOverride: {'events_default': 100},
// initialState: [
// if (avatar != null)
// sdk.StateEvent(
// type: sdk.EventTypes.RoomAvatar,
// content: {'url': avatarUrl.toString()},
// ),
// ],
// );
// if (!mounted) return;
// context.pop<String>(spaceId);
final spaceId = await Matrix.of(context).client.createPangeaSpace(
name: nameController.text,
visibility:
groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private,
// roomAliasName: publicGroup
// ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
// : null,
// Pangea#
name: nameController.text.trim(),
powerLevelContentOverride: {'events_default': 100},
initialState: [
// #Pangea
..._spaceInitialState(joinCode),
// Pangea#
if (avatar != null)
sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()},
),
],
joinRules:
requiredCodeToJoin ? sdk.JoinRules.knock : sdk.JoinRules.public,
avatar: avatar,
avatarUrl: avatarUrl,
);
if (!mounted) return;
// #Pangea
Room? room = client.getRoomById(spaceId);
if (room == null) {
await Matrix.of(context).client.waitForRoomInSync(spaceId);
room = client.getRoomById(spaceId);
}
final room = Matrix.of(context).client.getRoomById(spaceId);
if (room == null) return;
GoogleAnalytics.createClass(room.name, room.classCode(context));
try {
await room.invite(BotName.byEnvironment);
} catch (err) {
ErrorHandler.logError(
e: "Failed to invite pangea bot to new space",
data: {"spaceId": spaceId, "error": err},
);
final spaceCode = room.classCode(context);
if (spaceCode != null) {
GoogleAnalytics.createClass(room.name, spaceCode);
}
// if a timeout happened, don't redirect to the space
if (error != null) return;
MatrixState.pangeaController.classController
.setActiveSpaceIdInChatListController(spaceId);
context.go("/rooms?spaceId=$spaceId");
// Pangea#
context.pop<String>(spaceId);
}
// #Pangea
List<StateEvent> _spaceInitialState(String joinCode) {
return [
StateEvent(
type: EventTypes.RoomPowerLevels,
stateKey: '',
content: {
'events': {
EventTypes.SpaceChild: 0,
},
'users_default': 0,
'users': {
Matrix.of(context).client.userID: SpaceConstants.powerLevelOfAdmin,
},
},
),
StateEvent(
type: sdk.EventTypes.RoomJoinRules,
content: {
ModelKey.joinRule: requiredCodeToJoin
? sdk.JoinRules.knock.toString().replaceAll('JoinRules.', '')
: sdk.JoinRules.public.toString().replaceAll('JoinRules.', ''),
ModelKey.accessCode: joinCode,
},
),
];
}
//Pangea#
void submitAction([_]) async {
final client = Matrix.of(context).client;

View file

@ -9,12 +9,12 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/new_private_chat/new_private_chat_view.dart';
import 'package:fluffychat/pages/new_private_chat/qr_scanner_modal.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../widgets/adaptive_dialogs/user_dialog.dart';
class NewPrivateChat extends StatefulWidget {
const NewPrivateChat({super.key});
@ -98,12 +98,9 @@ class NewPrivateChatController extends State<NewPrivateChat> {
);
}
void openUserModal(Profile profile) => showAdaptiveBottomSheet(
void openUserModal(Profile profile) => UserDialog.show(
context: context,
builder: (c) => UserBottomSheet(
profile: profile,
outerContext: context,
),
profile: profile,
);
@override

View file

@ -15,6 +15,7 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
import '../../widgets/mxc_image_viewer.dart';
import 'settings.dart';
class SettingsView extends StatelessWidget {
@ -74,6 +75,7 @@ class SettingsView extends StatelessWidget {
future: controller.profileFuture,
builder: (context, snapshot) {
final profile = snapshot.data;
final avatar = profile?.avatarUrl;
final mxid = Matrix.of(context).client.userID ??
L10n.of(context).user;
final displayname =
@ -85,12 +87,19 @@ class SettingsView extends StatelessWidget {
child: Stack(
children: [
Avatar(
mxContent: profile?.avatarUrl,
mxContent: avatar,
name: displayname,
// #Pangea
userId: profile?.userId,
// Pangea#
size: Avatar.defaultSize * 2.5,
onTap: avatar != null
? () => showDialog(
context: context,
builder: (_) =>
MxcImageViewer(avatar),
)
: null,
),
if (profile != null)
Positioned(
@ -321,7 +330,7 @@ class SettingsView extends StatelessWidget {
},
),
// Conditional ListTile based on the environment (staging or not)
if (Environment.isStaging)
if (Environment.isStagingEnvironment)
ListTile(
leading: const Icon(Icons.bug_report_outlined),
title: Text(L10n.of(context).connectedToStaging),

View file

@ -169,6 +169,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Federation Base URL'),
subtitle: Linkify(
text: data.federationBaseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
@ -231,6 +233,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Base URL'),
subtitle: Linkify(
text: wellKnown.mHomeserver.baseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,
@ -244,6 +248,8 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Identity Server:'),
subtitle: Linkify(
text: identityServer.baseUrl.toString(),
textScaleFactor:
MediaQuery.textScalerOf(context).scale(1),
options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle(
color: theme.colorScheme.primary,

View file

@ -138,10 +138,10 @@ class SettingsSecurityController extends State<SettingsSecurity> {
void changeShareKeysWith(ShareKeysWith? shareKeysWith) async {
if (shareKeysWith == null) return;
Matrix.of(context).store.setString(
SettingKeys.shareKeysWith,
shareKeysWith.name,
);
AppSettings.shareKeysWith.setItem(
Matrix.of(context).store,
shareKeysWith.name,
);
Matrix.of(context).client.shareKeysWith = shareKeysWith;
setState(() {});
}

View file

@ -16,6 +16,7 @@ import 'settings_security.dart';
class SettingsSecurityView extends StatelessWidget {
final SettingsSecurityController controller;
const SettingsSecurityView(this.controller, {super.key});
@override
@ -143,7 +144,7 @@ class SettingsSecurityView extends StatelessWidget {
leading: const Icon(Icons.vpn_key_outlined),
subtitle: SelectableText(
Matrix.of(context).client.fingerprintKey.beautified,
style: const TextStyle(fontFamily: 'UbuntuMono'),
style: const TextStyle(fontFamily: 'RobotoMono'),
),
),
if (capabilities?.mChangePassword?.enabled != false ||

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