syncing with main
41
.github/workflows/auto_pull_request.yaml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
name: Auto Pull Request
|
||||
|
||||
#on:
|
||||
# schedule:
|
||||
# - cron: '0 0 * * 0' # Run at midnight (00:00) every Sunday
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- auto-pr # Change this to match your main branch name
|
||||
|
||||
jobs:
|
||||
auto_pull_request:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Test
|
||||
run: echo ${{ github.head_ref }}.${{ github.sha }}
|
||||
|
||||
- name: Pull changes from FluffyChat
|
||||
run: |
|
||||
git config --global user.email "${{ vars.CI_EMAIL }}"
|
||||
git config --global user.name "${{ vars.CI_USERNAME }}"
|
||||
git remote add fluffychat https://github.com/krille-chan/fluffychat
|
||||
git fetch fluffychat main
|
||||
git merge --no-edit fluffychat/main --allow-unrelated-histories
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push origin HEAD:main-update-fluffy-automatic
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
title: Updated fork with new fluffy changes
|
||||
body: |
|
||||
This is an automatic PR created by GitHub Actions.
|
||||
branch: main
|
||||
14
.github/workflows/integrate.yaml
vendored
|
|
@ -19,22 +19,23 @@ jobs:
|
|||
run: dart format lib/ test/ --set-exit-if-changed
|
||||
- name: Check import formatting
|
||||
run: dart run import_sorter:main --no-comments --exit-if-changed
|
||||
- name: Check license compliance
|
||||
run: dart run license_checker check-licenses -c licenses.yaml --problematic
|
||||
- run: flutter analyze
|
||||
- name: Apply google services patch
|
||||
run: git apply ./scripts/enable-android-google-services.patch
|
||||
- run: flutter analyze
|
||||
- run: flutter test
|
||||
- name: Check for unused localization strings
|
||||
run: flutter pub run translations_cleaner list-unused-terms -a
|
||||
|
||||
build_debug_apk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: "zulu"
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
|
@ -66,7 +67,7 @@ jobs:
|
|||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y
|
||||
run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev -y
|
||||
- run: flutter pub get
|
||||
- run: flutter build linux --target-platform linux-x64
|
||||
|
||||
|
|
@ -80,6 +81,9 @@ jobs:
|
|||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- name: Setup Xcode version
|
||||
uses: maxim-lobanov/setup-xcode@v1.5.1
|
||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||
with:
|
||||
xcode-version: latest
|
||||
- run: brew install sqlcipher
|
||||
- run: flutter pub get
|
||||
- run: flutter build ios --no-codesign
|
||||
|
|
|
|||
4
.github/workflows/main_deploy.yaml
vendored
|
|
@ -19,6 +19,10 @@ 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
|
||||
|
|
|
|||
28
.github/workflows/process_tags.yaml
vendored
|
|
@ -1,28 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "rc*"
|
||||
|
||||
name: Process Tags
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get Changelog Entry
|
||||
id: changelog_reader
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: ${{ startsWith(github.ref, 'rc') }}
|
||||
18
.github/workflows/release.yaml
vendored
|
|
@ -9,6 +9,10 @@ concurrency:
|
|||
group: release_workflow
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -22,6 +26,10 @@ 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
|
||||
|
|
@ -55,9 +63,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: 'zulu'
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
|
@ -97,7 +106,7 @@ jobs:
|
|||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 -y
|
||||
run: sudo apt-get update && sudo apt-get install curl clang cmake ninja-build pkg-config libgtk-3-dev libblkid-dev liblzma-dev libjsoncpp-dev cmake-data libsecret-1-dev libsecret-1-0 librhash0 libssl-dev -y
|
||||
- run: flutter pub get
|
||||
- run: flutter build linux --release --target-platform linux-x64
|
||||
- name: Create archive
|
||||
|
|
@ -118,9 +127,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: cat .github/workflows/versions.env >> $GITHUB_ENV
|
||||
- uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
distribution: 'zulu'
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
|
@ -128,7 +138,7 @@ jobs:
|
|||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
ruby-version: '3.3'
|
||||
- name: Install Fastlane
|
||||
run: gem install fastlane -NV
|
||||
- name: Apply Google Services Patch
|
||||
|
|
|
|||
4
.github/workflows/versions.env
vendored
|
|
@ -1,2 +1,2 @@
|
|||
FLUTTER_VERSION=3.16.7
|
||||
JAVA_VERSION=17
|
||||
FLUTTER_VERSION=3.19.5
|
||||
JAVA_VERSION=17
|
||||
|
|
|
|||
25
.metadata
|
|
@ -4,7 +4,7 @@
|
|||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: "efbf63d9c66b9f6ec30e9ad4611189aa80003d31"
|
||||
revision: "abb292a07e20d696c4568099f918f6c5f330e6b0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
|
@ -13,26 +13,11 @@ project_type: app
|
|||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
- platform: android
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
- platform: ios
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
|
||||
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
|
||||
- platform: linux
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
- platform: macos
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
- platform: web
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
- platform: windows
|
||||
create_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
base_revision: efbf63d9c66b9f6ec30e9ad4611189aa80003d31
|
||||
create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
|
||||
base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0
|
||||
|
||||
# User provided section
|
||||
|
||||
|
|
|
|||
132
CHANGELOG.md
|
|
@ -1,3 +1,135 @@
|
|||
## v1.19.1
|
||||
Minor bugfix release for login with SSO on web.
|
||||
|
||||
- feat: Show/hide third column in chat view (krille-chan)
|
||||
- design: Adjust some colors in inputbar (krille-chan)
|
||||
- fix: Login with SSO on web (krille-chan)
|
||||
- fix: Make chat permission settings null and type safe (krille-chan)
|
||||
- chore: do not use static openssl (ShootingStarDragons)
|
||||
- refactor: Move room headers into appbar bottom field (krille-chan)
|
||||
- refactor: new flutter only typing animation (krille-chan)
|
||||
|
||||
## v1.19.0
|
||||
FluffyChat v1.19.0 features an improved design for message bubbles and a lot of fixes under the hood.
|
||||
|
||||
- build: Update matrix dart sdk (Krille)
|
||||
- build: Update to flutter 3.19.5 (krille-chan)
|
||||
- chore: Add missing command hints (krille-chan)
|
||||
- chore: Add pagekey to custom page builder (Krille)
|
||||
- chore: Adjust design of typing indicator (Krille)
|
||||
- chore: Adjust ticker of notifications for Android (Krille)
|
||||
- chore: Calc blurhash in other thread (Krille)
|
||||
- chore: Mark muted unread rooms with bold text (krille-chan)
|
||||
- chore: More minimal matrix pill (Krille)
|
||||
- chore: Try out CupertinoPage instead of custom transition in router (krille-chan)
|
||||
- ci: add a license compliance check (lauren n. liberda)
|
||||
- design: Connect bubbles from same sender (krille-chan)
|
||||
- design: Display images in correct ratio in timeline (krille-chan)
|
||||
- design: Make appbar in material you design for mobile mode (krille-chan)
|
||||
- design: New sticker picker next to emoji picker (krille-chan)
|
||||
- design: Nicer QR Code design (krille-chan)
|
||||
- design: Nicer reactions design with size animations (Krille)
|
||||
- feat: Add insert content via gboard (krille-chan)
|
||||
- feat: Reply with one button in desktop (krille-chan)
|
||||
- fix: Do not sync in background mode (krille-chan)
|
||||
- fix: FluffyChat should assume m.change_password capabilitiy is supported if not present per spec (krille-chan)
|
||||
- fix: never use root navigator for bottom sheets (The one with the braid)
|
||||
- fix: Remove pantalaimon message with normal error message (krille-chan)
|
||||
- fix: Search in spaces view (krille-chan)
|
||||
- fix: Set read marker on web (Krille)
|
||||
- fix: Point to correct path for auth.html so completing sso login flow no longer 404s (Gavin Mogan)
|
||||
- refactor: Better logic for removing outdated notifications (Krille)
|
||||
- refactor: Enhance logic when to mark room as read (krille-chan)
|
||||
- refactor: Remove old aliases workaround (Krille)
|
||||
- refactor: Sticker widget code (Krille)
|
||||
- refactor: Use dart blurhash (Krille)
|
||||
- Translated using Weblate (Basque) (xabirequejo)
|
||||
- Translated using Weblate (Interlingua) (Software In Interlingua)
|
||||
|
||||
## v1.18.0
|
||||
- feat: Add speed button for audioplayer (krille-chan)
|
||||
- feat: enhanced send video functionality by adding toggle send original (Mubeen Rizvi)
|
||||
- feat: add dialog to hide presence list with long-press (Marcus Hoffmann)
|
||||
- feat: Add notification shortcuts to android (krille-chan)
|
||||
- feat: make showing user presence info optional (Marcus Hoffmann)
|
||||
- feat: Open chat on shortcut click on android (krille-chan)
|
||||
- fix: BuildContext crash when joining room (krille-chan)
|
||||
- fix: Export session (krille-chan)
|
||||
- fix: Notifications open sometimes automatically on android (krille-chan)
|
||||
- fix: Open room after join (krille-chan)
|
||||
- fix: Open room by notification happened multiple times (krille-chan)
|
||||
- fix: Open room links with event id (krille-chan)
|
||||
- fix: properly initialize hideUnimportantStateEvents setting (Marcus Hoffmann)
|
||||
- fix: Remove status msg not changeable from old cache (krille-chan)
|
||||
- fix: use correct icons for chat pin/unpin (Marcus Hoffmann)
|
||||
- fix: use correct icons for mark read/unread action (Marcus Hoffmann)
|
||||
- build: Update Linux build files (krille-chan)
|
||||
- build: Update to Flutter 3.19.1 (Krille)
|
||||
- chore: Add more information to Person object in android notifications (krille-chan)
|
||||
- chore: Thumbnail follow up for notifications (Krille)
|
||||
- refactor: Better download UX with file picker for android and iOS (krille-chan)
|
||||
- refactor: Use hashcode instead of string to id workaround for notifications (Krille)
|
||||
- Added translation using Weblate (Belarusian) (kopatych)
|
||||
- Added translation using Weblate (Interlingua) (Software In Interlingua)
|
||||
- Translated using Weblate (Arabic) (Rex_sa)
|
||||
- Translated using Weblate (Basque) (xabirequejo)
|
||||
- Translated using Weblate (Chinese (Simplified)) (Poesty Li)
|
||||
- Translated using Weblate (Chinese (Simplified)) (大王叫我来巡山)
|
||||
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||
- Translated using Weblate (Galician) (josé m)
|
||||
- Translated using Weblate (German) (Benjamin Wagner)
|
||||
- Translated using Weblate (Greek) (Benjamin Wagner)
|
||||
- Translated using Weblate (Russian) (Benjamin Wagner)
|
||||
- Translated using Weblate (Russian) (v1s7)
|
||||
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||
- Translated using Weblate (Ukrainian) (Сергій)
|
||||
|
||||
## v1.17.3
|
||||
- feat: New account data based wallpaper feature (Krille)
|
||||
- build: Update dependencies (Krille)
|
||||
- build: Update flutter to 3.16.9 (Krille)
|
||||
- build: Update matrix dart sdk to 0.25.7 (Krille)
|
||||
- build: Update minor versions (Krille)
|
||||
- chore: Adjust status msg design (krille-chan)
|
||||
- chore: Improved error handling for recovery key (Krille)
|
||||
- chore: Make stickers smaller (Krille)
|
||||
- chore: Wait for device keys before ask bootstrap (Krille)
|
||||
- fix: Missing null check in public room bottom sheet (Krille)
|
||||
- fix: onDragDone crashes when no files found (Krille)
|
||||
- fix: Render tg-forward html tags (Krille)
|
||||
- fix: Use HapticFeedback.selectionClick() for long press on message (Krille)
|
||||
- fix: whitespaces sometimes encoded in html message (Krille)
|
||||
- fix: Share invite links of public rooms (Krille)
|
||||
|
||||
## v1.17.2
|
||||
Another minor bugfix release which also implements private read receipts.
|
||||
|
||||
- feat: Implement private read receipts (krille-chan)
|
||||
- feat: Join room by alias by tpying alias in searchbar (krille-chan)
|
||||
- fix: Add cancel button to key request dialog (Krille)
|
||||
- fix: Encode component for links correctly (Krille)
|
||||
- fix: Forward arbitrary message content (krille-chan)
|
||||
- fix: Open publicroombottomsheet by alias (krille-chan)
|
||||
- docs: Add noto animated emojis link (krille-chan)
|
||||
- docs: New website (krille-chan)
|
||||
- build: Do not load emojis at initial start on web (krille-chan)
|
||||
- build: Update flutter to 3.16.8 (krille-chan)
|
||||
- build: Update sdk to 0.25.6 (Krille)
|
||||
- chore: Add more explaining text for key verification (krille-chan)
|
||||
- chore: Resort settings and add more description text (krille-chan)
|
||||
- refactor: Dialog BuildContext (krille-chan)
|
||||
- refactor: Use popupmenudivider instead of workaround (krille-chan)
|
||||
- Translated using Weblate (Arabic) (Rex_sa)
|
||||
- Translated using Weblate (Basque) (xabirequejo)
|
||||
- Translated using Weblate (Chinese (Simplified)) (Poesty Li)
|
||||
- Translated using Weblate (Estonian) (Priit Jõerüüt)
|
||||
- Translated using Weblate (Galician) (josé m)
|
||||
- Translated using Weblate (German) (nautilusx)
|
||||
- Translated using Weblate (Russian) (v1s7)
|
||||
- Translated using Weblate (Swedish) (Flat)
|
||||
- Translated using Weblate (Ukrainian) (Ihor Hordiichuk)
|
||||
- Translated using Weblate (Ukrainian) (Сергій)
|
||||
|
||||
## v1.17.1
|
||||
Minor bugfix release.
|
||||
|
||||
|
|
|
|||
1
assets/l10n/intl_be.arb
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -2182,7 +2182,7 @@
|
|||
"@doNotShowAgain": {},
|
||||
"appearOnTopDetails": "Ermöglicht, dass die App oben angezeigt wird (nicht erforderlich, wenn Sie Fluffychat bereits als Anrufkonto eingerichtet haben)",
|
||||
"@appearOnTopDetails": {},
|
||||
"noKeyForThisMessage": "Dies kann passieren, wenn die Nachricht gesendet wurde, bevor du dich auf diesem Gerät bei deinem Konto angemeldet hast.\n\nEs ist auch möglich, dass der Absender dein Gerät blockiert hat oder etwas mit der Internetverbindung schief gelaufen ist.\n\nKannst du die Nachricht in einer anderen Sitzung lesen? Dann kannst du die Nachricht davon übertragen! Gehe zu denEinstellungen > Geräte und vergewissere dich, dass sich deine Geräte gegenseitig verifiziert haben. Wenn du den Raum das nächste Mal öffnest und beide Sitzungen im Vordergrund sind, werden die Schlüssel automatisch übertragen.\n\nDu möchtest die Schlüssel beim Abmelden oder Gerätewechsel nicht verlieren? Stelle sicher, dass du das Chat-Backup in den Einstellungen aktiviert hast.",
|
||||
"noKeyForThisMessage": "Dies kann passieren, wenn die Nachricht gesendet wurde, bevor du dich auf diesem Gerät bei deinem Konto angemeldet hast.\n\nEs ist auch möglich, dass der Absender dein Gerät blockiert hat oder etwas mit der Internetverbindung schief gelaufen ist.\n\nKannst du die Nachricht in einer anderen Sitzung lesen? Dann kannst du die Nachricht davon übertragen! Gehe zu den Einstellungen > Geräte und vergewissere dich, dass sich deine Geräte gegenseitig verifiziert haben. Wenn du den Raum das nächste Mal öffnest und beide Sitzungen im Vordergrund sind, werden die Schlüssel automatisch übertragen.\n\nDu möchtest die Schlüssel beim Abmelden oder Gerätewechsel nicht verlieren? Stelle sicher, dass du das Chat-Backup in den Einstellungen aktiviert hast.",
|
||||
"@noKeyForThisMessage": {},
|
||||
"foregroundServiceRunning": "Diese Benachrichtigung wird angezeigt, wenn der Vordergrunddienst ausgeführt wird.",
|
||||
"@foregroundServiceRunning": {},
|
||||
|
|
@ -2485,5 +2485,106 @@
|
|||
"joinSpace": "Space beitreten",
|
||||
"@joinSpace": {},
|
||||
"searchForUsers": "Suche nach @benutzer ...",
|
||||
"@searchForUsers": {}
|
||||
"@searchForUsers": {},
|
||||
"initAppError": "Beim Starten der App ist ein Fehler aufgetreten",
|
||||
"@initAppError": {},
|
||||
"databaseBuildErrorBody": "Die SQlite-Datenbank kann nicht erstellt werden. Die App versucht vorerst, die Legacy-Datenbank zu verwenden. Bitte melde diesen Fehler an die Entwickler unter {url}. Die Fehlermeldung lautet: {error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"sessionLostBody": "Die App versucht nun, deine Sitzung aus der Sicherung wiederherzustellen. Bitte melde diesen Fehler an die Entwickler unter {url}. Die Fehlermeldung lautet: {error}",
|
||||
"@sessionLostBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"restoreSessionBody": "Die App versucht nun, deine Sitzung aus der Sicherung wiederherzustellen. Bitte melde diesen Fehler an die Entwickler unter {url}. Die Fehlermeldung lautet: {error}",
|
||||
"@restoreSessionBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"youInvitedToBy": "📩 Du wurdest per Link eingeladen zu:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceipts": "Lesebestätigungen senden",
|
||||
"@sendReadReceipts": {},
|
||||
"formattedMessages": "Formatierte Nachrichten",
|
||||
"@formattedMessages": {},
|
||||
"forwardMessageTo": "Nachricht weiterleiten an {roomName}?",
|
||||
"@forwardMessageTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"roomName": {}
|
||||
}
|
||||
},
|
||||
"sendTypingNotificationsDescription": "Andere Teilnehmer in einem Chat können sehen, wenn du eine neue Nachricht tippst.",
|
||||
"@sendTypingNotificationsDescription": {},
|
||||
"formattedMessagesDescription": "Formatierte Nachrichteninhalte wie fettgedruckten Text mit Markdown anzeigen.",
|
||||
"@formattedMessagesDescription": {},
|
||||
"verifyOtherUser": "🔐 Anderen Benutzer verifizieren",
|
||||
"@verifyOtherUser": {},
|
||||
"sendReadReceiptsDescription": "Andere Teilnehmer in einem Chat können sehen, ob du eine Nachricht gelesen hast.",
|
||||
"@sendReadReceiptsDescription": {},
|
||||
"transparent": "Transparent",
|
||||
"@transparent": {},
|
||||
"verifyOtherDevice": "🔐 Anderes Gerät verifizieren",
|
||||
"@verifyOtherDevice": {},
|
||||
"verifyOtherUserDescription": "Wenn du einen anderen Benutzer verifizierst, kannst du sicher sein, dass du weißt, an wen du wirklich schreibst. 💪\n\nWenn du eine Verifizierung startest, wird dir und dem anderen Nutzer ein Popup in der App angezeigt. Dort siehst du dann eine Reihe von Emojis oder Zahlen, die ihr miteinander vergleichen müsst.\n\nDas geht am besten, wenn man sich trifft oder einen Videoanruf startet. 👭",
|
||||
"@verifyOtherUserDescription": {},
|
||||
"acceptedKeyVerification": "{sender} hat die Schlüsselverifikation akzeptiert",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"canceledKeyVerification": "{sender} hat die Schlüsselverifikation abgebrochen",
|
||||
"@canceledKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"completedKeyVerification": "{sender} hat die Schlüsselverifikation abgeschlossen",
|
||||
"@completedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"isReadyForKeyVerification": "{sender} ist bereit für die Schlüsselverifikation",
|
||||
"@isReadyForKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"requestedKeyVerification": "{sender} hat eine Schlüsselverifikation angefragt",
|
||||
"@requestedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"startedKeyVerification": "{sender} hat die Schlüsselverifikation gestartet",
|
||||
"@startedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"verifyOtherDeviceDescription": "Wenn Sie ein anderes Gerät verifizieren, können diese Geräteschlüssel austauschen, was Ihre Sicherheit insgesamt erhöht. 💪 Wenn Sie eine Verifizierung starten, erscheint ein Pop-up in der App auf beiden Geräten. Dort sehen Sie dann eine Reihe von Emojis oder Zahlen, die Sie miteinander vergleichen müssen. Am besten hältst du beide Geräte bereit, bevor du die Verifizierung startest. 🤳",
|
||||
"@verifyOtherDeviceDescription": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1601,11 +1601,6 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"renderRichContent": "Render rich message content",
|
||||
"@renderRichContent": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"replaceRoomWithNewerVersion": "Replace room with newer version",
|
||||
"@replaceRoomWithNewerVersion": {
|
||||
"type": "text",
|
||||
|
|
@ -1811,6 +1806,16 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"presenceStyle": "Presence:",
|
||||
"@presenceStyle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"presencesToggle": "Show status messages from other users",
|
||||
"@presencesToggle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"singlesignon": "Single Sign on",
|
||||
"@singlesignon": {
|
||||
"type": "text",
|
||||
|
|
@ -2261,6 +2266,12 @@
|
|||
"user": {}
|
||||
}
|
||||
},
|
||||
"youInvitedToBy": "📩 You have been invited via link to:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"youInvitedBy": "📩 You have been invited by {user}",
|
||||
"@youInvitedBy": {
|
||||
"placeholders": {
|
||||
|
|
@ -2361,6 +2372,7 @@
|
|||
}
|
||||
},
|
||||
"hideUnimportantStateEvents": "Hide unimportant state events",
|
||||
"hidePresences": "Hide Status List?",
|
||||
"doNotShowAgain": "Do not show again",
|
||||
"wasDirectChatDisplayName": "Empty chat (was {oldDisplayName})",
|
||||
"@wasDirectChatDisplayName": {
|
||||
|
|
@ -3832,6 +3844,15 @@
|
|||
"enableModerationDesc": "Enable automatic moderation to review messages before they are sent",
|
||||
"conversationLanguageLevel": "What is the language level of this conversation?",
|
||||
"showDefinition": "Show Definition",
|
||||
"sendReadReceipts": "Send read receipts",
|
||||
"sendTypingNotificationsDescription": "Other participants in a chat can see when you are typing a new message.",
|
||||
"sendReadReceiptsDescription": "Other participants in a chat can see when you have read a message.",
|
||||
"formattedMessages": "Formatted messages",
|
||||
"formattedMessagesDescription": "Display rich message content like bold text using markdown.",
|
||||
"verifyOtherUser": "🔐 Verify other user",
|
||||
"verifyOtherUserDescription": "If you verify another user, you can be sure that you know who you are really writing to. 💪\n\nWhen you start a verification, you and the other user will see a popup in the app. There you will then see a series of emojis or numbers that you have to compare with each other.\n\nThe best way to do this is to meet up or start a video call. 👭",
|
||||
"verifyOtherDevice": "🔐 Verify other device",
|
||||
"verifyOtherDeviceDescription": "When you verify another device, those devices can exchange keys, increasing your overall security. 💪 When you start a verification, a popup will appear in the app on both devices. There you will then see a series of emojis or numbers that you have to compare with each other. It's best to have both devices handy before you start the verification. 🤳",
|
||||
"acceptedKeyVerification": "{sender} accepted key verification",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
|
|
@ -3899,5 +3920,20 @@
|
|||
"joinToView": "Join this room to view details",
|
||||
"refresh": "Refresh",
|
||||
"autoPlayTitle": "Auto Play Messages",
|
||||
"autoPlayDesc": "When enabled, the text-to-speech audio of messages will play automatically when selected."
|
||||
"autoPlayDesc": "When enabled, the text-to-speech audio of messages will play automatically when selected.",
|
||||
"transparent": "Transparent",
|
||||
"incomingMessages": "Incoming messages",
|
||||
"stickers": "Stickers",
|
||||
"discover": "Discover",
|
||||
"commandHint_ignore": "Ignore the given matrix ID",
|
||||
"commandHint_unignore": "Unignore the given matrix ID",
|
||||
"unreadChatsInApp": "{appname}: {unread} unread chats",
|
||||
"@unreadChatsInApp": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"appname": {},
|
||||
"unread": {}
|
||||
}
|
||||
},
|
||||
"messageAnalytics": "Message Analytics"
|
||||
}
|
||||
|
|
@ -2273,7 +2273,7 @@
|
|||
},
|
||||
"jumpToLastReadMessage": "Joan irakurritako azken mezura",
|
||||
"@jumpToLastReadMessage": {},
|
||||
"reportErrorDescription": "O ez! Zerbaitek huts egin du. Saiatu berriro geroago. Nahi izanez gero, eman garatzaileei errorearen berri.",
|
||||
"reportErrorDescription": "😭 O ez! Zerbaitek huts egin du. Nahi izanez gero, eman garatzaileei errorearen berri.",
|
||||
"@reportErrorDescription": {},
|
||||
"cuddleContent": "{senderName}(e)k samurki besarkatu zaitu",
|
||||
"@cuddleContent": {
|
||||
|
|
@ -2485,5 +2485,120 @@
|
|||
"thisDevice": "Gailu hau:",
|
||||
"@thisDevice": {},
|
||||
"decline": "Baztertu",
|
||||
"@decline": {}
|
||||
"@decline": {},
|
||||
"databaseBuildErrorBody": "Ezin izan da SQlite datu-basea eraiki. Aplikazioa aurreko datu-basea erabiltzen saiatuko da oraingoz. Jakinarazi errorea garatzaileei {url} helbidean. Errorearen mezua ondorengoa da: {error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"initAppError": "Errorea aplikazioa abiaraztean",
|
||||
"@initAppError": {},
|
||||
"sessionLostBody": "Zure saioa galdu da. Jakinarazi errorea garatzaileei {url} helbidean. Errorearen mezua ondorengoa da: {error}",
|
||||
"@sessionLostBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"restoreSessionBody": "Aplikazioa babeskopia erabiliz saioa leheneratzen saiatuko da. Jakinarazi errorea garatzaileei {url} helbidean. Errorearen mezua ondorengoa da: {error}",
|
||||
"@restoreSessionBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"youInvitedToBy": "📩 Esteka baten bidez gonbidatu zaituzte:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"transparent": "Gardena",
|
||||
"@transparent": {},
|
||||
"sendReadReceipts": "Bidali irakurri izanaren adierazlea",
|
||||
"@sendReadReceipts": {},
|
||||
"formattedMessages": "Formatua duten mezuak",
|
||||
"@formattedMessages": {},
|
||||
"verifyOtherDevice": "🔐 Egiaztatu beste gailu bat",
|
||||
"@verifyOtherDevice": {},
|
||||
"acceptedKeyVerification": "{sender}(e)k gakoaren egiaztapena onartu du",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"canceledKeyVerification": "{sender}(e)k gakoen egiaztapena ezeztatu du",
|
||||
"@canceledKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"requestedKeyVerification": "{sender}(e)k gakoen egiaztapena galdegin du",
|
||||
"@requestedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceiptsDescription": "Txateko beste kideek mezu bat irakurri duzula ikus dezakete.",
|
||||
"@sendReadReceiptsDescription": {},
|
||||
"forwardMessageTo": "Birbidali mezua {roomName}(e)ra?",
|
||||
"@forwardMessageTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"roomName": {}
|
||||
}
|
||||
},
|
||||
"completedKeyVerification": "{sender}(e)k gakoen egiaztapena osatu du",
|
||||
"@completedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"isReadyForKeyVerification": "{sender} gakoak egiaztatzeko prest dago",
|
||||
"@isReadyForKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"verifyOtherDeviceDescription": "Beste gailu bat egiaztatzean, gailu horiek gakoak truka ditzakete, eta segurtasun orokorra handitu. 💪 Egiaztapena hasten duzunean, laster-leiho bat agertuko da bi gailuetan. Bertan, elkarrekin alderatu behar diren emoji edo zenbaki batzuk ikusiko dituzu. Hobe da bi gailuak eskura izatea egiaztapena hasi aurretik. 🤳",
|
||||
"@verifyOtherDeviceDescription": {},
|
||||
"verifyOtherUserDescription": "Beste erabiltzaile bat egiaztatzen baduzu, ziur egon zaitezke nori idazten ari zaren. 💪\n\nEgiaztapena hasten duzunean, zuk eta beste erabiltzaileak laster-leiho bat ikusiko duzue aplikazioan. Bertan, elkarrekin alderatu behar diren emoji edo zenbaki batzuk erakutsiko dira.\n\nBideo-dei bat hastea edo aurrez-aurre batzea da horretarako modurik onena. 👭",
|
||||
"@verifyOtherUserDescription": {},
|
||||
"formattedMessagesDescription": "Erakutsi mezu aberatsen edukia markdown erabiliz, testu lodia esaterako.",
|
||||
"@formattedMessagesDescription": {},
|
||||
"sendTypingNotificationsDescription": "Txateko beste kideek mezu berri bat idazten ari zarela ikus dezakete.",
|
||||
"@sendTypingNotificationsDescription": {},
|
||||
"verifyOtherUser": "🔐 Egiaztatu beste erabiltzaile bat",
|
||||
"@verifyOtherUser": {},
|
||||
"startedKeyVerification": "{sender}(e)k gakoen egiaztapena hasi du",
|
||||
"@startedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"presencesToggle": "Erakutsi beste erabiltzaileen egoera-mezuak",
|
||||
"@presencesToggle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"presenceStyle": "Presentzia:",
|
||||
"@presenceStyle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"incomingMessages": "Jasotako mezuak",
|
||||
"@incomingMessages": {},
|
||||
"hidePresences": "Ezkutatu Egoeren Zerrenda?",
|
||||
"@hidePresences": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2320,7 +2320,7 @@
|
|||
"@exportEmotePack": {},
|
||||
"replace": "Substituír",
|
||||
"@replace": {},
|
||||
"sendTypingNotifications": "Enviar notificación de escritura",
|
||||
"sendTypingNotifications": "Permitir ver que estás escribindo",
|
||||
"@sendTypingNotifications": {},
|
||||
"createGroup": "Crear grupo",
|
||||
"@createGroup": {},
|
||||
|
|
@ -2485,5 +2485,120 @@
|
|||
"databaseMigrationTitle": "Base de datos optimizada",
|
||||
"@databaseMigrationTitle": {},
|
||||
"databaseMigrationBody": "Agarda, podería levarnos un pouco.",
|
||||
"@databaseMigrationBody": {}
|
||||
"@databaseMigrationBody": {},
|
||||
"databaseBuildErrorBody": "Non se puido crear a base de datos SQlite. A app intentará usar a base de datos clásica. Por favor informa deste fallo ás desenvolvedoras en {url}. A mensaxe do erro é: {error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"initAppError": "Houbo un fallo ao iniciar a app",
|
||||
"@initAppError": {},
|
||||
"sessionLostBody": "Estragouse a túa sesión. Por favor informa deste fallo ás desenvolvedoras en {url}. A mensaxe do erro é: {error}",
|
||||
"@sessionLostBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"restoreSessionBody": "A app vai intentar restablecer a sesión desde a copia de apoio. Por favor informa deste erro ás desenvolvedoras en {url}. A mensaxe do erro é: {error}",
|
||||
"@restoreSessionBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"youInvitedToBy": "📩 Convidáronte cunha ligazón a:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"transparent": "Transparente",
|
||||
"@transparent": {},
|
||||
"sendReadReceipts": "Enviar confirmación de lectura",
|
||||
"@sendReadReceipts": {},
|
||||
"sendReadReceiptsDescription": "Outras participantes na conversa poden ver cando liches unha mensaxe.",
|
||||
"@sendReadReceiptsDescription": {},
|
||||
"formattedMessages": "Mensaxes con formato",
|
||||
"@formattedMessages": {},
|
||||
"verifyOtherDevice": "🔐 Verificar outro dispositivo",
|
||||
"@verifyOtherDevice": {},
|
||||
"verifyOtherUser": "🔐 Verificar outra usuaria",
|
||||
"@verifyOtherUser": {},
|
||||
"verifyOtherDeviceDescription": "Ao verificar outro dispositivo estás compartindo as chaves, aumentando a túa seguridade 💪. Ao iniciar a verificación aparecerá unha ventá emerxente nos dous dispositivos. Nesa ventá verás varios emojis ou números que tes que comparar entre eles. O mellor xeito de facelo é ter os dous dispositivos contigo cando inicias o proceso de verificación. 🤳",
|
||||
"@verifyOtherDeviceDescription": {},
|
||||
"canceledKeyVerification": "{sender} desbotou a verificación da chave",
|
||||
"@canceledKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"isReadyForKeyVerification": "{sender} xa pode verificar a chave",
|
||||
"@isReadyForKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"forwardMessageTo": "Reenviar a mensaxe a {roomName}?",
|
||||
"@forwardMessageTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"roomName": {}
|
||||
}
|
||||
},
|
||||
"sendTypingNotificationsDescription": "As outras participantes da conversa poden ver cando estás a escribir unha mensaxe.",
|
||||
"@sendTypingNotificationsDescription": {},
|
||||
"formattedMessagesDescription": "Mostrar texto enriquecido nas mensaxes como letra grosa usando markdown.",
|
||||
"@formattedMessagesDescription": {},
|
||||
"verifyOtherUserDescription": "Se verificas a outra usuaria, podes ter a certeza de que sabes con quen estás a conversar. 💪\n\nAo iniciar a verificación, ti mais a outra usuaria veredes unha ventá emerxente na app onde aparecerán varios emojis ou números que teredes que comparar entre vós.\n\nO mellor xeito de facelo é en persoa o cunha chamada de vídeo. 👭",
|
||||
"@verifyOtherUserDescription": {},
|
||||
"requestedKeyVerification": "{sender} solicitou verificar a chave",
|
||||
"@requestedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"acceptedKeyVerification": "{sender} aceptou a verificación da chave",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"completedKeyVerification": "{sender} completou a verificación da chave",
|
||||
"@completedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"startedKeyVerification": "{sender} comezou coa verificación da chave",
|
||||
"@startedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"presenceStyle": "Presenza:",
|
||||
"@presenceStyle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"hidePresences": "Agochar Lista de estados?",
|
||||
"@hidePresences": {},
|
||||
"presencesToggle": "Mostra mensaxes de estado de outras usuarias",
|
||||
"@presencesToggle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"incomingMessages": "Mensaxes recibidas",
|
||||
"@incomingMessages": {}
|
||||
}
|
||||
|
|
|
|||
55
assets/l10n/intl_ia.arb
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"repeatPassword": "Repeter le contrasigno",
|
||||
"@repeatPassword": {},
|
||||
"notAnImage": "Non es un file de imagine.",
|
||||
"@notAnImage": {},
|
||||
"remove": "Remover",
|
||||
"@remove": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"importEmojis": "Importar emojis",
|
||||
"@importEmojis": {},
|
||||
"importFromZipFile": "Importar ab un file .zip",
|
||||
"@importFromZipFile": {},
|
||||
"importNow": "Importar ora",
|
||||
"@importNow": {},
|
||||
"exportEmotePack": "Exportar pacchetto de emotes como un .zip",
|
||||
"@exportEmotePack": {},
|
||||
"replace": "Reimplaciar",
|
||||
"@replace": {},
|
||||
"about": "A proposito de",
|
||||
"@about": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"accept": "Acceptar",
|
||||
"@accept": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"acceptedTheInvitation": "👍 {username} acceptava tu invitation",
|
||||
"@acceptedTheInvitation": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"username": {}
|
||||
}
|
||||
},
|
||||
"account": "Conto",
|
||||
"@account": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"addEmail": "Adder email",
|
||||
"@addEmail": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"supposedMxid": "Isto deberea esser {mxid}",
|
||||
"@supposedMxid": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"mxid": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -639,7 +639,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"emoteInvalid": "Недопустимый краткий код эмодзи!",
|
||||
"emoteInvalid": "Недопустимый код эмодзи!",
|
||||
"@emoteInvalid": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -654,7 +654,7 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"emoteShortcode": "Краткий код для эмодзи",
|
||||
"emoteShortcode": "Код эмодзи",
|
||||
"@emoteShortcode": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
|
|
@ -2260,7 +2260,7 @@
|
|||
"@disableEncryptionWarning": {},
|
||||
"deviceKeys": "Ключи устройств:",
|
||||
"@deviceKeys": {},
|
||||
"noBackupWarning": "Внимание! Без резервных копий, Вы потеряете доступ к своим зашифрованным сообщениям. Крайне рекомендуется включить резервные копии перед выходом.",
|
||||
"noBackupWarning": "Внимание! Без резервного копиирования, Вы потеряете доступ к своим зашифрованным сообщениям. Крайне рекомендуется включить резервное копирование перед выходом.",
|
||||
"@noBackupWarning": {},
|
||||
"noOtherDevicesFound": "Другие устройства не найдены",
|
||||
"@noOtherDevicesFound": {},
|
||||
|
|
@ -2477,5 +2477,128 @@
|
|||
"joinSpace": "Присоединиться к пространству",
|
||||
"@joinSpace": {},
|
||||
"searchForUsers": "Поиск @пользователей...",
|
||||
"@searchForUsers": {}
|
||||
"@searchForUsers": {},
|
||||
"thisDevice": "Данное устройство:",
|
||||
"@thisDevice": {},
|
||||
"decline": "Отклонить",
|
||||
"@decline": {},
|
||||
"databaseBuildErrorBody": "Невозможно собрать базу данных SQlite. Приложение пытается использовать старую базу данных. Пожалуйста, сообщите об этой ошибке разработчикам по адресу {url}. Сообщение об ошибке: {error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"initAppError": "Произошла ошибка при запуске приложения",
|
||||
"@initAppError": {},
|
||||
"sessionLostBody": "Ваш сеанс утерян. Пожалуйста, сообщите об этой ошибке разработчикам по адресу {url}. Сообщение об ошибке: {error}",
|
||||
"@sessionLostBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"restoreSessionBody": "Приложение пытается восстановить сеанс из резервной копии. Пожалуйста, сообщите об этой ошибке разработчикам по адресу {url}. Сообщение об ошибке: {error}",
|
||||
"@restoreSessionBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"subspace": "Субпространство",
|
||||
"@subspace": {},
|
||||
"addChatOrSubSpace": "Добавить чат или субпространство",
|
||||
"@addChatOrSubSpace": {},
|
||||
"youInvitedToBy": "📩 Вы были приглашены по ссылке на:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceipts": "Отправка квитанций о прочтении",
|
||||
"@sendReadReceipts": {},
|
||||
"verifyOtherUser": "🔐 Подтвердить другого пользователя",
|
||||
"@verifyOtherUser": {},
|
||||
"verifyOtherDevice": "🔐 Подтвердить другое устройство",
|
||||
"@verifyOtherDevice": {},
|
||||
"forwardMessageTo": "Переслать сообщение в {roomName}?",
|
||||
"@forwardMessageTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"roomName": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceiptsDescription": "Другие участники чата могут видеть, когда вы прочитали сообщение.",
|
||||
"@sendReadReceiptsDescription": {},
|
||||
"transparent": "Прозрачный",
|
||||
"@transparent": {},
|
||||
"verifyOtherUserDescription": "Если вы подтвердите другого пользователя, то вы можете быть уверены зная, кому вы действительно пишете. 💪\n\nКогда вы начинаете подтверждение, вы и другой пользователь увидите всплывающее окно в приложении. Там вы увидите ряд чисел или эмодзи, которые вы должны сравнить друг с другом.\n\nЛучший способ сделать это - встретиться в реальной жизни или по видео звонку. 👭",
|
||||
"@verifyOtherUserDescription": {},
|
||||
"verifyOtherDeviceDescription": "При подтверждении другого устройства эти устройства могут обмениваться ключами, повышая общую безопасность. 💪 При запуске подтверждения в приложении на обоих устройствах появится всплывающее окно. Там вы увидите ряд чисел или эмодзи, которые вы должны сравнить друг с другом. Лучше иметь оба устройства под рукой перед началом проверки. 🤳",
|
||||
"@verifyOtherDeviceDescription": {},
|
||||
"formattedMessagesDescription": "Отображать содержимое расширенных сообщений, такой как жирный текст, с помощью Markdown.",
|
||||
"@formattedMessagesDescription": {},
|
||||
"acceptedKeyVerification": "{sender} принял(а) подтверждение ключей",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"canceledKeyVerification": "{sender} отклонил(а) подтверждение ключей",
|
||||
"@canceledKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"sendTypingNotificationsDescription": "Другие участники чата могут видеть, когда вы набираете новое сообщение.",
|
||||
"@sendTypingNotificationsDescription": {},
|
||||
"formattedMessages": "Форматированные сообщения",
|
||||
"@formattedMessages": {},
|
||||
"startedKeyVerification": "{sender} начал(а) подтверждение ключей",
|
||||
"@startedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"isReadyForKeyVerification": "{sender} готов(а) к подтверждению ключей",
|
||||
"@isReadyForKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"requestedKeyVerification": "{sender} запросил(а) подтверждение ключей",
|
||||
"@requestedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"completedKeyVerification": "{sender} завершил(а) подтверждение ключей",
|
||||
"@completedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"incomingMessages": "Входящие сообщения",
|
||||
"@incomingMessages": {},
|
||||
"presencesToggle": "Показывать сообщения в статусах других пользователей",
|
||||
"@presencesToggle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"presenceStyle": "Представление:",
|
||||
"@presenceStyle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"hidePresences": "Скрыть список статусов?",
|
||||
"@hidePresences": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2298,7 +2298,7 @@
|
|||
"path": {}
|
||||
}
|
||||
},
|
||||
"reportErrorDescription": "😭哦不。出了点差错。如果你愿意,可以向开发人员报告此错误。",
|
||||
"reportErrorDescription": "😭 哦不。出了点差错。如果你愿意,可以向开发人员报告此错误。",
|
||||
"@reportErrorDescription": {},
|
||||
"noBackupWarning": "警告!如果不启用聊天备份,你将无法访问加密消息。强烈建议在注销前先启用聊天备份。",
|
||||
"@noBackupWarning": {},
|
||||
|
|
@ -2453,5 +2453,152 @@
|
|||
"searchChatsRooms": "搜索 #聊天,@用户…",
|
||||
"@searchChatsRooms": {},
|
||||
"databaseMigrationBody": "请稍候。可能需要稍等片刻。",
|
||||
"@databaseMigrationBody": {}
|
||||
"@databaseMigrationBody": {},
|
||||
"thisDevice": "此设备:",
|
||||
"@thisDevice": {},
|
||||
"publicSpaces": "公开空间",
|
||||
"@publicSpaces": {},
|
||||
"passwordIsWrong": "你输入的密码有误",
|
||||
"@passwordIsWrong": {},
|
||||
"pleaseEnterYourCurrentPassword": "请输入你当前的密码",
|
||||
"@pleaseEnterYourCurrentPassword": {},
|
||||
"publicLink": "公开链接",
|
||||
"@publicLink": {},
|
||||
"nothingFound": "未找到任何内容…",
|
||||
"@nothingFound": {},
|
||||
"decline": "拒绝",
|
||||
"@decline": {},
|
||||
"newPassword": "新的密码",
|
||||
"@newPassword": {},
|
||||
"passwordsDoNotMatch": "密码不匹配",
|
||||
"@passwordsDoNotMatch": {},
|
||||
"subspace": "子空间",
|
||||
"@subspace": {},
|
||||
"select": "选择",
|
||||
"@select": {},
|
||||
"pleaseChooseAStrongPassword": "请选择一个强密码",
|
||||
"@pleaseChooseAStrongPassword": {},
|
||||
"addChatOrSubSpace": "添加聊天或子空间",
|
||||
"@addChatOrSubSpace": {},
|
||||
"leaveEmptyToClearStatus": "留空以清除你的状态。",
|
||||
"@leaveEmptyToClearStatus": {},
|
||||
"joinSpace": "加入空间",
|
||||
"@joinSpace": {},
|
||||
"searchForUsers": "搜索 @用户…",
|
||||
"@searchForUsers": {},
|
||||
"databaseBuildErrorBody": "无法构建 SQLite 数据库。目前应用尝试使用旧数据库。请将此错误报告给开发者,网址为 {url}。错误消息为:{error}",
|
||||
"@databaseBuildErrorBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"initAppError": "在初始化应用时发生错误",
|
||||
"@initAppError": {},
|
||||
"sessionLostBody": "你的会话已丢失。请将此错误报告给开发者,网址为 {url}。错误消息为:{error}",
|
||||
"@sessionLostBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"restoreSessionBody": "应用现在尝试从备份中恢复你的会话。请将此错误报告给开发者,网址为 {url}。错误消息为:{error}",
|
||||
"@restoreSessionBody": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"url": {},
|
||||
"error": {}
|
||||
}
|
||||
},
|
||||
"sendTypingNotificationsDescription": "聊天中的其他参与者可以看到你正在输入新消息。",
|
||||
"@sendTypingNotificationsDescription": {},
|
||||
"formattedMessagesDescription": "使用 Markdown 显示富文本内容,例如加粗文本。",
|
||||
"@formattedMessagesDescription": {},
|
||||
"verifyOtherUserDescription": "如果你验证了其他用户,就可以确保你清楚自己正在与谁进行通信。💪\n\n当你开始验证时,你和其他用户将在应用中看到一个弹出窗口。然后你会看到一系列表情符号或数字,你和其他用户需要比较它们是否一致。\n\n最好的方式是线下会面或开始视频通话。👭",
|
||||
"@verifyOtherUserDescription": {},
|
||||
"verifyOtherDeviceDescription": "当你验证另一个设备时,这些设备可以交换密钥,从而提高整体安全性。 💪 当你开始验证时,两个设备上的应用都将显示一个弹出窗口。然后你会看到一系列表情符号或数字,你需要比较两个设备上显示的内容。在开始验证之前,最好将两个设备都放在手边。🤳",
|
||||
"@verifyOtherDeviceDescription": {},
|
||||
"canceledKeyVerification": "{sender} 取消了密钥验证",
|
||||
"@canceledKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceipts": "发送已读回执",
|
||||
"@sendReadReceipts": {},
|
||||
"formattedMessages": "格式化的消息",
|
||||
"@formattedMessages": {},
|
||||
"verifyOtherDevice": "🔐 验证其它设备",
|
||||
"@verifyOtherDevice": {},
|
||||
"verifyOtherUser": "🔐 验证其他用户",
|
||||
"@verifyOtherUser": {},
|
||||
"forwardMessageTo": "转发消息至 {roomName} ?",
|
||||
"@forwardMessageTo": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"roomName": {}
|
||||
}
|
||||
},
|
||||
"sendReadReceiptsDescription": "聊天中的其他参与者可以看到你是否读过消息。",
|
||||
"@sendReadReceiptsDescription": {},
|
||||
"acceptedKeyVerification": "{sender} 接受了密钥验证",
|
||||
"@acceptedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"isReadyForKeyVerification": "{sender} 已准备好进行密钥验证",
|
||||
"@isReadyForKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"completedKeyVerification": "{sender} 完成了密钥验证",
|
||||
"@completedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"requestedKeyVerification": "{sender} 请求了密钥验证",
|
||||
"@requestedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"startedKeyVerification": "{sender} 开始了密钥验证",
|
||||
"@startedKeyVerification": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"sender": {}
|
||||
}
|
||||
},
|
||||
"transparent": "透明",
|
||||
"@transparent": {},
|
||||
"youInvitedToBy": "📩 你已通过链接被邀请到:\n{alias}",
|
||||
"@youInvitedToBy": {
|
||||
"placeholders": {
|
||||
"alias": {}
|
||||
}
|
||||
},
|
||||
"presencesToggle": "显示其他用户的状态消息",
|
||||
"@presencesToggle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"presenceStyle": "是否在线:",
|
||||
"@presenceStyle": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"hidePresences": "隐藏状态列表?",
|
||||
"@hidePresences": {},
|
||||
"incomingMessages": "传入消息",
|
||||
"@incomingMessages": {}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 18 KiB |
|
|
@ -1,80 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="64"
|
||||
height="30"
|
||||
viewBox="0 0 64 30"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
sodipodi:docname="typing.svg"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs12" />
|
||||
<sodipodi:namedview
|
||||
id="namedview10"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.9296875"
|
||||
inkscape:cx="24.018883"
|
||||
inkscape:cy="15.307632"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1012"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg8" />
|
||||
<circle
|
||||
style="fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
cx="10"
|
||||
cy="15"
|
||||
r="9"
|
||||
id="circle2">
|
||||
<animate
|
||||
attributeName="fill"
|
||||
dur="2s"
|
||||
values="#000000; #efefef; #000000"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 0.5; 1"
|
||||
keySplines="0 .75 .25 1; .5 0 .5 1"
|
||||
begin="0s" />
|
||||
</circle>
|
||||
<circle
|
||||
style="fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
cx="32"
|
||||
cy="15"
|
||||
r="9"
|
||||
id="circle4">
|
||||
<animate
|
||||
attributeName="fill"
|
||||
dur="2s"
|
||||
values="#000000; #efefef; #000000"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 0.5; 1"
|
||||
keySplines="0 .75 .25 1; .5 0 .5 1"
|
||||
begin="0.25s" />
|
||||
</circle>
|
||||
<circle
|
||||
style="fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"
|
||||
cx="54"
|
||||
cy="15"
|
||||
r="9"
|
||||
id="circle6">
|
||||
<animate
|
||||
attributeName="fill"
|
||||
dur="2s"
|
||||
values="#000000; #efefef; #000000"
|
||||
calcMode="spline"
|
||||
keyTimes="0; 0.5; 1"
|
||||
keySplines="0 .75 .25 1; .5 0 .5 1"
|
||||
begin="0.5s" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
2
docs/.well-known/org.flathub.VerifiedApps.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# im.fluffychat.Fluffychat
|
||||
8b25b37b-f160-4350-b4f6-9a04554e8f9e
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,138 +0,0 @@
|
|||
# Code Style
|
||||
|
||||
FluffyChat tries to be as minimal as possible even in the code style. We try to keep the code clean, simple and easy to read. The source code of the app is under `/lib` with the main entry point `/lib/main.dart`.
|
||||
|
||||
### Directory Structure:
|
||||
|
||||
|
||||
- /lib
|
||||
- /config
|
||||
- app_config.dart
|
||||
- ...Constants, styles and other configurations
|
||||
- /utils
|
||||
- handy_function.dart
|
||||
- ...Helper functions and extensions
|
||||
- /pages
|
||||
- /chat
|
||||
- chat.dart
|
||||
- chat_view.dart
|
||||
- /chat_list
|
||||
- chat_list.dart
|
||||
- chat_list_view.dart
|
||||
- ...The pages of the app separated in Controllers and Views
|
||||
- /widgets
|
||||
- /layouts
|
||||
- ...Custom widgets created for this project
|
||||
- main.dart
|
||||
|
||||
|
||||
Most of the business model is in the Famedly Matrix Dart SDK. We try to not keep a model inside of the source code but extend it under `/utils`.
|
||||
|
||||
### Separation of Controllers and Views
|
||||
|
||||
We split views and controller logic with stateful widgets as controller where the build method just builds a stateless widget which receives the state as the only parameter. A common controller would look like this:
|
||||
|
||||
```dart
|
||||
// /lib/controller/enter_name_controller.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EnterName extends StatefulWidget {
|
||||
@override
|
||||
EnterNameController createState() => EnterNameController();
|
||||
}
|
||||
|
||||
class EnterNameController extends State<EnterName> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
String name = 'Unknown';
|
||||
|
||||
/// Changes the name with the content in the textfield. If the textfield is
|
||||
/// empty, this breaks up and displays a SnackBar.
|
||||
void setNameAction() {
|
||||
if (textEditingController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('You have not entered your name'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => name = textEditingController.text);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => EnterNameView(this);
|
||||
}
|
||||
```
|
||||
|
||||
So we have a controller for a `EnterName` view which as a `TextEditingController`, a state `name` and an action `void setNameAction()`. Actions must always be methods of a type, that we dont need to pass parameters in the corresponding view class and must have dartdoc comments.
|
||||
|
||||
The view class could look like this:
|
||||
|
||||
```dart
|
||||
// /lib/views/enter_name_view.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EnterNameView extends StatelessWidget {
|
||||
final EnterNameController controller;
|
||||
|
||||
const EnterNameView(this.controller, {Key key}) : super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Your name: ${controller.name}'),
|
||||
),
|
||||
body: Center(
|
||||
child: TextField(
|
||||
controller: controller.textEditingController,
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: controller.setNameAction,
|
||||
child: Icon(Icons.save),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Views should just contain code which describes the view. All other parameters or logic should be in the controller. The job of the view class is just to take the current state and build the widget tree and pipe the callbacks back. If there is any calulation necessary which is not solveable as a simple if-else or switch statement, it should be done in an external helper function unter `/lib/utils/`.
|
||||
|
||||
All file names must be lower_snake_case. All views must have a `View` suffix and all controller must have a `Controller` suffix. Widgets may have a controller too but they should pass the callbacks back to the view where possible. Calling one line methods directly in the view is only recommended if there is no need to pass a parameter.
|
||||
|
||||
To perform an action on state initialization we use the initState method:
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
}
|
||||
```
|
||||
|
||||
And the dispose method to perform an action on disposing:
|
||||
```dart
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
}
|
||||
```
|
||||
|
||||
To run code after the widget was created first we use the WidgetBindings in the initState:
|
||||
```dart
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
// Do something when build is finished
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
We do not allow code with wrong formatting. Please run `flutter format lib` if your IDE doesn't do this automatically.
|
||||
|
||||
### Code Analyzis
|
||||
|
||||
We do not allow codes with dart errors or warnings. We use the [pedantic](https://pub.dev/packages/pedantic) package for static code analysis with additional rules under `analysis_options.yaml`.
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# F-Droid Repository
|
||||
|
||||
Our own F-Droid repository contains the Google Services which is not possible in the main F-Droid repository. Release candidates
|
||||
are also published on it.
|
||||
|
||||
## Add Repository to F-Droid
|
||||
|
||||
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click it**.
|
||||
|
||||
<a href="fdroidrepos://fluffychat.im/repo/stable/repo/?fingerprint=5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07" >
|
||||
<img src="qr-stable.svg" width="300" height="300"/>
|
||||
</a>
|
||||
|
||||
|
||||
### If the QR-Code doesn't work:
|
||||
|
||||
First check if you have f-droid installed. If not you can get it from: [https://f-droid.org/](https://f-droid.org/).
|
||||
After you made sure you installed it and you didn't have it installed before you can try again the QR-Code.
|
||||
If this still isn't working follow the next steps:
|
||||
|
||||
1. Open the f-droid App
|
||||
2. Go to the `Settings` Tab in the Bottom bar
|
||||
3. Click the `Repositories` Action
|
||||
4. Click on the plus sign at the top.
|
||||
5. Fill in `https://fluffychat.im/repo/stable/repo/` into the top field and `5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07` in the bottom field.
|
||||
|
||||
## What is the fingerprint?
|
||||
|
||||
The fingerprint of the Repository is: `5EDB5C4395B2F2D9BA682F6A1D275170CCE5365A6FA27D2220EA8D52A6D95F07`
|
||||
|
||||
# Nightly Repository
|
||||
|
||||
## Add Repository to F-Droid
|
||||
|
||||
Easiest way to add the Repository is to either **scan the QR-Code** or if you are on your phone **directly click** it.
|
||||
|
||||
<a href="fdroidrepos://fluffychat.im/repo/nightly/repo/?fingerprint=21A469657300576478B623DF99D8EB889A80BCD939ACA60A4074741BEAEC397D" >
|
||||
<img src="qr-nightly.svg" width="300" height="300"/>
|
||||
</a>
|
||||
|
||||
|
||||
### If the QR-Code doesn't work:
|
||||
|
||||
First check if you have f-droid installed. If not you can get it from: [https://f-droid.org/](https://f-droid.org/).
|
||||
After you made sure you installed it and you didn't have it installed before you can try again the QR-Code.
|
||||
If this still isn't working follow the next steps:
|
||||
|
||||
1. Open the f-droid App
|
||||
2. Go to the `Settings` Tab in the Bottom bar
|
||||
3. Click the `Repositories` Action
|
||||
4. Click on the plus sign at the top.
|
||||
5. Fill in `https://fluffychat.im/repo/nightly/repo/` into the top field and `21A469657300576478B623DF99D8EB889A80BCD939ACA60A4074741BEAEC397D` in the bottom field.
|
||||
|
||||
## What is the fingerprint?
|
||||
|
||||
The fingerprint of the Repository is: `21A469657300576478B623DF99D8EB889A80BCD939ACA60A4074741BEAEC397D`
|
||||
BIN
docs/feature1.gif
Normal file
|
After Width: | Height: | Size: 647 KiB |
BIN
docs/feature2.gif
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
docs/feature3.gif
Normal file
|
After Width: | Height: | Size: 766 KiB |
BIN
docs/feature4.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/feature5.gif
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
docs/feature6.gif
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
docs/feature7.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/feature8.gif
Normal file
|
After Width: | Height: | Size: 448 KiB |
BIN
docs/feature9.gif
Normal file
|
After Width: | Height: | Size: 1 MiB |
205
docs/index.html
|
|
@ -17,25 +17,39 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.png">
|
||||
<link href="tailwind.css" rel="stylesheet">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: Zen Kurenaido;
|
||||
src: url(ZenKurenaido-Regular.ttf);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="flex flex-col items-center justify-center min-h-screen w-screen bg-gradient-to-t from-purple-200 to-blue-50 dark:from-gray-800 dark:to-slate-900 p-4"
|
||||
class="flex flex-col items-center min-h-screen w-full bg-gradient-to-t from-purple-200 to-blue-50 dark:from-purple-900 dark:to-slate-900"
|
||||
style="font-family: 'Zen Kurenaido', sans-serif;">
|
||||
<img src="favicon.png" class="h-10" />
|
||||
<h1 class="flex text-4xl items-center mb-4">
|
||||
<span style="color: #5625BA">Fluffy</span>
|
||||
<span style="color: #41a2bc">Chat</span>
|
||||
</h1>
|
||||
<img src="screenshots/screenshots.png" class="sm:max-w-lg max-w-screen mb-8" />
|
||||
<div
|
||||
class="w-full md:h-12 min-h-12 bg-white dark:bg-gray-800 bg-opacity-50 border-b dark:border-gray-600 px-4 py-4 md:py-0 mb-8">
|
||||
<nav class="flex flex-wrap h-full justify-center items-center space-x-6 w-full max-w-4xl m-auto">
|
||||
<a href="https://ko-fi.com/krille/posts"
|
||||
class="text-lg dark:text-white hover:text-purple-800 dark:hover:text-purple-400">News</a>
|
||||
<a href="https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md"
|
||||
class="text-lg dark:text-white hover:text-purple-800 dark:hover:text-purple-400">Changelog</a>
|
||||
<a href="https://github.com/krille-chan/fluffychat/wiki"
|
||||
class="text-lg dark:text-white hover:text-purple-800 dark:hover:text-purple-400">Wiki</a>
|
||||
|
||||
<div class="max-w-lg mb-8 flex justify-center flex-wrap">
|
||||
<a href="https://github.com/krille-chan/fluffychat"
|
||||
class="text-lg dark:text-white hover:text-purple-800 dark:hover:text-purple-400">Code</a>
|
||||
<div class="md:flex-grow"> </div>
|
||||
<a href='https://ko-fi.com/C1C86VN53' target='_blank' class="m-2 hover:scale-110 transition-transform "><img
|
||||
class="h-7" src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0'
|
||||
alt='Buy Me a Coffee at ko-fi.com' /></a>
|
||||
<a href="https://mastodon.art/@krille" rel="me" class="m-2 hover:scale-110 transition-transform "><img
|
||||
src="mastodon.svg" class="h-7" /></a>
|
||||
</nav>
|
||||
</div>
|
||||
<img src="info-logo.png" alt="FluffyChat Logo" class="h-56" />
|
||||
<p class="text-xl dark:text-gray-200 text-gray-700 mb-8">The cutest messenger in [<a href="https://matrix.org"
|
||||
target="_blank" class="text-xl underline hover:text-purple-800 dark:hover:text-purple-400">matrix</a>]
|
||||
</p>
|
||||
|
||||
<img src="screenshots/screenshots.png" alt="Mobile and desktop screenshots" class="max-w-xl mb-16 w-full px-8" />
|
||||
|
||||
<div class="max-w-lg mb-16 flex justify-center flex-wrap">
|
||||
<a href="https://apps.apple.com/app/fluffychat/id1551469600"><img src="appstore-badge.png"
|
||||
class="w-36 pr-2 mb-2 inline hover:scale-105 transition-transform"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=chat.fluffy.fluffychat"><img src="google-play-badge.png"
|
||||
|
|
@ -52,68 +66,115 @@
|
|||
class="w-36 pr-2 mb-2 hover:scale-105 transition-transform inline"></a>
|
||||
</div>
|
||||
|
||||
<div class="flex mb-8 justify-center content-center">
|
||||
<a rel="me"
|
||||
class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
||||
rel="me" href="https://mastodon.art/@krille">
|
||||
<svg class="fill-current h-6" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<clipPath id="_clip1">
|
||||
<rect x="33.6" y="-0.035" width="932.844" height="1000" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip1)">
|
||||
<path
|
||||
d="M946.586,599.455c-13.713,70.541 -122.816,147.742 -248.121,162.703c-65.341,7.796 -129.674,14.962 -198.275,11.815c-112.191,-5.139 -200.716,-26.776 -200.716,-26.776c0,10.92 0.673,21.319 2.02,31.044c14.586,110.711 109.787,117.344 199.967,120.436c91.021,3.114 172.068,-22.44 172.068,-22.44l3.74,82.281c0,0 -63.666,34.185 -177.079,40.473c-62.539,3.437 -140.192,-1.573 -230.636,-25.511c-196.158,-51.916 -229.893,-260.996 -235.055,-473.143c-1.573,-62.987 -0.603,-122.381 -0.603,-172.056c0,-216.931 142.142,-280.516 142.142,-280.516c71.672,-32.914 194.655,-46.755 322.508,-47.8l3.142,0c127.853,1.045 250.917,14.886 322.583,47.8c0,0 142.138,63.585 142.138,280.516c0,0 1.783,160.053 -19.823,271.174"
|
||||
style="fill-rule:nonzero;" />
|
||||
<path
|
||||
d="M798.748,345.11l0,262.667l-104.07,0l0,-254.946c0,-53.743 -22.614,-81.021 -67.847,-81.021c-50.012,0 -75.077,32.359 -75.077,96.343l0,139.547l-103.457,0l0,-139.547c0,-63.984 -25.07,-96.343 -75.082,-96.343c-45.233,0 -67.847,27.278 -67.847,81.021l0,254.946l-104.07,0l0,-262.667c0,-53.683 13.669,-96.343 41.127,-127.904c28.314,-31.561 65.395,-47.741 111.425,-47.741c53.256,0 93.585,20.468 120.251,61.41l25.922,43.451l25.927,-43.451c26.66,-40.942 66.99,-61.41 120.251,-61.41c46.025,0 83.106,16.18 111.425,47.741c27.453,31.561 41.122,74.221 41.122,127.904"
|
||||
style="fill:#fff;fill-rule:nonzero;" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="inline-block text-indigo-500 no-underline hover:text-indigo-900 hover:scale-105 transition-all text-center h-auto p-4"
|
||||
href="https://matrix.to/#/#fluffychat:matrix.org">
|
||||
<svg class="fill-current h-6" enable-background="new -91 49.217 56.693 56.693" id="Layer_1" version="1.1"
|
||||
viewBox="-91 49.217 56.693 56.693" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path
|
||||
d="M-38.3289,79.8244c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.5351,1.5264l-3.0737-9.1321l4.4169-1.4866 c2.2362-0.7526,3.4388-3.1756,2.6861-5.4117c-0.7526-2.2362-3.1756-3.4388-5.4117-2.6861l-4.4168,1.4866l-1.4877-4.4201 c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861v0c-2.2362,0.7526-3.4388,3.1756-2.6861,5.4117l1.4877,4.4201l-9.3246,3.1385 l-1.4697-4.3666c-0.7527-2.2362-3.1756-3.4388-5.4117-2.6861c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117l1.4697,4.3666 l-4.445,1.4961c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117v0c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861l4.445-1.4961 l3.0737,9.1321l-4.3268,1.4563c-2.2362,0.7527-3.4388,3.1756-2.6861,5.4117c0.7526,2.2362,3.1756,3.4388,5.4117,2.6861 l4.3268-1.4563l1.5778,4.6877c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5778-4.6877 l9.3246-3.1385l1.5598,4.6342c0.7527,2.2362,3.1756,3.4388,5.4117,2.6861c2.2362-0.7527,3.4388-3.1756,2.6861-5.4117l-1.5598-4.6342 l4.5351-1.5264C-38.7789,84.4835-37.5762,82.0606-38.3289,79.8244z M-65.6982,84.5288l-3.0737-9.1321l9.3246-3.1385l3.0737,9.1321 L-65.6982,84.5288z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="inline-block no-underline hover:scale-105 transition-all text-center h-auto p-4"
|
||||
href="https://ko-fi.com/krille">
|
||||
<img src="kofi_button_dark.png" class="h-6 fill-current" />
|
||||
</a>
|
||||
<div class="grid md:grid-cols-3 md:grid-rows-3 max-w-4xl justify-center w-full mb-16">
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated dancing woman" loading="lazy" src="feature1.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Easy to use</h1>
|
||||
<p class="text-center dark:text-white">FluffyChat is designed to be as easy to use as possible. No one
|
||||
should be left behind.</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated pencil" loading="lazy" src="feature2.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Material You</h1>
|
||||
<p class="text-center dark:text-white">The well polished design is based on Material You and works great on
|
||||
all platforms.</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated mechanical arm" loading="lazy" src="feature3.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Secure</h1>
|
||||
<p class="text-center dark:text-white">With end-to-end encryption, cross-signing and encrypted backups,
|
||||
FluffyChat is one of the most secure messenger out there.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated planet earth" loading="lazy" src="feature4.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Decentral</h1>
|
||||
<p class="text-center dark:text-white">You can choose the <a href="https://joinmatrix.org"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">server</a> you want to use or
|
||||
even <a href="https://matrix.org/ecosystem/servers/"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">self-host</a> your own!</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated bell" loading="lazy" src="feature5.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Push Notifications</h1>
|
||||
<p class="text-center dark:text-white">You can choose between Firebase Cloud Messaging or the more privacy
|
||||
focused <a href="https://unifiedpush.org"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">Unified Push</a>.</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated rocket" loading="lazy" src="feature6.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Spaces</h1>
|
||||
<p class="text-center dark:text-white">With spaces you can join or create a community which organizes chats
|
||||
and users. Using sub-spaces you can even nest your communities.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated glass sphere" loading="lazy" src="feature7.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Video calls</h1>
|
||||
<p class="text-center dark:text-white">Still an experimental feature but you can already try out video and
|
||||
audio calls, compatible with other [matrix] clients.</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated chick" loading="lazy" src="feature8.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Stickers</h1>
|
||||
<p class="text-center dark:text-white">Create your own sticker sets and share them with your friends. You
|
||||
can even use them as inline emojis.</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center p-8">
|
||||
<img alt="Animated whoa emoji" loading="lazy" src="feature9.gif" class="h-32" />
|
||||
<h1 class="text-purple-500 dark:text-purple-300 text-2xl">Compatible</h1>
|
||||
<p class="text-center dark:text-white">FluffyChat is compatible with any other [matrix] client like <a
|
||||
href="https://element.io"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">Element</a>,
|
||||
<a href="https://nheko-reborn.github.io/"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">Nheko</a>, <a
|
||||
href="https://cinny.in" class="underline hover:text-purple-800 dark:hover:text-purple-400">Cinny</a>
|
||||
or <a href="https://apps.kde.org/de/neochat/"
|
||||
class="underline hover:text-purple-800 dark:hover:text-purple-400">NeoChat</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Footer-->
|
||||
<div class="w-full text-sm text-center max-w-lg">
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://github.com/krille-chan/fluffychat">Source
|
||||
code</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md">Privacy</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md">Changelog</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://github.com/krille-chan/fluffychat/blob/main/docs/fdroid_repo.md">FluffyChat F-Droid
|
||||
repository</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800"
|
||||
href="https://krillefear.gitlab.io">Created
|
||||
by Krille Fear</a>
|
||||
<div class="w-full bg-white dark:bg-gray-800 bg-opacity-50 border-t dark:border-gray-600 flex justify-center">
|
||||
<footer class="w-full text-center max-w-4xl p-4 text-slate-700 dark:text-slate-200">
|
||||
|
||||
|
||||
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://liberapay.com/KrilleChritzelius">Liberapay</a>
|
||||
-
|
||||
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://github.com/krille-chan/fluffychat">Source
|
||||
code</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://github.com/krille-chan/fluffychat/blob/main/PRIVACY.md">Privacy</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://github.com/krille-chan/fluffychat/blob/main/CHANGELOG.md">Changelog</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://hosted.weblate.org/projects/fluffychat/">Translations</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://googlefonts.github.io/noto-emoji-animation/">Noto Animated Emojis</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://keys.mailvelope.com/pks/lookup?op=get&search=christian-pauly%40posteo.de">Contact</a>
|
||||
-
|
||||
<a class="text-slate-700 dark:text-slate-200 no-underline hover:text-purple-800 text-sm"
|
||||
href="https://krille-chan.github.io">Created
|
||||
by Krille-chan</a>
|
||||
|
||||
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
BIN
docs/info-logo.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
10
docs/mastodon.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="75" height="79" viewBox="0 0 75 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" fill="url(#paint0_linear_549_34)"/>
|
||||
<path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_549_34" x1="37.0692" y1="0" x2="37.0692" y2="79" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6364FF"/>
|
||||
<stop offset="1" stop-color="#563ACC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
|
@ -1,543 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="390.000000pt" height="390.000000pt" viewBox="0 0 390.000000 390.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,390.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M240 3450 l0 -210 210 0 210 0 0 210 0 210 -210 0 -210 0 0 -210z
|
||||
m360 0 l0 -150 -150 0 -150 0 0 150 0 150 150 0 150 0 0 -150z"/>
|
||||
<path d="M360 3450 l0 -90 90 0 90 0 0 90 0 90 -90 0 -90 0 0 -90z"/>
|
||||
<path d="M780 3630 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 3 30 30 l0 30 90 0 90 0 0 30 0 30 -90 0 -90 0 0 -30z"/>
|
||||
<path d="M1080 3600 l0 -60 -60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
l-30 0 0 -60 0 -60 -30 0 -30 0 0 -90 0 -90 -120 0 -120 0 0 -60 0 -60 -60 0
|
||||
-60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30
|
||||
l-30 0 0 -60 0 -60 30 0 30 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3
|
||||
-30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30
|
||||
27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0
|
||||
30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 60 0 60 0 0 30 0 30
|
||||
90 0 90 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 l-30 0
|
||||
0 60 0 60 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30
|
||||
l30 0 0 -60 0 -60 -30 0 c-27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30 0 -30
|
||||
-90 0 -90 0 0 -30 c0 -27 -3 -30 -30 -30 l-30 0 0 -90 0 -90 -30 0 c-27 0 -30
|
||||
3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 -30 0
|
||||
-30 -90 0 -90 0 0 30 c0 27 -3 30 -30 30 l-30 0 0 -60 0 -60 60 0 60 0 0 -30
|
||||
c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30
|
||||
-30 l0 -30 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27
|
||||
3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0
|
||||
-27 -3 -30 -30 -30 l-30 0 0 -150 0 -150 60 0 60 0 0 -30 0 -30 -60 0 -60 0 0
|
||||
-120 0 -120 30 0 c27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30
|
||||
-30 l0 -30 90 0 90 0 0 -60 0 -60 -30 0 c-27 0 -30 3 -30 30 l0 30 -60 0 -60
|
||||
0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 -3 -30 -30 l0 -30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 l30 0 0 -60 0 -60
|
||||
-30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3
|
||||
30 -30 30 l-30 0 0 -60 0 -60 60 0 60 0 0 30 c0 27 3 30 30 30 l30 0 0 -60 0
|
||||
-60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0
|
||||
27 -3 30 -30 30 l-30 0 0 -60 0 -60 150 0 150 0 0 30 0 30 60 0 60 0 0 -30 c0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30
|
||||
0 27 3 30 30 30 l30 0 0 -150 0 -150 30 0 30 0 0 120 0 120 30 0 30 0 0 -60 0
|
||||
-60 60 0 60 0 0 -60 0 -60 -30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0
|
||||
-30 0 -30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30
|
||||
30 -30 l30 0 0 60 0 60 -30 0 c-27 0 -30 3 -30 30 l0 30 60 0 60 0 0 -90 0
|
||||
-90 60 0 60 0 0 60 0 60 -30 0 c-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3
|
||||
30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30
|
||||
0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0 -30 0 -30 60 0
|
||||
60 0 0 30 0 30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30
|
||||
27 0 30 3 30 30 l0 30 60 0 60 0 0 90 0 90 30 0 c27 0 30 -3 30 -30 0 -27 3
|
||||
-30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 60 0 60 30 0 30 0 0 -90
|
||||
0 -90 30 0 30 0 0 -90 0 -90 30 0 30 0 0 90 0 90 30 0 c27 0 30 -3 30 -30 0
|
||||
-27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30
|
||||
-30 l0 -30 90 0 90 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27
|
||||
3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0
|
||||
30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3
|
||||
30 -30 30 l-30 0 0 90 0 90 30 0 c27 0 30 -3 30 -30 l0 -30 150 0 150 0 0 -60
|
||||
0 -60 -30 0 c-27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -60 0 -60 30 0 c27 0 30
|
||||
3 30 30 0 27 3 30 30 30 l30 0 0 -60 0 -60 30 0 30 0 0 60 0 60 60 0 60 0 0
|
||||
-60 0 -60 30 0 30 0 0 90 0 90 60 0 60 0 0 30 0 30 -60 0 -60 0 0 -30 c0 -27
|
||||
-3 -30 -30 -30 l-30 0 0 120 0 120 60 0 60 0 0 30 0 30 -90 0 -90 0 0 30 0 30
|
||||
120 0 120 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 3 -30 30 l0 30 60 0 60 0 0 30 0 30 -60 0 -60 0 0 -30 c0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3
|
||||
30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 l30 0 0 60 0 60 -60 0 -60 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3
|
||||
-30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30
|
||||
-30 30 l-30 0 0 60 0 60 -60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30
|
||||
-3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30
|
||||
c0 -27 -3 -30 -30 -30 l-30 0 0 60 0 60 30 0 c27 0 30 3 30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30
|
||||
l0 30 150 0 150 0 0 30 c0 27 -3 30 -30 30 l-30 0 0 60 0 60 -30 0 c-27 0 -30
|
||||
3 -30 30 l0 30 60 0 60 0 0 60 0 60 -30 0 c-27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 l30 0 0 90 0 90 -90 0 -90 0 0 60 0 60 30 0 c27 0 30 3 30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 l0 30 90 0 90 0 0 30 c0 27 -3 30 -30 30 l-30 0 0
|
||||
60 0 60 30 0 30 0 0 60 0 60 -30 0 -30 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30
|
||||
-30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0 -30 0 -30
|
||||
-90 0 -90 0 0 60 0 60 -30 0 -30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 l0
|
||||
-30 60 0 60 0 0 -30 0 -30 -90 0 -90 0 0 30 0 30 -60 0 -60 0 0 60 0 60 30 0
|
||||
30 0 0 60 0 60 30 0 c27 0 30 3 30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 30
|
||||
0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 90 0 90 0 0
|
||||
60 0 60 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 -30 c0 -27 -3 -30
|
||||
-30 -30 l-30 0 0 60 0 60 -60 0 -60 0 0 -90 0 -90 -60 0 -60 0 0 30 c0 27 -3
|
||||
30 -30 30 l-30 0 0 60 0 60 60 0 60 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30
|
||||
0 -27 -3 -30 -30 -30 l-30 0 0 60 0 60 30 0 30 0 0 150 0 150 -60 0 -60 0 0
|
||||
-30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0 60 0 60 -30
|
||||
0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30 -60 0
|
||||
-60 0 0 -60 0 -60 -60 0 -60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 l0 30
|
||||
-120 0 -120 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30
|
||||
30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 30 0 30 -90 0 -90 0 0 -30 c0
|
||||
-27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30
|
||||
-30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 30 c0 27
|
||||
3 30 30 30 l30 0 0 60 0 60 30 0 c27 0 30 3 30 30 l0 30 -90 0 -90 0 0 -30 c0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30 0 -30 60 0 60
|
||||
0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 l-30 0 0
|
||||
-60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30
|
||||
30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -60
|
||||
0 -60 -30 0 -30 0 0 90 0 90 30 0 c27 0 30 3 30 30 l0 30 -60 0 -60 0 0 -30
|
||||
c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30 -90 0 -90 0 0 30 c0 27 3 30
|
||||
30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30
|
||||
l30 0 0 60 0 60 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 l-30 0 0 -60z m1080 -60
|
||||
l0 -60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30
|
||||
30 30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 -3 30 -30 30 l-30 0 0
|
||||
60 0 60 60 0 60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30
|
||||
90 0 90 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 120 0 120
|
||||
0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3
|
||||
-30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30
|
||||
-30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0
|
||||
-27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 -60 0 -60 -30 0
|
||||
-30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30
|
||||
3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 -3 -30 -30 l0 -30 -60 0 -60 0 0 -30 0 -30 60 0 60 0 0 -30 c0 -27 3 -30
|
||||
30 -30 l30 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0
|
||||
-60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 l30 0 0 -90 0 -90
|
||||
-30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
l-30 0 0 -90 0 -90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30
|
||||
-3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 -30 c0
|
||||
-27 3 -30 30 -30 l30 0 0 240 0 240 60 0 60 0 0 -30 0 -30 90 0 90 0 0 -30 0
|
||||
-30 -90 0 -90 0 0 -30 c0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 30 0 c27 0
|
||||
30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30
|
||||
-30 0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 30 0 30 0 0 -60 0 -60 30 0 c27
|
||||
0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30
|
||||
-3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0 -30
|
||||
0 -30 -90 0 -90 0 0 -30 0 -30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30
|
||||
-3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 -3 -30 -30 0 -27 3 -30 30 -30 l30 0 0 -60 0 -60 -60 0 -60 0 0 90 0 90
|
||||
-30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 -30 0 c-27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30
|
||||
27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
l-30 0 0 60 0 60 -60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30
|
||||
l0 30 -90 0 -90 0 0 -30 0 -30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 l30 0 0
|
||||
-60 0 -60 -30 0 -30 0 0 -60 0 -60 30 0 c27 0 30 3 30 30 l0 30 60 0 60 0 0
|
||||
30 c0 27 -3 30 -30 30 l-30 0 0 60 0 60 30 0 30 0 0 -60 0 -60 30 0 c27 0 30
|
||||
-3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0
|
||||
30 -3 30 -30 l0 -30 -60 0 -60 0 0 -30 0 -30 -60 0 -60 0 0 -60 0 -60 -30 0
|
||||
c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 -60 0 -60 -30 0 c-27 0 -30 3
|
||||
-30 30 l0 30 -120 0 -120 0 0 -30 0 -30 -90 0 -90 0 0 -30 0 -30 60 0 60 0 0
|
||||
-30 0 -30 60 0 60 0 0 -30 0 -30 -60 0 -60 0 0 -60 0 -60 -30 0 c-27 0 -30 3
|
||||
-30 30 0 27 -3 30 -30 30 l-30 0 0 -90 0 -90 60 0 60 0 0 30 c0 27 3 30 30 30
|
||||
27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0
|
||||
0 -90 0 -90 30 0 30 0 0 120 0 120 -30 0 -30 0 0 60 0 60 30 0 c27 0 30 3 30
|
||||
30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 l0 -30 90 0
|
||||
90 0 0 30 c0 27 3 30 30 30 l30 0 0 -90 0 -90 60 0 60 0 0 -30 c0 -27 -3 -30
|
||||
-30 -30 l-30 0 0 -90 0 -90 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3
|
||||
30 30 l0 30 120 0 120 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30
|
||||
-30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30
|
||||
60 0 60 0 0 -30 0 -30 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 l-30 0 0 -60 0
|
||||
-60 -30 0 -30 0 0 -60 0 -60 30 0 30 0 0 -60 0 -60 60 0 60 0 0 -30 0 -30 -60
|
||||
0 -60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 l0 -30 -90 0 -90 0 0
|
||||
30 0 30 -60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27
|
||||
3 -30 30 -30 l30 0 0 -90 0 -90 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 l-30 0 0
|
||||
-60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27
|
||||
0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 l-30 0 0 60 0
|
||||
60 -30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3
|
||||
30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30
|
||||
30 30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 60 0 60 -30 0 c-27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 l-30
|
||||
0 0 -60 0 -60 -30 0 -30 0 0 90 0 90 30 0 c27 0 30 3 30 30 l0 30 -60 0 -60 0
|
||||
0 60 0 60 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3
|
||||
-30 -30 -30 l-30 0 0 -90 0 -90 -30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 3 -30 30 0 27 3 30 30 30 l30 0 0 60 0 60 90 0 90 0 0 30 0 30 -60 0 -60
|
||||
0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0
|
||||
30 -3 30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 -30 0 c-27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30
|
||||
-3 -30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -150 0 -150 60 0 60 0 0 -30 0 -30
|
||||
-60 0 -60 0 0 30 0 30 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30
|
||||
-30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3
|
||||
-30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30 c0 -27
|
||||
3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27
|
||||
-3 30 -30 30 l-30 0 0 60 0 60 -30 0 -30 0 0 60 0 60 30 0 30 0 0 90 0 90 -30
|
||||
0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30
|
||||
-30 -30 l-30 0 0 -60 0 -60 -30 0 -30 0 0 -60 0 -60 30 0 c27 0 30 3 30 30 0
|
||||
27 3 30 30 30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30
|
||||
-30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 -30 0 c-27 0 -30 3 -30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 90 0 90 -30 0 c-27 0
|
||||
-30 -3 -30 -30 l0 -30 -90 0 -90 0 0 -30 0 -30 -60 0 -60 0 0 30 c0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 l0 30 60 0 60 0 0
|
||||
-30 0 -30 -60 0 -60 0 0 -30 0 -30 60 0 60 0 0 -30 0 -30 150 0 150 0 0 60 0
|
||||
60 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 -90 0 -90 0 0 90 0 90 30 0 c27 0 30 3
|
||||
30 30 l0 30 60 0 60 0 0 60 0 60 30 0 c27 0 30 3 30 30 l0 30 -60 0 -60 0 0
|
||||
30 0 30 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 l-30 0
|
||||
0 -90 0 -90 -120 0 -120 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30
|
||||
30 30 27 0 30 3 30 30 l0 30 -90 0 -90 0 0 30 0 30 60 0 60 0 0 30 0 30 60 0
|
||||
60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0
|
||||
120 0 120 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 -120 0 -120 0 0 30 c0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27
|
||||
0 30 3 30 30 0 27 3 30 30 30 l30 0 0 90 0 90 -30 0 -30 0 0 60 0 60 -30 0
|
||||
c-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 l0 30 60 0 60 0 0 30 c0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 30 c0 27 3 30 30 30
|
||||
27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0
|
||||
30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3
|
||||
30 30 l0 30 60 0 60 0 0 30 0 30 -90 0 -90 0 0 60 0 60 -30 0 -30 0 0 -60 0
|
||||
-60 -30 0 -30 0 0 60 0 60 30 0 c27 0 30 3 30 30 l0 30 150 0 150 0 0 90 0 90
|
||||
60 0 60 0 0 60 0 60 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30
|
||||
0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0 -90 0 -90 30 0
|
||||
30 0 0 90 0 90 30 0 30 0 0 -60 0 -60 30 0 30 0 0 150 0 150 -30 0 c-27 0 -30
|
||||
3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 60 0 60 0 0 -30 c0 -27 3 -30 30
|
||||
-30 l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3
|
||||
30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 60 0 60
|
||||
-30 0 c-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30
|
||||
-30 l0 -30 150 0 150 0 0 -60 0 -60 90 0 90 0 0 30 0 30 -60 0 -60 0 0 60 0
|
||||
60 30 0 c27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27
|
||||
0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 90 0 90 -30 0 c-27 0 -30 -3 -30
|
||||
-30 l0 -30 -90 0 -90 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30
|
||||
30 l-30 0 0 60 0 60 30 0 30 0 0 -60z m-120 -90 c0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m-960 -60 c0 -27 -3 -30 -30
|
||||
-30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m720 -90 l0 -60
|
||||
-30 0 -30 0 0 60 0 60 30 0 30 0 0 -60z m240 -30 l0 -90 -90 0 -90 0 0 90 0
|
||||
90 90 0 90 0 0 -90z m-840 -180 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0
|
||||
27 3 30 30 30 27 0 30 -3 30 -30z m-540 -240 c0 -27 -3 -30 -30 -30 -27 0 -30
|
||||
3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m2700 -900 l0 -90 -90 0 -90 0 0
|
||||
90 0 90 90 0 90 0 0 -90z m-240 -180 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0
|
||||
27 3 30 30 30 l30 0 0 -60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30
|
||||
-60 0 -60 0 0 60 0 60 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 -3 30 -30z
|
||||
m420 -30 l0 -60 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 l-30 0 0 -60 0 -60 -30
|
||||
0 -30 0 0 60 0 60 30 0 c27 0 30 3 30 30 l0 30 90 0 90 0 0 -60z m-1020 -390
|
||||
c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z
|
||||
m960 -420 l0 -30 -60 0 -60 0 0 30 0 30 60 0 60 0 0 -30z m-2940 -60 l0 -30
|
||||
-60 0 -60 0 0 30 0 30 60 0 60 0 0 -30z m1500 -240 l0 -90 -90 0 -90 0 0 90 0
|
||||
90 90 0 90 0 0 -90z m1020 60 c0 -27 3 -30 30 -30 l30 0 0 -60 0 -60 -30 0
|
||||
c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 30 0 c27 0 30 -3 30
|
||||
-30z m300 -60 l0 -90 -90 0 -90 0 0 90 0 90 90 0 90 0 0 -90z"/>
|
||||
<path d="M2880 3330 l0 -90 30 0 30 0 0 90 0 90 -30 0 -30 0 0 -90z"/>
|
||||
<path d="M2400 3300 l0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30
|
||||
-27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 -90 0 -90 30 0 30 0 0 -90 0
|
||||
-90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 -3 30 -30 l0 -30 90 0 90 0 0 -30 c0 -27 -3 -30 -30 -30 -27
|
||||
0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -120 0 -120 0
|
||||
0 60 0 60 -60 0 -60 0 0 -30 0 -30 -60 0 -60 0 0 30 c0 27 3 30 30 30 l30 0 0
|
||||
60 0 60 -30 0 -30 0 0 60 0 60 -60 0 -60 0 0 30 0 30 -90 0 -90 0 0 30 c0 27
|
||||
-3 30 -30 30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 -30 0 -30 -60 0 -60
|
||||
0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0
|
||||
-90 0 -90 -90 0 -90 0 0 -60 0 -60 -30 0 -30 0 0 60 0 60 -60 0 -60 0 0 30 c0
|
||||
27 3 30 30 30 27 0 30 3 30 30 l0 30 -60 0 -60 0 0 -60 0 -60 30 0 c27 0 30
|
||||
-3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0 30 c0
|
||||
27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30
|
||||
-30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 -30 0 -30
|
||||
60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30
|
||||
-3 30 -30 0 -27 3 -30 30 -30 l30 0 0 -60 0 -60 60 0 60 0 0 -30 0 -30 -90 0
|
||||
-90 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 l0 -30 -60 0 -60 0 0 -30 0 -30 -60 0 -60 0 0 -30 c0 -27 3
|
||||
-30 30 -30 l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 l0 -30 -90 0 -90 0 0
|
||||
-30 c0 -27 3 -30 30 -30 l30 0 0 -120 0 -120 -30 0 -30 0 0 -90 0 -90 -30 0
|
||||
c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 l30 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3
|
||||
30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30
|
||||
30 l0 30 90 0 90 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 3 -30 30 l0 30 60 0 60 0 0 -90 0 -90 -30 0 -30 0 0 -60 0 -60 90 0
|
||||
90 0 0 -30 0 -30 -60 0 -60 0 0 -30 0 -30 90 0 90 0 0 -60 0 -60 -30 0 c-27 0
|
||||
-30 -3 -30 -30 l0 -30 60 0 60 0 0 -30 0 -30 -90 0 -90 0 0 -60 0 -60 -60 0
|
||||
-60 0 0 -30 0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 l0 30 60
|
||||
0 60 0 0 -60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 3
|
||||
30 30 l0 30 60 0 60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0
|
||||
-27 -3 -30 -30 -30 l-30 0 0 -120 0 -120 -60 0 -60 0 0 -30 0 -30 60 0 60 0 0
|
||||
30 0 30 60 0 60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 l30 0 0 90 0 90 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 l0
|
||||
30 -60 0 -60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 l0 30 60 0 60 0 0 -90
|
||||
0 -90 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30
|
||||
30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -120 0
|
||||
-120 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0
|
||||
0 -90 0 -90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30
|
||||
-30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30
|
||||
30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 -30 0 c-27 0 -30 3
|
||||
-30 30 l0 30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 l0 30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 -3
|
||||
30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 3 -30 30 l0 30 60 0 60 0 0 30 0 30 -60 0 -60 0 0 -30 c0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 120 0 120
|
||||
0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 60
|
||||
0 60 30 0 30 0 0 90 0 90 -30 0 -30 0 0 90 0 90 30 0 c27 0 30 -3 30 -30 0
|
||||
-27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 l0 30 90 0
|
||||
90 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 3 30 30 l0 30 90 0 90 0 0 30 c0 27 3
|
||||
30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30
|
||||
-30 30 l-30 0 0 90 0 90 -30 0 -30 0 0 60 0 60 30 0 c27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27
|
||||
0 30 3 30 30 l0 30 -90 0 -90 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30
|
||||
0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3
|
||||
-30 30 l0 30 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30
|
||||
60 0 60 0 0 120 0 120 30 0 30 0 0 60 0 60 -30 0 -30 0 0 60 0 60 -60 0 -60 0
|
||||
0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 l0 30
|
||||
-60 0 -60 0 0 -30 c0 -27 -3 -30 -30 -30 l-30 0 0 90 0 90 30 0 c27 0 30 -3
|
||||
30 -30 0 -27 3 -30 30 -30 l30 0 0 60 0 60 -90 0 -90 0 0 -60z m0 -210 c0 -27
|
||||
3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3
|
||||
-30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3
|
||||
30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27
|
||||
-3 -30 -30 -30 l-30 0 0 60 0 60 -30 0 c-27 0 -30 3 -30 30 l0 30 60 0 60 0 0
|
||||
-30z m-600 -120 l0 -30 -60 0 -60 0 0 -60 0 -60 -30 0 -30 0 0 -60 0 -60 30 0
|
||||
c27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0 -30 0 -30 60 0 60 0 0 -30 c0 -27 3
|
||||
-30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 l0 30
|
||||
-60 0 -60 0 0 30 0 30 -120 0 -120 0 0 -30 0 -30 60 0 60 0 0 -30 c0 -27 3
|
||||
-30 30 -30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27
|
||||
0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30
|
||||
-3 30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 30 0 c27 0 30 3 30 30 0
|
||||
27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30
|
||||
-30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 30 0 30 0 0 90
|
||||
0 90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0
|
||||
27 3 30 30 30 l30 0 0 90 0 90 30 0 c27 0 30 3 30 30 l0 30 60 0 60 0 0 -30
|
||||
c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30
|
||||
-30 l0 -30 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 l30 0 0 -60 0 -60 30 0 30
|
||||
0 0 60 0 60 30 0 30 0 0 -60 0 -60 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0
|
||||
30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3
|
||||
30 30 l0 30 90 0 90 0 0 -30 0 -30 -90 0 -90 0 0 -30 0 -30 90 0 90 0 0 -60 0
|
||||
-60 -90 0 -90 0 0 -60 0 -60 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0
|
||||
-120 0 -120 60 0 60 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30
|
||||
l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 l-30
|
||||
0 0 60 0 60 30 0 30 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 -60 0
|
||||
-60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60 0 0 -30
|
||||
0 -30 -120 0 -120 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30
|
||||
-120 0 -120 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30
|
||||
-30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30
|
||||
30 l-30 0 0 -90 0 -90 -30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30
|
||||
3 -30 30 l0 30 -60 0 -60 0 0 90 0 90 30 0 c27 0 30 3 30 30 l0 30 60 0 60 0
|
||||
0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 90 0 90 -30 0
|
||||
-30 0 0 -60 0 -60 -60 0 -60 0 0 30 c0 27 3 30 30 30 l30 0 0 60 0 60 -30 0
|
||||
c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 3 30 30 l0 30 -60 0 -60 0 0 -30 0 -30 -60 0 -60 0 0 30 0 30 60 0
|
||||
60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30
|
||||
-3 -30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 -60 0 -60 0 0 -30 0 -30
|
||||
120 0 120 0 0 -90 0 -90 -30 0 -30 0 0 -90 0 -90 30 0 c27 0 30 -3 30 -30 0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 l30 0 0 -60 0
|
||||
-60 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30
|
||||
-30 -30 -27 0 -30 3 -30 30 l0 30 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0
|
||||
30 3 30 30 0 27 -3 30 -30 30 l-30 0 0 90 0 90 -90 0 -90 0 0 30 0 30 90 0 90
|
||||
0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 30 0 30 60 0
|
||||
60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3
|
||||
-30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 30 0
|
||||
30 60 0 60 0 0 30 0 30 -60 0 -60 0 0 30 0 30 60 0 60 0 0 30 c0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 l30 0 0 90 0
|
||||
90 -60 0 -60 0 0 30 0 30 60 0 60 0 0 30 0 30 60 0 60 0 0 -30 c0 -27 3 -30
|
||||
30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0
|
||||
30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3
|
||||
30 30 l0 30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30
|
||||
30 -30 l30 0 0 60 0 60 90 0 90 0 0 -30z m-540 -240 c0 -27 -3 -30 -30 -30
|
||||
-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m-360 -60 c0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m1380 -60
|
||||
c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z
|
||||
m300 -360 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30
|
||||
-3 30 -30z m-1740 -60 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30z m600 -60 l0 -90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 l30 0 0 90 0 90 30 0 30 0 0
|
||||
-90z m-780 0 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0
|
||||
30 -3 30 -30z m60 -180 l0 -90 -90 0 -90 0 0 90 0 90 90 0 90 0 0 -90z m-60
|
||||
-180 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 90 0 90 0 0 -30z
|
||||
m780 -180 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27
|
||||
0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30
|
||||
l0 30 120 0 120 0 0 -60 0 -60 30 0 30 0 0 90 0 90 60 0 60 0 0 -90 0 -90 -60
|
||||
0 -60 0 0 -60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0
|
||||
-30 -3 -30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 -3 30 -30 0
|
||||
-27 3 -30 30 -30 l30 0 0 -60 0 -60 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 60 0
|
||||
60 0 0 -30 0 -30 -60 0 -60 0 0 30 c0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30
|
||||
l0 -30 -60 0 -60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 l0 30 -60 0 -60 0
|
||||
0 30 c0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3
|
||||
-30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3
|
||||
-30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30
|
||||
0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 -3 30 -30z m660 -60 l0 -90 30 0 c27 0 30 -3 30 -30 0 -27
|
||||
-3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27
|
||||
-3 30 -30 30 l-30 0 0 60 0 60 60 0 60 0 0 -90z m-960 0 c0 -27 -3 -30 -30
|
||||
-30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m1080 0 c0 -27 -3
|
||||
-30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z m-960 -60
|
||||
c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30
|
||||
30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z
|
||||
m-60 -150 l0 -60 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 l30 0 0 -60 0 -60
|
||||
-30 0 c-27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0 30 c0 27 3 30 30 30 27 0
|
||||
30 3 30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 30 0 30 0 0 -60z m480 -750 l0
|
||||
-30 -60 0 -60 0 0 30 0 30 60 0 60 0 0 -30z"/>
|
||||
<path d="M1500 2850 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 90 0 90 -30 0 c-27 0
|
||||
-30 -3 -30 -30z"/>
|
||||
<path d="M1860 2850 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1860 2460 l0 -60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0
|
||||
30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 90 0 90 0 0 30 0
|
||||
30 -60 0 -60 0 0 30 0 30 90 0 90 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 -3 -30 -30 l0 -30 -90 0 -90 0 0 60 0 60 -30 0 -30
|
||||
0 0 -90 0 -90 -30 0 c-27 0 -30 3 -30 30 0 27 -3 30 -30 30 l-30 0 0 60 0 60
|
||||
-30 0 -30 0 0 -60z"/>
|
||||
<path d="M1080 2190 c0 -27 -3 -30 -30 -30 l-30 0 0 -90 0 -90 -30 0 c-27 0
|
||||
-30 -3 -30 -30 l0 -30 60 0 60 0 0 -30 c0 -27 -3 -30 -30 -30 l-30 0 0 -60 0
|
||||
-60 30 0 c27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30
|
||||
3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30
|
||||
30 0 27 -3 30 -30 30 l-30 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3
|
||||
-30 -30 -30 l-30 0 0 90 0 90 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27
|
||||
0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 -3
|
||||
-30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30
|
||||
-3 -30 -30z m120 -300 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30z m0 -120 l0 -30 -60 0 -60 0 0 30 0 30 60 0 60 0 0 -30z"/>
|
||||
<path d="M1860 2190 c0 -27 -3 -30 -30 -30 l-30 0 0 -90 0 -90 -30 0 c-27 0
|
||||
-30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 l0 -30 -60 0 -60 0 0
|
||||
-60 0 -60 60 0 60 0 0 60 0 60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27
|
||||
0 30 -3 30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 30 0 c27 0 30 3 30
|
||||
30 l0 30 60 0 60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 120 0 120 30 0 30 0 0 90 0
|
||||
90 -30 0 c-27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27
|
||||
-3 30 -30 30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 l-30 0 0 60 0 60 -30
|
||||
0 c-27 0 -30 -3 -30 -30z m180 -240 l0 -90 -90 0 -90 0 0 90 0 90 90 0 90 0 0
|
||||
-90z"/>
|
||||
<path d="M1920 1950 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2220 2040 l0 -120 -30 0 c-27 0 -30 -3 -30 -30 l0 -30 60 0 60 0 0
|
||||
30 c0 27 3 30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30 60 0 60 0 0 30 0 30 -60 0 -60 0 0
|
||||
30 c0 27 3 30 30 30 27 0 30 3 30 30 l0 30 -90 0 -90 0 0 -120z m120 -30 c0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z"/>
|
||||
<path d="M1440 1860 l0 -60 30 0 30 0 0 60 0 60 -30 0 -30 0 0 -60z"/>
|
||||
<path d="M600 1950 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1560 1410 l0 -30 -60 0 -60 0 0 -30 c0 -27 3 -30 30 -30 27 0 30 -3
|
||||
30 -30 l0 -30 90 0 90 0 0 30 c0 27 3 30 30 30 l30 0 0 60 0 60 -30 0 c-27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1800 1170 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0
|
||||
60 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1200 3270 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2640 3270 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1320 3090 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1440 3090 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2700 2970 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3060 2580 l0 -60 -30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30
|
||||
l30 0 0 -60 0 -60 30 0 c27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 60 0
|
||||
60 60 0 60 0 0 30 0 30 -60 0 -60 0 0 90 0 90 -60 0 -60 0 0 -60z m60 -90 c0
|
||||
-27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30
|
||||
0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z"/>
|
||||
<path d="M2940 2340 l0 -60 30 0 30 0 0 60 0 60 -30 0 -30 0 0 -60z"/>
|
||||
<path d="M2820 2130 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2700 1470 c0 -27 -3 -30 -30 -30 l-30 0 0 -60 0 -60 30 0 c27 0 30
|
||||
-3 30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30
|
||||
30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0
|
||||
30 -60 0 -60 0 0 -30z m60 -60 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27
|
||||
3 30 30 30 27 0 30 -3 30 -30z"/>
|
||||
<path d="M600 1230 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3060 1230 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 l0 -30 -60 0 -60
|
||||
0 0 -60 0 -60 30 0 c27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3
|
||||
30 30 30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 l30 0 0 60 0 60 30 0 c27 0 30
|
||||
3 30 30 l0 30 -120 0 -120 0 0 -30z"/>
|
||||
<path d="M2400 1170 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2700 1140 l0 -60 60 0 60 0 0 -30 c0 -27 -3 -30 -30 -30 -27 0 -30
|
||||
-3 -30 -30 0 -27 3 -30 30 -30 l30 0 0 -60 0 -60 -30 0 c-27 0 -30 3 -30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 -3 -30 -30 0
|
||||
-27 -3 -30 -30 -30 l-30 0 0 -90 0 -90 60 0 60 0 0 30 0 30 60 0 60 0 0 -60 0
|
||||
-60 60 0 60 0 0 30 0 30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30 0
|
||||
27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0
|
||||
27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 l0 30
|
||||
-90 0 -90 0 0 -60z m-120 -150 c0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27
|
||||
3 30 30 30 27 0 30 -3 30 -30z m300 0 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30
|
||||
0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30
|
||||
30 0 27 3 30 30 30 27 0 30 -3 30 -30z"/>
|
||||
<path d="M2340 990 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3120 930 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30
|
||||
l30 0 0 60 0 60 -60 0 -60 0 0 -30z"/>
|
||||
<path d="M1320 870 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M2640 690 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30
|
||||
27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30 -27
|
||||
0 -30 -3 -30 -30z"/>
|
||||
<path d="M1920 3270 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3240 1950 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3060 1650 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1920 630 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3240 630 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M1320 3630 c0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 -3 30 -30 0 -27 3 -30 30 -30 27 0 30 -3 30 -30 0 -27 3 -30 30
|
||||
-30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 3 -30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 -3 30 -30 30 -27 0
|
||||
-30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3 -30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M3240 3450 l0 -210 210 0 210 0 0 210 0 210 -210 0 -210 0 0 -210z
|
||||
m360 0 l0 -150 -150 0 -150 0 0 150 0 150 150 0 150 0 0 -150z"/>
|
||||
<path d="M3360 3450 l0 -90 90 0 90 0 0 90 0 90 -90 0 -90 0 0 -90z"/>
|
||||
<path d="M3540 2970 l0 -30 60 0 60 0 0 30 0 30 -60 0 -60 0 0 -30z"/>
|
||||
<path d="M3600 1530 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 l0 -30 60 0
|
||||
60 0 0 60 0 60 -30 0 c-27 0 -30 -3 -30 -30z"/>
|
||||
<path d="M240 450 l0 -210 210 0 210 0 0 210 0 210 -210 0 -210 0 0 -210z
|
||||
m360 0 l0 -150 -150 0 -150 0 0 150 0 150 150 0 150 0 0 -150z"/>
|
||||
<path d="M360 450 l0 -90 90 0 90 0 0 90 0 90 -90 0 -90 0 0 -90z"/>
|
||||
<path d="M3540 570 l0 -30 60 0 60 0 0 30 0 30 -60 0 -60 0 0 -30z"/>
|
||||
<path d="M2280 510 l0 -30 -60 0 -60 0 0 -30 0 -30 -60 0 -60 0 0 -60 0 -60
|
||||
-30 0 c-27 0 -30 -3 -30 -30 0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 3 30 30
|
||||
30 27 0 30 -3 30 -30 l0 -30 60 0 60 0 0 30 c0 27 3 30 30 30 27 0 30 3 30 30
|
||||
0 27 3 30 30 30 l30 0 0 90 0 90 -30 0 c-27 0 -30 -3 -30 -30z m-60 -120 c0
|
||||
-27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 -3 -30 -30 -30 -27 0 -30 3
|
||||
-30 30 0 27 3 30 30 30 27 0 30 3 30 30 0 27 3 30 30 30 27 0 30 -3 30 -30z"/>
|
||||
<path d="M1260 300 l0 -60 30 0 30 0 0 60 0 60 -30 0 -30 0 0 -60z"/>
|
||||
<path d="M2400 330 c0 -27 -3 -30 -30 -30 -27 0 -30 -3 -30 -30 0 -27 3 -30
|
||||
30 -30 27 0 30 3 30 30 l0 30 90 0 90 0 0 30 0 30 -90 0 -90 0 0 -30z"/>
|
||||
<path d="M3600 270 c0 -27 3 -30 30 -30 27 0 30 3 30 30 0 27 -3 30 -30 30
|
||||
-27 0 -30 -3 -30 -30z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 38 KiB |
|
|
@ -38,6 +38,12 @@ post_install do |installer|
|
|||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
# ensure all dependencies are using SQLCipher instead of SQLite
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
xcconfig = File.read(xcconfig_path)
|
||||
new_xcconfig = xcconfig.sub(' -l"sqlite3"', '')
|
||||
File.open(xcconfig_path, "w") { |file| file << new_xcconfig }
|
||||
|
||||
config.build_settings['ENABLE_BITCODE'] = 'NO'
|
||||
|
||||
# see https://github.com/flutter-webrtc/flutter-webrtc/issues/1054
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1240;
|
||||
LastUpgradeCheck = 1430;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ abstract class AppConfig {
|
|||
static bool sendOnEnter = true;
|
||||
// static bool sendOnEnter = false;
|
||||
//Pangea#
|
||||
static bool sendPublicReadReceipts = true;
|
||||
static bool showPresences = true;
|
||||
static bool experimentalVoip = false;
|
||||
static const bool hideTypingUsernames = false;
|
||||
static const bool hideAllStateEvents = false;
|
||||
|
|
@ -104,7 +106,7 @@ abstract class AppConfig {
|
|||
static const String emojiFontName = 'Noto Emoji';
|
||||
static const String emojiFontUrl =
|
||||
'https://github.com/googlefonts/noto-emoji/';
|
||||
static const double borderRadius = 16.0;
|
||||
static const double borderRadius = 18.0;
|
||||
static const double columnWidth = 360.0;
|
||||
static final Uri homeserverList = Uri(
|
||||
scheme: 'https',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
|||
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
|
||||
import 'package:fluffychat/widgets/log_view.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../pangea/pages/analytics/class_analytics/class_analytics.dart';
|
||||
|
|
@ -77,6 +77,7 @@ abstract class AppRoutes {
|
|||
path: '/home',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const HomeserverPicker(),
|
||||
),
|
||||
redirect: loggedInRedirect,
|
||||
|
|
@ -85,6 +86,7 @@ abstract class AppRoutes {
|
|||
path: 'login',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const Login(),
|
||||
),
|
||||
redirect: loggedInRedirect,
|
||||
|
|
@ -94,6 +96,7 @@ abstract class AppRoutes {
|
|||
path: 'signup',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SignupPage(),
|
||||
),
|
||||
redirect: loggedInRedirect,
|
||||
|
|
@ -105,6 +108,7 @@ abstract class AppRoutes {
|
|||
path: '/logs',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const LogViewer(),
|
||||
),
|
||||
),
|
||||
|
|
@ -113,6 +117,7 @@ abstract class AppRoutes {
|
|||
path: '/user_age',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const PUserAge(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -121,6 +126,7 @@ abstract class AppRoutes {
|
|||
ShellRoute(
|
||||
pageBuilder: (context, state, child) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
FluffyThemes.isColumnMode(context) &&
|
||||
state.fullPath?.startsWith('/rooms/settings') == false
|
||||
? TwoColumnLayout(
|
||||
|
|
@ -141,6 +147,7 @@ abstract class AppRoutes {
|
|||
path: '/spaces/:roomid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatDetails(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -151,6 +158,7 @@ abstract class AppRoutes {
|
|||
path: '/join_with_link',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const JoinClassWithLink(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -161,6 +169,7 @@ abstract class AppRoutes {
|
|||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? const EmptyPage()
|
||||
: ChatList(
|
||||
|
|
@ -173,6 +182,7 @@ abstract class AppRoutes {
|
|||
path: 'mylearning',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -181,6 +191,7 @@ abstract class AppRoutes {
|
|||
path: 'analytics',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AnalyticsClassList(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -190,6 +201,7 @@ abstract class AppRoutes {
|
|||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ClassAnalyticsPage(),
|
||||
),
|
||||
),
|
||||
|
|
@ -200,6 +212,7 @@ abstract class AppRoutes {
|
|||
path: 'archive',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const Archive(),
|
||||
),
|
||||
routes: [
|
||||
|
|
@ -207,6 +220,7 @@ abstract class AppRoutes {
|
|||
path: ':roomid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -220,6 +234,7 @@ abstract class AppRoutes {
|
|||
path: 'newprivatechat',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewPrivateChat(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -231,6 +246,7 @@ abstract class AppRoutes {
|
|||
// Pangea#
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewGroup(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -239,6 +255,7 @@ abstract class AppRoutes {
|
|||
path: ':spaceid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewGroup(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -249,6 +266,7 @@ abstract class AppRoutes {
|
|||
path: 'newspace',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewSpace(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -258,6 +276,7 @@ abstract class AppRoutes {
|
|||
path: 'newspace/:newexchange',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const NewSpace(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -266,6 +285,7 @@ abstract class AppRoutes {
|
|||
path: 'join_exchange/:exchangeid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AddExchangeToClass(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -274,6 +294,7 @@ abstract class AppRoutes {
|
|||
path: 'partner',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const FindPartner(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -282,6 +303,7 @@ abstract class AppRoutes {
|
|||
ShellRoute(
|
||||
pageBuilder: (context, state, child) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? TwoColumnLayout(
|
||||
mainView: const Settings(),
|
||||
|
|
@ -295,6 +317,7 @@ abstract class AppRoutes {
|
|||
path: 'settings',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? const EmptyPage()
|
||||
: const Settings(),
|
||||
|
|
@ -304,6 +327,7 @@ abstract class AppRoutes {
|
|||
path: 'notifications',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsNotifications(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -312,6 +336,7 @@ abstract class AppRoutes {
|
|||
path: 'style',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsStyle(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -320,6 +345,7 @@ abstract class AppRoutes {
|
|||
path: 'devices',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const DevicesSettings(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -328,6 +354,7 @@ abstract class AppRoutes {
|
|||
path: 'chat',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsChat(),
|
||||
),
|
||||
routes: [
|
||||
|
|
@ -335,6 +362,7 @@ abstract class AppRoutes {
|
|||
path: 'emotes',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const EmotesSettings(),
|
||||
),
|
||||
),
|
||||
|
|
@ -366,6 +394,7 @@ abstract class AppRoutes {
|
|||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsSecurity(),
|
||||
),
|
||||
routes: [
|
||||
|
|
@ -374,6 +403,7 @@ abstract class AppRoutes {
|
|||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsPassword(),
|
||||
);
|
||||
},
|
||||
|
|
@ -384,6 +414,7 @@ abstract class AppRoutes {
|
|||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
SettingsIgnoreList(
|
||||
initialUserId: state.extra?.toString(),
|
||||
),
|
||||
|
|
@ -395,6 +426,7 @@ abstract class AppRoutes {
|
|||
path: '3pid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const Settings3Pid(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -406,6 +438,7 @@ abstract class AppRoutes {
|
|||
path: 'learning',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SettingsLearning(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -414,6 +447,7 @@ abstract class AppRoutes {
|
|||
path: 'subscription',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SubscriptionManagement(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -428,6 +462,7 @@ abstract class AppRoutes {
|
|||
path: ':roomid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
shareText: state.uri.queryParameters['body'],
|
||||
|
|
@ -440,6 +475,7 @@ abstract class AppRoutes {
|
|||
// path: 'encryption',
|
||||
// pageBuilder: (context, state) => defaultPageBuilder(
|
||||
// context,
|
||||
// state,
|
||||
// const ChatEncryptionSettings(),
|
||||
// ),
|
||||
// redirect: loggedOutRedirect,
|
||||
|
|
@ -449,6 +485,7 @@ abstract class AppRoutes {
|
|||
path: 'invite',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
InvitationSelection(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -459,6 +496,7 @@ abstract class AppRoutes {
|
|||
path: 'details',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatDetails(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -468,6 +506,7 @@ abstract class AppRoutes {
|
|||
path: 'members',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatMembersPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -478,6 +517,7 @@ abstract class AppRoutes {
|
|||
path: 'permissions',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ChatPermissionsSettings(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -487,6 +527,7 @@ abstract class AppRoutes {
|
|||
path: 'class_settings',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const ClassSettingsPage(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -496,6 +537,7 @@ abstract class AppRoutes {
|
|||
path: 'invite',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
InvitationSelection(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
),
|
||||
|
|
@ -506,6 +548,7 @@ abstract class AppRoutes {
|
|||
path: 'multiple_emotes',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const MultipleEmotesSettings(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -514,6 +557,7 @@ abstract class AppRoutes {
|
|||
path: 'emotes',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const EmotesSettings(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -522,6 +566,7 @@ abstract class AppRoutes {
|
|||
path: 'emotes/:state_key',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const EmotesSettings(),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
|
|
@ -537,17 +582,23 @@ abstract class AppRoutes {
|
|||
),
|
||||
];
|
||||
|
||||
static Page defaultPageBuilder(BuildContext context, Widget child) =>
|
||||
CustomTransitionPage(
|
||||
child: child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? FadeTransition(opacity: animation, child: child)
|
||||
: CupertinoPageTransition(
|
||||
primaryRouteAnimation: animation,
|
||||
secondaryRouteAnimation: secondaryAnimation,
|
||||
linearTransition: false,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
static Page defaultPageBuilder(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
Widget child,
|
||||
) =>
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? CustomTransitionPage(
|
||||
key: state.pageKey,
|
||||
restorationId: state.pageKey.value,
|
||||
child: child,
|
||||
transitionsBuilder:
|
||||
(context, animation, secondaryAnimation, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
)
|
||||
: MaterialPage(
|
||||
key: state.pageKey,
|
||||
restorationId: state.pageKey.value,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,17 @@ abstract class SettingKeys {
|
|||
static const String unifiedPushRegistered =
|
||||
'chat.fluffy.unifiedpush.registered';
|
||||
static const String unifiedPushEndpoint = 'chat.fluffy.unifiedpush.endpoint';
|
||||
static const String notificationCurrentIds = 'chat.fluffy.notification_ids';
|
||||
static const String ownStatusMessage = 'chat.fluffy.status_msg';
|
||||
static const String dontAskForBootstrapKey =
|
||||
'chat.fluffychat.dont_ask_bootstrap';
|
||||
static const String autoplayImages = 'chat.fluffy.autoplay_images';
|
||||
static const String sendTypingNotifications =
|
||||
'chat.fluffy.send_typing_notifications';
|
||||
static const String sendPublicReadReceipts =
|
||||
'chat.fluffy.send_public_read_receipts';
|
||||
static const String sendOnEnter = 'chat.fluffy.send_on_enter';
|
||||
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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ abstract class FluffyThemes {
|
|||
useMaterial3: true,
|
||||
brightness: brightness,
|
||||
colorScheme: colorScheme,
|
||||
textTheme: PlatformInfos.isDesktop || PlatformInfos.isWeb
|
||||
textTheme: PlatformInfos.isDesktop
|
||||
? brightness == Brightness.light
|
||||
? Typography.material2018().black.merge(fallbackTextTheme)
|
||||
: Typography.material2018().white.merge(fallbackTextTheme)
|
||||
|
|
@ -89,17 +89,24 @@ abstract class FluffyThemes {
|
|||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
selectionColor: colorScheme.onBackground.withAlpha(128),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: UnderlineInputBorder(
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
filled: true,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56,
|
||||
shadowColor: Colors.grey.withAlpha(64),
|
||||
surfaceTintColor: colorScheme.background,
|
||||
shadowColor: FluffyThemes.isColumnMode(context)
|
||||
? Colors.grey.withAlpha(64)
|
||||
: null,
|
||||
surfaceTintColor:
|
||||
FluffyThemes.isColumnMode(context) ? colorScheme.background : null,
|
||||
systemOverlayStyle: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: brightness.reversed,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ void main() async {
|
|||
AppLifecycleState.detached == WidgetsBinding.instance.lifecycleState) {
|
||||
// Do not send online presences when app is in background fetch mode.
|
||||
for (final client in clients) {
|
||||
client.backgroundSync = false;
|
||||
client.syncPresence = PresenceType.offline;
|
||||
}
|
||||
|
||||
|
|
@ -121,6 +122,7 @@ class AppStarter with WidgetsBindingObserver {
|
|||
);
|
||||
// Switching to foreground mode needs to reenable send online sync presence.
|
||||
for (final client in clients) {
|
||||
client.backgroundSync = true;
|
||||
client.syncPresence = PresenceType.online;
|
||||
}
|
||||
startGui(clients, store);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,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/pages/archive/archive.dart';
|
||||
|
|
@ -59,6 +60,8 @@ class ArchiveView extends StatelessWidget {
|
|||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||
controller.archive[i],
|
||||
onForget: () => controller.forgetRoomAction(i),
|
||||
onTap: () => context
|
||||
.go('/rooms/archive/${controller.archive[i].id}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
),
|
||||
hintText: L10n.of(context)!.recoveryKey,
|
||||
errorText: _recoveryKeyInputError,
|
||||
errorMaxLines: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -290,6 +291,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
final key = _recoveryKeyTextEditingController
|
||||
.text
|
||||
.trim();
|
||||
if (key.isEmpty) return;
|
||||
await bootstrap.newSsssKey!.unlock(
|
||||
keyOrPassphrase: key,
|
||||
);
|
||||
|
|
@ -317,6 +319,11 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
() => _recoveryKeyInputError =
|
||||
e.toLocalizedString(context),
|
||||
);
|
||||
} on FormatException catch (_) {
|
||||
setState(
|
||||
() => _recoveryKeyInputError =
|
||||
L10n.of(context)!.wrongRecoveryKey,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorReporter(
|
||||
context,
|
||||
|
|
@ -351,6 +358,16 @@ class BootstrapDialogState extends State<BootstrapDialog> {
|
|||
onPressed: _recoveryKeyInputLoading
|
||||
? null
|
||||
: () async {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.verifyOtherDevice,
|
||||
message: L10n.of(context)!
|
||||
.verifyOtherDeviceDescription,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
fullyCapitalizedForMaterial: false,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
final req = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
|||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_view.dart';
|
||||
import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
||||
|
|
@ -28,7 +29,6 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
|||
import 'package:fluffychat/pangea/utils/report_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/error_reporter.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart';
|
||||
|
|
@ -47,13 +47,13 @@ import 'package:image_picker/image_picker.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import '../../utils/account_bundles.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'send_file_dialog.dart';
|
||||
import 'send_location_dialog.dart';
|
||||
import 'sticker_picker_dialog.dart';
|
||||
|
||||
class ChatPage extends StatelessWidget {
|
||||
final String roomId;
|
||||
|
|
@ -81,31 +81,10 @@ class ChatPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ChatPageWithRoom(
|
||||
key: Key('chat_page_$roomId'),
|
||||
room: room,
|
||||
shareText: shareText,
|
||||
),
|
||||
),
|
||||
if (FluffyThemes.isThreeColumnMode(context) &&
|
||||
room.membership == Membership.join)
|
||||
Container(
|
||||
width: FluffyThemes.columnWidth,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ChatDetails(roomId: roomId),
|
||||
),
|
||||
],
|
||||
return ChatPageWithRoom(
|
||||
key: Key('chat_page_$roomId'),
|
||||
room: room,
|
||||
shareText: shareText,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +105,7 @@ class ChatPageWithRoom extends StatefulWidget {
|
|||
|
||||
class ChatController extends State<ChatPageWithRoom>
|
||||
with WidgetsBindingObserver {
|
||||
// #Pangea
|
||||
// #Pangea
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
late Choreographer choreographer = Choreographer(pangeaController, this);
|
||||
// Pangea#
|
||||
|
|
@ -143,6 +122,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
final AutoScrollController scrollController = AutoScrollController();
|
||||
|
||||
FocusNode inputFocus = FocusNode();
|
||||
StreamSubscription<html.Event>? onFocusSub;
|
||||
|
||||
Timer? typingCoolDown;
|
||||
Timer? typingTimeout;
|
||||
|
|
@ -156,26 +136,32 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
// void onDragDone(DropDoneDetails details) async {
|
||||
// setState(() => dragging = false);
|
||||
// final bytesList = await showFutureLoadingDialog(
|
||||
// if (details.files.isEmpty) return;
|
||||
// final result = await showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () => Future.wait(
|
||||
// details.files.map(
|
||||
// (xfile) => xfile.readAsBytes(),
|
||||
// ),
|
||||
// ),
|
||||
// future: () async {
|
||||
// final clientConfig = await room.client.getConfig();
|
||||
// final maxUploadSize = clientConfig.mUploadSize ?? 100 * 1024 * 1024;
|
||||
// final matrixFiles = await Future.wait(
|
||||
// details.files.map(
|
||||
// (xfile) async {
|
||||
// final length = await xfile.length();
|
||||
// if (length > maxUploadSize) {
|
||||
// throw FileTooBigMatrixException(length, maxUploadSize);
|
||||
// }
|
||||
// return MatrixFile(
|
||||
// bytes: await xfile.readAsBytes(),
|
||||
// name: xfile.name,
|
||||
// mimeType: xfile.mimeType,
|
||||
// ).detectFileType;
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// return matrixFiles;
|
||||
// },
|
||||
// );
|
||||
// if (bytesList.error != null) return;
|
||||
|
||||
// final matrixFiles = <MatrixFile>[];
|
||||
// for (var i = 0; i < bytesList.result!.length; i++) {
|
||||
// matrixFiles.add(
|
||||
// MatrixFile(
|
||||
// bytes: bytesList.result![i],
|
||||
// name: details.files[i].name,
|
||||
// ).detectFileType,
|
||||
// );
|
||||
// }
|
||||
// if (matrixFiles.isEmpty) return;
|
||||
// final matrixFiles = result.result;
|
||||
// if (matrixFiles == null || matrixFiles.isEmpty) return;
|
||||
|
||||
// await showAdaptiveDialog(
|
||||
// context: context,
|
||||
|
|
@ -186,7 +172,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// );
|
||||
// }
|
||||
// Pangea#
|
||||
|
||||
bool get canSaveSelectedEvent =>
|
||||
selectedEvents.length == 1 &&
|
||||
{
|
||||
|
|
@ -246,7 +231,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
EmojiPickerType emojiPickerType = EmojiPickerType.keyboard;
|
||||
|
||||
// #Pangea
|
||||
// void requestHistory() async {
|
||||
// void requestHistory([_]) async {
|
||||
Future<void> requestHistory() async {
|
||||
if (timeline == null) return;
|
||||
// Pangea#
|
||||
|
|
@ -275,6 +260,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
setState(() => _scrolledUp = true);
|
||||
} else if (scrollController.position.pixels <= 0 && _scrolledUp == true) {
|
||||
setState(() => _scrolledUp = false);
|
||||
setReadMarker();
|
||||
}
|
||||
|
||||
if (scrollController.position.pixels == 0 ||
|
||||
|
|
@ -301,7 +287,12 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
inputFocus.addListener(_inputFocusListener);
|
||||
_loadDraft();
|
||||
super.initState();
|
||||
_displayChatDetailsColumn = ValueNotifier(
|
||||
Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false,
|
||||
);
|
||||
sendingClient = Matrix.of(context).client;
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
// #Pangea
|
||||
if (!mounted) return;
|
||||
Future.delayed(const Duration(seconds: 1), () async {
|
||||
|
|
@ -344,6 +335,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
// Pangea#
|
||||
_tryLoadTimeline();
|
||||
if (kIsWeb) {
|
||||
onFocusSub = html.window.onFocus.listen((_) => setReadMarker());
|
||||
}
|
||||
}
|
||||
|
||||
void _tryLoadTimeline() async {
|
||||
|
|
@ -351,7 +345,10 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
try {
|
||||
await loadTimelineFuture;
|
||||
final fullyRead = room.fullyRead;
|
||||
if (fullyRead.isEmpty) return;
|
||||
if (fullyRead.isEmpty) {
|
||||
setReadMarker();
|
||||
return;
|
||||
}
|
||||
if (timeline!.events.any((event) => event.eventId == fullyRead)) {
|
||||
Logs().v('Scroll up to visible event', fullyRead);
|
||||
setReadMarker();
|
||||
|
|
@ -385,6 +382,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
int? animateInEventIndex;
|
||||
|
||||
void onInsert(int i) {
|
||||
if (timeline?.events[i].status == EventStatus.synced) {
|
||||
final index = timeline!.events.firstIndexWhereNotError;
|
||||
if (i == index) setReadMarker(eventId: timeline?.events[i].eventId);
|
||||
}
|
||||
|
||||
// setState will be called by updateView() anyway
|
||||
animateInEventIndex = i;
|
||||
}
|
||||
|
|
@ -463,7 +465,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
if (!_scrolledUp) return;
|
||||
setReadMarker();
|
||||
}
|
||||
|
||||
|
|
@ -471,20 +472,32 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void setReadMarker({String? eventId}) {
|
||||
if (_setReadMarkerFuture != null) return;
|
||||
if (_scrolledUp) return;
|
||||
if (scrollUpBannerEventId != null) return;
|
||||
if (eventId == null &&
|
||||
!room.hasNewMessages &&
|
||||
room.notificationCount == 0) {
|
||||
return;
|
||||
}
|
||||
if (!Matrix.of(context).webHasFocus) return;
|
||||
|
||||
// Do not send read markers when app is not in foreground
|
||||
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
|
||||
if (!kIsWeb &&
|
||||
WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final timeline = this.timeline;
|
||||
if (timeline == null || timeline.events.isEmpty) return;
|
||||
|
||||
Logs().d('Set read marker...', eventId);
|
||||
// ignore: unawaited_futures
|
||||
_setReadMarkerFuture = timeline.setReadMarker(eventId: eventId).then((_) {
|
||||
_setReadMarkerFuture = timeline
|
||||
.setReadMarker(
|
||||
eventId: eventId,
|
||||
public: AppConfig.sendPublicReadReceipts,
|
||||
)
|
||||
.then((_) {
|
||||
_setReadMarkerFuture = null;
|
||||
});
|
||||
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
|
||||
|
|
@ -497,6 +510,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
timeline?.cancelSubscriptions();
|
||||
timeline = null;
|
||||
inputFocus.removeListener(_inputFocusListener);
|
||||
onFocusSub?.cancel();
|
||||
//#Pangea
|
||||
choreographer.stateListener.close();
|
||||
choreographer.dispose();
|
||||
|
|
@ -536,6 +550,12 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
|
||||
// #Pangea
|
||||
final List<String> edittingEvents = [];
|
||||
void clearEdittingEvent(String eventId) {
|
||||
edittingEvents.remove(eventId);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// Future<void> send() async {
|
||||
// Original send function gets the tx id within the matrix lib,
|
||||
// but for choero, the tx id is generated before the message send.
|
||||
|
|
@ -578,6 +598,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// editEventId: editEvent?.eventId,
|
||||
// parseCommands: parseCommands,
|
||||
// );
|
||||
final previousEdit = editEvent;
|
||||
room
|
||||
.pangeaSendTextEvent(
|
||||
sendController.text,
|
||||
|
|
@ -593,6 +614,13 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
)
|
||||
.then(
|
||||
(String? msgEventId) {
|
||||
// #Pangea
|
||||
setState(() {
|
||||
if (previousEdit != null) {
|
||||
edittingEvents.add(previousEdit.eventId);
|
||||
}
|
||||
});
|
||||
// Pangea#
|
||||
GoogleAnalytics.sendMessage(
|
||||
room.id,
|
||||
room.classCode,
|
||||
|
|
@ -615,6 +643,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
useType: useType ?? UseType.un,
|
||||
time: DateTime.now(),
|
||||
),
|
||||
isEdit: previousEdit != null,
|
||||
);
|
||||
|
||||
if (choreo != null &&
|
||||
|
|
@ -628,6 +657,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
...choreo.toGrammarConstructUse(msgEventId, room.id),
|
||||
],
|
||||
originalSent!.langCode,
|
||||
isEdit: previousEdit != null,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -756,24 +786,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
}
|
||||
|
||||
void sendStickerAction() async {
|
||||
final sticker = await showAdaptiveBottomSheet<ImagePackImageContent>(
|
||||
context: context,
|
||||
builder: (c) => StickerPickerDialog(room: room),
|
||||
);
|
||||
if (sticker == null) return;
|
||||
final eventContent = <String, dynamic>{
|
||||
'body': sticker.body,
|
||||
if (sticker.info != null) 'info': sticker.info,
|
||||
'url': sticker.url.toString(),
|
||||
};
|
||||
// send the sticker
|
||||
await room.sendEvent(
|
||||
eventContent,
|
||||
type: EventTypes.Sticker,
|
||||
);
|
||||
}
|
||||
|
||||
void voiceMessageAction() async {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
if (PlatformInfos.isAndroid) {
|
||||
|
|
@ -792,7 +804,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// #Pangea
|
||||
// if (await Record().hasPermission() == false) return;
|
||||
// Pangea#
|
||||
|
||||
final result = await showDialog<RecordingResult>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
|
@ -833,12 +844,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
}
|
||||
|
||||
void hideEmojiPicker() {
|
||||
setState(() => showEmojiPicker = false);
|
||||
}
|
||||
|
||||
void emojiPickerAction() {
|
||||
// #Pangea
|
||||
if (choreographer.itController.isOpen) {
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
if (showEmojiPicker) {
|
||||
inputFocus.requestFocus();
|
||||
} else {
|
||||
|
|
@ -1138,7 +1148,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
});
|
||||
await loadTimelineFuture;
|
||||
setReadMarker();
|
||||
}
|
||||
scrollController.jumpTo(0);
|
||||
}
|
||||
|
|
@ -1220,7 +1229,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
void clearSelectedEvents() => setState(() {
|
||||
selectedEvents.clear();
|
||||
showEmojiPicker = false;
|
||||
//#Pangea
|
||||
//#Pangea
|
||||
choreographer.messageOptions.resetSelectedDisplayLang();
|
||||
//Pangea#
|
||||
});
|
||||
|
|
@ -1291,7 +1300,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
if (choreographer.itController.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pangea#
|
||||
if (!event.redacted) {
|
||||
if (selectedEvents.contains(event)) {
|
||||
|
|
@ -1355,9 +1363,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
if (choice == 'camera-video') {
|
||||
openVideoCameraAction();
|
||||
}
|
||||
if (choice == 'sticker') {
|
||||
sendStickerAction();
|
||||
}
|
||||
if (choice == 'location') {
|
||||
sendLocationAction();
|
||||
}
|
||||
|
|
@ -1402,7 +1407,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void onInputBarChanged(String text) {
|
||||
if (_inputTextIsEmpty != text.isEmpty) {
|
||||
setReadMarker();
|
||||
setState(() {
|
||||
_inputTextIsEmpty = text.isEmpty;
|
||||
});
|
||||
|
|
@ -1568,7 +1572,11 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
);
|
||||
}
|
||||
|
||||
void setToolbarDisplayController(String eventId) {
|
||||
void setToolbarDisplayController(
|
||||
String eventId, {
|
||||
Event? nextEvent,
|
||||
Event? previousEvent,
|
||||
}) {
|
||||
final Event? event = timeline!.events.firstWhereOrNull(
|
||||
(e) => e.eventId == eventId,
|
||||
);
|
||||
|
|
@ -1584,6 +1592,8 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
pangeaMessageEvent: _pangeaMessageEvents[eventId]!,
|
||||
immersionMode: choreographer.immersionMode,
|
||||
controller: this,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
_toolbarDisplayControllers[eventId]!.setToolbar();
|
||||
} catch (e, s) {
|
||||
|
|
@ -1607,16 +1617,85 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
return _pangeaMessageEvents[eventId];
|
||||
}
|
||||
|
||||
ToolbarDisplayController? getToolbarDisplayController(String eventId) {
|
||||
ToolbarDisplayController? getToolbarDisplayController(
|
||||
String eventId, {
|
||||
Event? nextEvent,
|
||||
Event? previousEvent,
|
||||
}) {
|
||||
if (_toolbarDisplayControllers[eventId] == null) {
|
||||
setToolbarDisplayController(eventId);
|
||||
setToolbarDisplayController(
|
||||
eventId,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
}
|
||||
return _toolbarDisplayControllers[eventId];
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
late final ValueNotifier<bool> _displayChatDetailsColumn;
|
||||
|
||||
void toggleDisplayChatDetailsColumn() async {
|
||||
await Matrix.of(context).store.setBool(
|
||||
SettingKeys.displayChatDetailsColumn,
|
||||
!_displayChatDetailsColumn.value,
|
||||
);
|
||||
_displayChatDetailsColumn.value = !_displayChatDetailsColumn.value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ChatView(this);
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ChatView(this),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _displayChatDetailsColumn,
|
||||
builder: (context, displayChatDetailsColumn, _) {
|
||||
if (!FluffyThemes.isThreeColumnMode(context) ||
|
||||
room.membership != Membership.join ||
|
||||
!displayChatDetailsColumn) {
|
||||
return const SizedBox(
|
||||
height: double.infinity,
|
||||
width: 0,
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: FluffyThemes.columnWidth,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ChatDetails(
|
||||
roomId: roomId,
|
||||
embeddedCloseButton: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: toggleDisplayChatDetailsColumn,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
enum EmojiPickerType { reaction, keyboard }
|
||||
|
||||
extension on List<Event> {
|
||||
int get firstIndexWhereNotError {
|
||||
if (isEmpty) return 0;
|
||||
final index = indexWhere((event) => !event.status.isError);
|
||||
if (index == -1) return length;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
lib/pages/chat/chat_app_bar_list_tile.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
|
||||
class ChatAppBarListTile extends StatelessWidget {
|
||||
final Widget? leading;
|
||||
final String title;
|
||||
final Widget? trailing;
|
||||
final void Function()? onTap;
|
||||
|
||||
const ChatAppBarListTile({
|
||||
super.key,
|
||||
this.leading,
|
||||
required this.title,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leading = this.leading;
|
||||
final trailing = this.trailing;
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) leading,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Linkify(
|
||||
text: title,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,9 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||
highlightColor: Colors.transparent,
|
||||
onTap: controller.isArchived
|
||||
? null
|
||||
: () => context.go('/rooms/${room.id}/details'),
|
||||
: () => FluffyThemes.isThreeColumnMode(context)
|
||||
? controller.toggleDisplayChatDetailsColumn()
|
||||
: context.go('/rooms/${room.id}/details'),
|
||||
child: Row(
|
||||
children: [
|
||||
Hero(
|
||||
|
|
@ -37,7 +39,6 @@ class ChatAppBarTitle extends StatelessWidget {
|
|||
MatrixLocals(L10n.of(context)!),
|
||||
),
|
||||
size: 32,
|
||||
presenceUserId: room.directChatMatrixID,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/sticker_picker_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'chat.dart';
|
||||
|
||||
class ChatEmojiPicker extends StatelessWidget {
|
||||
|
|
@ -16,30 +17,73 @@ class ChatEmojiPicker extends StatelessWidget {
|
|||
return AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
height: controller.showEmojiPicker
|
||||
? MediaQuery.of(context).size.height / 2
|
||||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.emojiPickerBackspace,
|
||||
config: Config(
|
||||
backspaceColor: theme.colorScheme.primary,
|
||||
bgColor: Color.lerp(
|
||||
theme.colorScheme.background,
|
||||
theme.colorScheme.primaryContainer,
|
||||
0.25,
|
||||
)!,
|
||||
iconColor: theme.colorScheme.primary.withOpacity(0.5),
|
||||
iconColorSelected: theme.colorScheme.primary,
|
||||
indicatorColor: theme.colorScheme.primary,
|
||||
noRecents: const NoRecent(),
|
||||
skinToneDialogBgColor: Color.lerp(
|
||||
theme.colorScheme.background,
|
||||
theme.colorScheme.primaryContainer,
|
||||
0.75,
|
||||
)!,
|
||||
skinToneIndicatorColor: theme.colorScheme.onBackground,
|
||||
? DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: L10n.of(context)!.emojis),
|
||||
Tab(text: L10n.of(context)!.stickers),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
EmojiPicker(
|
||||
onEmojiSelected: controller.onEmojiSelected,
|
||||
onBackspacePressed: controller.emojiPickerBackspace,
|
||||
config: Config(
|
||||
emojiViewConfig: EmojiViewConfig(
|
||||
noRecents: const NoRecent(),
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onInverseSurface,
|
||||
),
|
||||
bottomActionBarConfig: const BottomActionBarConfig(
|
||||
enabled: false,
|
||||
),
|
||||
categoryViewConfig: CategoryViewConfig(
|
||||
backspaceColor: theme.colorScheme.primary,
|
||||
iconColor:
|
||||
theme.colorScheme.primary.withOpacity(0.5),
|
||||
iconColorSelected: theme.colorScheme.primary,
|
||||
indicatorColor: theme.colorScheme.primary,
|
||||
),
|
||||
skinToneConfig: SkinToneConfig(
|
||||
dialogBackgroundColor: Color.lerp(
|
||||
theme.colorScheme.background,
|
||||
theme.colorScheme.primaryContainer,
|
||||
0.75,
|
||||
)!,
|
||||
indicatorColor: theme.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
StickerPickerDialog(
|
||||
room: controller.room,
|
||||
onSelected: (sticker) {
|
||||
controller.room.sendEvent(
|
||||
{
|
||||
'body': sticker.body,
|
||||
'info': sticker.info ?? {},
|
||||
'url': sticker.url.toString(),
|
||||
},
|
||||
type: EventTypes.Sticker,
|
||||
);
|
||||
controller.hideEmojiPicker();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
|||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.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';
|
||||
|
|
@ -27,7 +28,6 @@ class ChatEventList extends StatelessWidget {
|
|||
final events = controller.timeline!.events
|
||||
.where((event) => event.isVisibleInGui)
|
||||
.toList();
|
||||
|
||||
final animateInEventIndex = controller.animateInEventIndex;
|
||||
|
||||
// create a map of eventId --> index to greatly improve performance of
|
||||
|
|
@ -37,11 +37,14 @@ class ChatEventList extends StatelessWidget {
|
|||
thisEventsKeyMap[events[i].eventId] = i;
|
||||
}
|
||||
|
||||
final hasWallpaper =
|
||||
controller.room.client.applicationAccountConfig.wallpaperUrl != null;
|
||||
|
||||
return SelectionArea(
|
||||
child: ListView.custom(
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom: 4,
|
||||
bottom: 8,
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
|
|
@ -94,8 +97,12 @@ class ChatEventList extends StatelessWidget {
|
|||
if (controller.timeline!.canRequestHistory) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
// #Pangea
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => controller.requestHistory);
|
||||
// WidgetsBinding.instance
|
||||
// .addPostFrameCallback(controller.requestHistory);
|
||||
// Pangea#
|
||||
return Center(
|
||||
child: IconButton(
|
||||
onPressed: controller.requestHistory,
|
||||
|
|
@ -147,8 +154,8 @@ class ChatEventList extends StatelessWidget {
|
|||
onSelect: controller.onSelectMessage,
|
||||
scrollToEventId: (String eventId) =>
|
||||
controller.scrollToEventId(eventId),
|
||||
// #Pangea
|
||||
longPressSelect: controller.selectedEvents.isNotEmpty,
|
||||
// #Pangea
|
||||
selectedDisplayLang:
|
||||
controller.choreographer.messageOptions.selectedDisplayLang,
|
||||
immersionMode: controller.choreographer.immersionMode,
|
||||
|
|
@ -162,6 +169,9 @@ class ChatEventList extends StatelessWidget {
|
|||
controller.readMarkerEventId == event.eventId &&
|
||||
controller.timeline?.allowNewEvent == false,
|
||||
nextEvent: i + 1 < events.length ? events[i + 1] : null,
|
||||
previousEvent: i > 0 ? events[i - 1] : null,
|
||||
avatarPresenceBackgroundColor:
|
||||
hasWallpaper ? Colors.transparent : null,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import 'package:animations/animations.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_actions.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../config/themes.dart';
|
||||
|
|
@ -27,6 +24,8 @@ class ChatInputRow extends StatelessWidget {
|
|||
controller.emojiPickerType == EmojiPickerType.reaction) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
const height = 48.0;
|
||||
|
||||
// #Pangea
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -43,7 +42,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
if (controller.selectedEvents
|
||||
.every((event) => event.status == EventStatus.error))
|
||||
SizedBox(
|
||||
height: 56,
|
||||
height: height,
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
|
|
@ -59,7 +58,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 56,
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.forwardEventsAction,
|
||||
child: Row(
|
||||
|
|
@ -76,7 +75,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
.status
|
||||
.isSent
|
||||
? SizedBox(
|
||||
height: 56,
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.replyAction,
|
||||
child: Row(
|
||||
|
|
@ -88,7 +87,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: 56,
|
||||
height: height,
|
||||
child: TextButton(
|
||||
onPressed: controller.sendAgainAction,
|
||||
child: Row(
|
||||
|
|
@ -103,197 +102,209 @@ class ChatInputRow extends StatelessWidget {
|
|||
: const SizedBox.shrink(),
|
||||
]
|
||||
: <Widget>[
|
||||
KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.altLeft,
|
||||
LogicalKeyboardKey.keyA,
|
||||
},
|
||||
onKeysPressed: () =>
|
||||
controller.onAddPopupMenuButtonSelected('file'),
|
||||
helpLabel: L10n.of(context)!.sendFile,
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: 56,
|
||||
//#Pangea
|
||||
// width: controller.sendController.text.isEmpty ? 56 : 0,
|
||||
width: controller.sendController.text.isEmpty &&
|
||||
controller.pangeaController.permissionsController
|
||||
.showChatInputAddButton(controller.roomId)
|
||||
? 56
|
||||
: 0,
|
||||
//Pangea#
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
//#Pangea
|
||||
if (controller.pangeaController.permissionsController
|
||||
.canShareFile(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 4),
|
||||
// KeyBoardShortcuts(
|
||||
// keysToPress: {
|
||||
// LogicalKeyboardKey.altLeft,
|
||||
// LogicalKeyboardKey.keyA,
|
||||
// },
|
||||
// onKeysPressed: () =>
|
||||
// controller.onAddPopupMenuButtonSelected('file'),
|
||||
// helpLabel: L10n.of(context)!.sendFile,
|
||||
// child:
|
||||
// Pangea#
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: height,
|
||||
// #Pangea
|
||||
// width:
|
||||
// controller.sendController.text.isEmpty ? height : 0,
|
||||
width: controller.sendController.text.isEmpty &&
|
||||
controller.pangeaController.permissionsController
|
||||
.showChatInputAddButton(controller.roomId)
|
||||
? height
|
||||
: 0,
|
||||
// Pangea#
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
//#Pangea
|
||||
if (controller.pangeaController.permissionsController
|
||||
.canShareFile(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
if (controller.pangeaController.permissionsController
|
||||
.canSharePhoto(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
if (controller.pangeaController.permissionsController
|
||||
.canSharePhoto(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canSharePhoto(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canSharePhoto(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canShareVideo(controller.roomId))
|
||||
//Pangea#
|
||||
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),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canShareVideo(controller.roomId))
|
||||
//Pangea#
|
||||
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 (controller.room
|
||||
.getImagePacks(ImagePackUsage.sticker)
|
||||
.isNotEmpty)
|
||||
PopupMenuItem<String>(
|
||||
value: 'sticker',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.emoji_emotions_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.sendSticker),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
if (controller.room
|
||||
.getImagePacks(ImagePackUsage.sticker)
|
||||
.isNotEmpty)
|
||||
PopupMenuItem<String>(
|
||||
value: 'sticker',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.orange,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.emoji_emotions_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.sendSticker),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canShareLocation(controller.roomId))
|
||||
//Pangea#
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.brown,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
//#Pangea
|
||||
// if (PlatformInfos.isMobile)
|
||||
if (PlatformInfos.isMobile &&
|
||||
controller.pangeaController.permissionsController
|
||||
.canShareLocation(controller.roomId))
|
||||
//Pangea#
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.brown,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context)!.shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// ),
|
||||
// Pangea#
|
||||
Container(
|
||||
height: 56,
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.altLeft,
|
||||
LogicalKeyboardKey.keyE,
|
||||
},
|
||||
onKeysPressed: controller.emojiPickerAction,
|
||||
helpLabel: L10n.of(context)!.emojis,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context)!.emojis,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.emoji_emotions_outlined,
|
||||
key: ValueKey(controller.showEmojiPicker),
|
||||
),
|
||||
child:
|
||||
// #Pangea
|
||||
// KeyBoardShortcuts(
|
||||
// keysToPress: {
|
||||
// LogicalKeyboardKey.altLeft,
|
||||
// LogicalKeyboardKey.keyE,
|
||||
// },
|
||||
// onKeysPressed: controller.emojiPickerAction,
|
||||
// helpLabel: L10n.of(context)!.emojis,
|
||||
// child:
|
||||
// Pangea#
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.emojis,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.add_reaction_outlined,
|
||||
key: ValueKey(controller.showEmojiPicker),
|
||||
),
|
||||
onPressed: controller.emojiPickerAction,
|
||||
),
|
||||
onPressed: controller.emojiPickerAction,
|
||||
),
|
||||
// #Pangea
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
// #Pangea
|
||||
// if (Matrix.of(context).isMultiAccount &&
|
||||
// Matrix.of(context).hasComplexBundles &&
|
||||
// Matrix.of(context).currentBundle!.length > 1)
|
||||
// Container(
|
||||
// height: 56,
|
||||
// width: height,
|
||||
// height: height,
|
||||
// alignment: Alignment.center,
|
||||
// child: _ChatAccountPicker(controller),
|
||||
// ),
|
||||
// Pangea#
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
// #Pangea
|
||||
// autofocus: !PlatformInfos.isMobile,
|
||||
autofocus: false,
|
||||
// Pangea#
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: AppConfig.sendOnEnter == true &&
|
||||
PlatformInfos.isMobile
|
||||
|
|
@ -303,11 +314,17 @@ class ChatInputRow extends StatelessWidget {
|
|||
// onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitted: (String value) =>
|
||||
controller.onInputBarSubmitted(value, context),
|
||||
// #Pangea
|
||||
// Pangea#
|
||||
onSubmitImage: controller.sendImageFromClipBoard,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
),
|
||||
hintText: L10n.of(context)!.writeAMessage,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
|
|
@ -318,31 +335,46 @@ class ChatInputRow extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty)
|
||||
Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context)!.voiceMessage,
|
||||
icon: const Icon(Icons.mic_none_outlined),
|
||||
onPressed: controller.voiceMessageAction,
|
||||
),
|
||||
),
|
||||
if (!PlatformInfos.isMobile ||
|
||||
controller.sendController.text.isNotEmpty)
|
||||
// #Pangea
|
||||
ChoreographerSendButton(controller: controller),
|
||||
// Container(
|
||||
// height: 56,
|
||||
// alignment: Alignment.center,
|
||||
// child: IconButton(
|
||||
// icon: const Icon(Icons.send_outlined),
|
||||
// onPressed: controller.send,
|
||||
// tooltip: L10n.of(context)!.send,
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty
|
||||
? FloatingActionButton.small(
|
||||
tooltip: L10n.of(context)!.voiceMessage,
|
||||
onPressed: controller.voiceMessageAction,
|
||||
elevation: 0,
|
||||
heroTag: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(height),
|
||||
),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
child: const Icon(Icons.mic_none_outlined),
|
||||
)
|
||||
:
|
||||
// #Pangea
|
||||
ChoreographerSendButton(controller: controller),
|
||||
// FloatingActionButton.small(
|
||||
// tooltip: L10n.of(context)!.send,
|
||||
// onPressed: controller.send,
|
||||
// elevation: 0,
|
||||
// heroTag: null,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(height),
|
||||
// ),
|
||||
// backgroundColor: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onPrimaryContainer,
|
||||
// foregroundColor:
|
||||
// Theme.of(context).colorScheme.onPrimary,
|
||||
// child: const Icon(Icons.send_outlined),
|
||||
// ),
|
||||
// Pangea#
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import 'package:badges/badges.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pages/chat/reactions_picker.dart';
|
||||
import 'package:fluffychat/pages/chat/reply_display.dart';
|
||||
import 'package:fluffychat/pages/chat/tombstone_display.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
import 'package:fluffychat/widgets/connection_status_header.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -118,7 +119,8 @@ class ChatView extends StatelessWidget {
|
|||
ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat),
|
||||
];
|
||||
}
|
||||
// } else if (!controller.room.isArchived) {
|
||||
|
||||
// else if (!controller.room.isArchived) {
|
||||
// return [
|
||||
// if (Matrix.of(context).voipPlugin != null &&
|
||||
// controller.room.isDirectChat)
|
||||
|
|
@ -146,6 +148,8 @@ class ChatView extends StatelessWidget {
|
|||
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
|
||||
final scrollUpBannerEventId = controller.scrollUpBannerEventId;
|
||||
|
||||
final accountConfig = Matrix.of(context).client.applicationAccountConfig;
|
||||
|
||||
return PopScope(
|
||||
canPop: controller.selectedEvents.isEmpty && !controller.showEmojiPicker,
|
||||
onPopInvoked: (pop) async {
|
||||
|
|
@ -156,272 +160,276 @@ class ChatView extends StatelessWidget {
|
|||
controller.emojiPickerAction();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => controller.setReadMarker(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => controller.setReadMarker(),
|
||||
child: StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) => FutureBuilder(
|
||||
future: controller.loadTimelineFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedEvents.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) =>
|
||||
r.id != controller.roomId
|
||||
// #Pangea
|
||||
&&
|
||||
!r.isAnalyticsRoom,
|
||||
// Pangea#
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
child: StreamBuilder(
|
||||
stream: controller.room.onUpdate.stream
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) => FutureBuilder(
|
||||
future: controller.loadTimelineFuture,
|
||||
builder: (BuildContext context, snapshot) {
|
||||
var appbarBottomHeight = 0.0;
|
||||
if (controller.room.pinnedEventIds.isNotEmpty) {
|
||||
appbarBottomHeight += 42;
|
||||
}
|
||||
if (scrollUpBannerEventId != null) {
|
||||
appbarBottomHeight += 42;
|
||||
}
|
||||
final tombstoneEvent =
|
||||
controller.room.getState(EventTypes.RoomTombstone);
|
||||
if (tombstoneEvent != null) {
|
||||
appbarBottomHeight += 42;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedEvents.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context)!.close,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: UnreadRoomsBadge(
|
||||
filter: (r) =>
|
||||
r.id != controller.roomId
|
||||
// #Pangea
|
||||
&&
|
||||
!r.isAnalyticsRoom,
|
||||
// Pangea#,
|
||||
badgePosition: BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(appbarBottomHeight),
|
||||
child: Column(
|
||||
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),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
trailing: TextButton(
|
||||
onPressed: controller.goToNewRoomAction,
|
||||
child: Text(L10n.of(context)!.goToTheNewRoom),
|
||||
),
|
||||
),
|
||||
if (scrollUpBannerEventId != null)
|
||||
ChatAppBarListTile(
|
||||
leading: IconButton(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: L10n.of(context)!.close,
|
||||
onPressed: () {
|
||||
controller.discardScrollUpBannerEventId();
|
||||
controller.setReadMarker();
|
||||
},
|
||||
),
|
||||
title: L10n.of(context)!.jumpToLastReadMessage,
|
||||
trailing: TextButton(
|
||||
onPressed: () {
|
||||
controller.scrollToEventId(
|
||||
scrollUpBannerEventId,
|
||||
);
|
||||
controller.discardScrollUpBannerEventId();
|
||||
},
|
||||
child: Text(L10n.of(context)!.jump),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// #Pangea
|
||||
// floatingActionButton: controller.showScrollDownButton &&
|
||||
// controller.selectedEvents.isEmpty
|
||||
floatingActionButton: controller.selectedEvents.isEmpty
|
||||
? (controller.showScrollDownButton
|
||||
// Pangea#
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child:
|
||||
const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
: controller.choreographer.errorService.error != null
|
||||
? ChoreographerHasErrorButton(
|
||||
controller.pangeaController,
|
||||
controller.choreographer.errorService.error!,
|
||||
)
|
||||
: controller.showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: controller.choreographer,
|
||||
roomID: controller.roomId,
|
||||
)
|
||||
: null)
|
||||
// #Pangea
|
||||
: null,
|
||||
body:
|
||||
// #Pangea
|
||||
// DropTarget(
|
||||
// onDragDone: controller.onDragDone,
|
||||
// onDragEntered: controller.onDragEntered,
|
||||
// onDragExited: controller.onDragExited,
|
||||
// child:
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// floatingActionButton: controller.showScrollDownButton &&
|
||||
// controller.selectedEvents.isEmpty
|
||||
floatingActionButton: controller.selectedEvents.isEmpty
|
||||
? (controller.showScrollDownButton
|
||||
// Pangea#
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TombstoneDisplay(controller),
|
||||
if (scrollUpBannerEventId != null)
|
||||
Material(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant,
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 56.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: controller.scrollDown,
|
||||
heroTag: null,
|
||||
mini: true,
|
||||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
),
|
||||
)
|
||||
// #Pangea
|
||||
: controller.choreographer.errorService.error != null
|
||||
? ChoreographerHasErrorButton(
|
||||
controller.pangeaController,
|
||||
controller.choreographer.errorService.error!,
|
||||
)
|
||||
: controller.showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: controller.choreographer,
|
||||
roomID: controller.roomId,
|
||||
)
|
||||
: null)
|
||||
// #Pangea
|
||||
: null,
|
||||
body:
|
||||
// #Pangea
|
||||
// DropTarget(
|
||||
// onDragDone: controller.onDragDone,
|
||||
// onDragEntered: controller.onDragEntered,
|
||||
// onDragExited: controller.onDragExited,
|
||||
// child:
|
||||
// Pangea#
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
if (accountConfig.wallpaperUrl != null)
|
||||
Opacity(
|
||||
opacity: accountConfig.wallpaperOpacity ?? 1,
|
||||
child: MxcImage(
|
||||
uri: accountConfig.wallpaperUrl,
|
||||
fit: BoxFit.cover,
|
||||
isThumbnail: true,
|
||||
width: FluffyThemes.columnWidth * 4,
|
||||
height: FluffyThemes.columnWidth * 4,
|
||||
placeholder: (_) => Container(),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
// #Pangea
|
||||
// Container(
|
||||
ConditionalFlexible(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: ConditionalScroll(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: MeasurableWidget(
|
||||
onChange: (size, position) {
|
||||
controller.inputRowSize = size!.height;
|
||||
},
|
||||
child: Container(
|
||||
// Pangea#
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: IconButton(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: L10n.of(context)!.close,
|
||||
onPressed: () {
|
||||
controller.discardScrollUpBannerEventId();
|
||||
controller.setReadMarker();
|
||||
},
|
||||
.surfaceVariant,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(24),
|
||||
),
|
||||
child: controller.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed: controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(
|
||||
16,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.jumpToLastReadMessage,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 8),
|
||||
trailing: TextButton(
|
||||
onPressed: () {
|
||||
controller.scrollToEventId(
|
||||
scrollUpBannerEventId,
|
||||
);
|
||||
controller.discardScrollUpBannerEventId();
|
||||
},
|
||||
child: Text(L10n.of(context)!.jump),
|
||||
),
|
||||
),
|
||||
),
|
||||
PinnedEvents(controller),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.clearSingleSelectedEvent,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller.timeline == null) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ChatEventList(
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
// #Pangea
|
||||
// Container(
|
||||
ConditionalFlexible(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: ConditionalScroll(
|
||||
isScroll: controller.isRowScrollable,
|
||||
child: MeasurableWidget(
|
||||
onChange: (size, position) {
|
||||
controller.inputRowSize = size!.height;
|
||||
},
|
||||
child: Container(
|
||||
// Pangea#
|
||||
margin: EdgeInsets.only(
|
||||
bottom: bottomSheetPadding,
|
||||
left: bottomSheetPadding,
|
||||
right: bottomSheetPadding,
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth:
|
||||
FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
bottomRight: Radius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withAlpha(64),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
child: controller
|
||||
.room.isAbandonedDMRoom ==
|
||||
true
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceEvenly,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16),
|
||||
foregroundColor:
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.error,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.archive_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.leaveChat,
|
||||
label: Text(
|
||||
L10n.of(context)!.leave,
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
16),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.forum_outlined,
|
||||
),
|
||||
onPressed:
|
||||
controller.recreateChat,
|
||||
label: Text(
|
||||
L10n.of(context)!
|
||||
.reopenChat,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
ChatEmojiPicker(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// if (controller.dragging)
|
||||
// Container(
|
||||
// color: Theme.of(context)
|
||||
// .scaffoldBackgroundColor
|
||||
// .withOpacity(0.9),
|
||||
// alignment: Alignment.center,
|
||||
// child: const Icon(
|
||||
// Icons.upload_outlined,
|
||||
// size: 100,
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// if (controller.dragging)
|
||||
// Container(
|
||||
// color: Theme.of(context)
|
||||
// .scaffoldBackgroundColor
|
||||
// .withOpacity(0.9),
|
||||
// alignment: Alignment.center,
|
||||
// child: const Icon(
|
||||
// Icons.upload_outlined,
|
||||
// size: 100,
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ String commandHint(L10n l10n, String command) {
|
|||
return l10n.commandHint_cuddle;
|
||||
case 'sendraw':
|
||||
return l10n.commandHint_sendraw;
|
||||
case 'ignore':
|
||||
return l10n.commandHint_ignore;
|
||||
case 'unignore':
|
||||
return l10n.commandHint_unignore;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
this.color = Colors.black,
|
||||
// #Pangea
|
||||
this.matrixFile,
|
||||
super.key,
|
||||
this.autoplay = false,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -236,6 +236,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
|
||||
late final List<int> waveform;
|
||||
|
||||
void _toggleSpeed() async {
|
||||
final audioPlayer = this.audioPlayer;
|
||||
if (audioPlayer == null) return;
|
||||
switch (audioPlayer.speed) {
|
||||
case 1.0:
|
||||
await audioPlayer.setSpeed(1.5);
|
||||
break;
|
||||
case 1.5:
|
||||
await audioPlayer.setSpeed(2.0);
|
||||
break;
|
||||
case 2.0:
|
||||
await audioPlayer.setSpeed(0.5);
|
||||
break;
|
||||
case 0.5:
|
||||
default:
|
||||
await audioPlayer.setSpeed(1.0);
|
||||
break;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
Future<void> _downloadMatrixFile() async {
|
||||
if (kIsWeb) return;
|
||||
|
|
@ -272,12 +293,12 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final statusText = this.statusText ??= _durationString ?? '00:00';
|
||||
final audioPlayer = this.audioPlayer;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
|
|
@ -354,6 +375,35 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
child: InkWell(
|
||||
splashColor: widget.color.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
onTap: audioPlayer == null ? null : _toggleSpeed,
|
||||
child: Icon(Icons.mic_none_outlined, color: widget.color),
|
||||
),
|
||||
),
|
||||
if (audioPlayer != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'${audioPlayer.speed.toString()}x',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 9.0,
|
||||
color: widget.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
final newHtml = parts
|
||||
.map(
|
||||
(linkifyElement) => linkifyElement is! UrlElement
|
||||
? Uri.encodeComponent(linkifyElement.text)
|
||||
? linkifyElement.text.replaceAll('<', '<')
|
||||
: '<a href="${linkifyElement.text}">${linkifyElement.text}</a>',
|
||||
)
|
||||
.join(' ');
|
||||
|
|
@ -163,7 +163,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
),
|
||||
},
|
||||
extensions: [
|
||||
RoomPillExtension(context, room),
|
||||
RoomPillExtension(context, room, fontSize, linkColor),
|
||||
CodeExtension(fontSize: fontSize),
|
||||
MatrixMathExtension(
|
||||
style: TextStyle(fontSize: fontSize, color: textColor),
|
||||
|
|
@ -172,6 +172,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
SpoilerExtension(textColor: textColor),
|
||||
const ImageExtension(),
|
||||
FontColorExtension(),
|
||||
FallbackTextExtension(fontSize: fontSize),
|
||||
],
|
||||
onLinkTap: (url, _, element) => UrlLauncher(
|
||||
context,
|
||||
|
|
@ -195,6 +196,8 @@ class HtmlMessage extends StatelessWidget {
|
|||
// );
|
||||
}
|
||||
|
||||
static const Set<String> fallbackTextTags = {'tg-forward'};
|
||||
|
||||
/// Keep in sync with: https://spec.matrix.org/v1.6/client-server-api/#mroommessage-msgtypes
|
||||
static const Set<String> allowedHtmlTags = {
|
||||
'font',
|
||||
|
|
@ -240,7 +243,7 @@ class HtmlMessage extends StatelessWidget {
|
|||
'rp',
|
||||
'rt',
|
||||
// Workaround for https://github.com/krille-chan/fluffychat/issues/507
|
||||
'tg-forward',
|
||||
...fallbackTextTags,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -433,11 +436,29 @@ class CodeExtension extends HtmlExtension {
|
|||
);
|
||||
}
|
||||
|
||||
class FallbackTextExtension extends HtmlExtension {
|
||||
final double fontSize;
|
||||
|
||||
FallbackTextExtension({required this.fontSize});
|
||||
@override
|
||||
Set<String> get supportedTags => HtmlMessage.fallbackTextTags;
|
||||
|
||||
@override
|
||||
InlineSpan build(ExtensionContext context) => TextSpan(
|
||||
text: context.element?.text ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RoomPillExtension extends HtmlExtension {
|
||||
final Room room;
|
||||
final BuildContext context;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
|
||||
RoomPillExtension(this.context, this.room);
|
||||
RoomPillExtension(this.context, this.room, this.fontSize, this.color);
|
||||
@override
|
||||
Set<String> get supportedTags => {'a'};
|
||||
|
||||
|
|
@ -474,6 +495,8 @@ class RoomPillExtension extends HtmlExtension {
|
|||
avatar: _cachedUsers[room.id + matrixId]?.avatarUrl,
|
||||
uri: href,
|
||||
outerContext: this.context,
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -489,6 +512,8 @@ class RoomPillExtension extends HtmlExtension {
|
|||
avatar: room.avatar,
|
||||
uri: href,
|
||||
outerContext: this.context,
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -503,6 +528,8 @@ class MatrixPill extends StatelessWidget {
|
|||
final BuildContext outerContext;
|
||||
final Uri? avatar;
|
||||
final String uri;
|
||||
final double? fontSize;
|
||||
final Color? color;
|
||||
|
||||
const MatrixPill({
|
||||
super.key,
|
||||
|
|
@ -510,41 +537,34 @@ class MatrixPill extends StatelessWidget {
|
|||
required this.outerContext,
|
||||
this.avatar,
|
||||
required this.uri,
|
||||
required this.fontSize,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: UrlLauncher(outerContext, uri).launchUrl,
|
||||
child: Material(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
side: BorderSide(
|
||||
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
|
||||
width: 0.5,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: avatar,
|
||||
name: name,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
color: Theme.of(outerContext).colorScheme.primaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: avatar,
|
||||
name: name,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: Theme.of(outerContext).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
decorationColor: color,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: fontSize,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/image_viewer/image_viewer.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import '../../../widgets/blur_hash.dart';
|
||||
|
||||
class ImageBubble extends StatelessWidget {
|
||||
final Event event;
|
||||
|
|
@ -25,7 +25,7 @@ class ImageBubble extends StatelessWidget {
|
|||
this.tapToView = true,
|
||||
this.maxSize = true,
|
||||
this.backgroundColor,
|
||||
this.fit = BoxFit.cover,
|
||||
this.fit = BoxFit.contain,
|
||||
this.thumbnailOnly = true,
|
||||
this.width = 400,
|
||||
this.height = 300,
|
||||
|
|
@ -36,35 +36,18 @@ class ImageBubble extends StatelessWidget {
|
|||
});
|
||||
|
||||
Widget _buildPlaceholder(BuildContext context) {
|
||||
if (event.messageType == MessageTypes.Sticker) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
final String blurHashString =
|
||||
event.infoMap['xyz.amorgan.blurhash'] is String
|
||||
? event.infoMap['xyz.amorgan.blurhash']
|
||||
: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj';
|
||||
final ratio = event.infoMap['w'] is int && event.infoMap['h'] is int
|
||||
? event.infoMap['w'] / event.infoMap['h']
|
||||
: 1.0;
|
||||
var width = 32;
|
||||
var height = 32;
|
||||
if (ratio > 1.0) {
|
||||
height = (width / ratio).round();
|
||||
if (height <= 0) height = 1;
|
||||
} else {
|
||||
width = (height * ratio).round();
|
||||
if (width <= 0) width = 1;
|
||||
}
|
||||
return SizedBox(
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
width: width,
|
||||
height: height,
|
||||
child: BlurHash(
|
||||
hash: blurHashString,
|
||||
decodingWidth: width,
|
||||
decodingHeight: height,
|
||||
imageFit: fit,
|
||||
blurhash: blurHashString,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -100,22 +83,16 @@ class ImageBubble extends StatelessWidget {
|
|||
borderRadius: borderRadius,
|
||||
child: Hero(
|
||||
tag: event.eventId,
|
||||
child: ConstrainedBox(
|
||||
constraints: maxSize
|
||||
? BoxConstraints(
|
||||
maxWidth: width,
|
||||
maxHeight: height,
|
||||
)
|
||||
: const BoxConstraints.expand(),
|
||||
child: MxcImage(
|
||||
event: event,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
animated: animated,
|
||||
isThumbnail: thumbnailOnly,
|
||||
placeholder: _buildPlaceholder,
|
||||
),
|
||||
child: MxcImage(
|
||||
event: event,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
animated: animated,
|
||||
isThumbnail: thumbnailOnly,
|
||||
placeholder: event.messageType == MessageTypes.Sticker
|
||||
? null
|
||||
: _buildPlaceholder,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
|
|
@ -32,8 +31,8 @@ class MapBubble extends StatelessWidget {
|
|||
children: <Widget>[
|
||||
FlutterMap(
|
||||
options: MapOptions(
|
||||
center: LatLng(latitude, longitude),
|
||||
zoom: zoom,
|
||||
initialCenter: LatLng(latitude, longitude),
|
||||
initialZoom: zoom,
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
|
|
@ -50,7 +49,7 @@ class MapBubble extends StatelessWidget {
|
|||
point: LatLng(latitude, longitude),
|
||||
width: 30,
|
||||
height: 30,
|
||||
builder: (_) => Transform.translate(
|
||||
child: Transform.translate(
|
||||
// No idea why the offset has to be like this, instead of -15
|
||||
// It has been determined by trying out, though, that this yields
|
||||
// the tip of the location pin to be static when zooming.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import 'verification_request_content.dart';
|
|||
class Message extends StatelessWidget {
|
||||
final Event event;
|
||||
final Event? nextEvent;
|
||||
final Event? previousEvent;
|
||||
final bool displayReadMarker;
|
||||
final void Function(Event) onSelect;
|
||||
final void Function(Event) onAvatarTab;
|
||||
|
|
@ -43,10 +44,12 @@ class Message extends StatelessWidget {
|
|||
final bool definitions;
|
||||
final ChatController controller;
|
||||
// Pangea#
|
||||
final Color? avatarPresenceBackgroundColor;
|
||||
|
||||
const Message(
|
||||
this.event, {
|
||||
this.nextEvent,
|
||||
this.previousEvent,
|
||||
this.displayReadMarker = false,
|
||||
this.longPressSelect = false,
|
||||
required this.onSelect,
|
||||
|
|
@ -65,13 +68,25 @@ class Message extends StatelessWidget {
|
|||
required this.definitions,
|
||||
required this.controller,
|
||||
// Pangea#
|
||||
this.avatarPresenceBackgroundColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
// #Pangea
|
||||
PangeaMessageEvent? get pangeaMessageEvent =>
|
||||
controller.getPangeaMessageEvent(event.eventId);
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
debugPrint('Message.build()');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (controller.edittingEvents.contains(event.eventId)) {
|
||||
pangeaMessageEvent?.updateLatestEdit();
|
||||
controller.clearEdittingEvent(event.eventId);
|
||||
}
|
||||
});
|
||||
// Pangea#
|
||||
if (!{
|
||||
EventTypes.Message,
|
||||
|
|
@ -97,31 +112,40 @@ class Message extends StatelessWidget {
|
|||
final displayTime = event.type == EventTypes.RoomCreate ||
|
||||
nextEvent == null ||
|
||||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
|
||||
final sameSender = nextEvent != null &&
|
||||
final nextEventSameSender = nextEvent != null &&
|
||||
{
|
||||
EventTypes.Message,
|
||||
EventTypes.Sticker,
|
||||
EventTypes.Encrypted,
|
||||
}.contains(nextEvent!.type) &&
|
||||
nextEvent?.relationshipType == null &&
|
||||
nextEvent!.senderId == event.senderId &&
|
||||
!displayTime;
|
||||
|
||||
final previousEventSameSender = previousEvent != null &&
|
||||
{
|
||||
EventTypes.Message,
|
||||
EventTypes.Sticker,
|
||||
EventTypes.Encrypted,
|
||||
}.contains(previousEvent!.type) &&
|
||||
previousEvent!.senderId == event.senderId &&
|
||||
previousEvent!.originServerTs.sameEnvironment(event.originServerTs);
|
||||
|
||||
final textColor = ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground;
|
||||
final rowMainAxisAlignment =
|
||||
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start;
|
||||
|
||||
final displayEvent = event.getDisplayEvent(timeline);
|
||||
const hardCorner = Radius.circular(4);
|
||||
const roundedCorner = Radius.circular(AppConfig.borderRadius);
|
||||
final borderRadius = BorderRadius.only(
|
||||
topLeft: !ownMessage
|
||||
? const Radius.circular(4)
|
||||
: const Radius.circular(AppConfig.borderRadius),
|
||||
topRight: const Radius.circular(AppConfig.borderRadius),
|
||||
bottomLeft: const Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: ownMessage
|
||||
? const Radius.circular(4)
|
||||
: const Radius.circular(AppConfig.borderRadius),
|
||||
topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
|
||||
topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
|
||||
bottomLeft:
|
||||
!ownMessage && previousEventSameSender ? hardCorner : roundedCorner,
|
||||
bottomRight:
|
||||
ownMessage && previousEventSameSender ? hardCorner : roundedCorner,
|
||||
);
|
||||
final noBubble = {
|
||||
MessageTypes.Video,
|
||||
|
|
@ -137,17 +161,20 @@ class Message extends StatelessWidget {
|
|||
if (ownMessage) {
|
||||
color = displayEvent.status.isError
|
||||
? Colors.redAccent
|
||||
: Theme.of(context).colorScheme.primaryContainer;
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
final PangeaMessageEvent? pangeaMessageEvent =
|
||||
controller.getPangeaMessageEvent(event.eventId);
|
||||
ToolbarDisplayController? toolbarController;
|
||||
if (event.messageType == MessageTypes.Text ||
|
||||
if (event.type == EventTypes.Message &&
|
||||
event.messageType == MessageTypes.Text ||
|
||||
event.messageType == MessageTypes.Notice ||
|
||||
event.messageType == MessageTypes.Audio) {
|
||||
toolbarController = controller.getToolbarDisplayController(event.eventId);
|
||||
toolbarController = controller.getToolbarDisplayController(
|
||||
event.eventId,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
|
|
@ -180,7 +207,7 @@ class Message extends StatelessWidget {
|
|||
onChanged: (_) => onSelect(event),
|
||||
),
|
||||
)
|
||||
else if (sameSender || ownMessage)
|
||||
else if (nextEventSameSender || ownMessage)
|
||||
SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
child: Center(
|
||||
|
|
@ -207,6 +234,7 @@ class Message extends StatelessWidget {
|
|||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
presenceUserId: user.stateKey,
|
||||
presenceBackgroundColor: avatarPresenceBackgroundColor,
|
||||
onTap: () => onAvatarTab(event),
|
||||
);
|
||||
},
|
||||
|
|
@ -216,7 +244,7 @@ class Message extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!sameSender)
|
||||
if (!nextEventSameSender)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, bottom: 4),
|
||||
child: ownMessage || event.room.isDirectChat
|
||||
|
|
@ -232,7 +260,6 @@ class Message extends StatelessWidget {
|
|||
displayname,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? displayname.color
|
||||
|
|
@ -254,8 +281,8 @@ class Message extends StatelessWidget {
|
|||
onLongPress: longPressSelect
|
||||
? null
|
||||
: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
onSelect(event);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
child: AnimatedOpacity(
|
||||
opacity: animateIn
|
||||
|
|
@ -352,7 +379,8 @@ class Message extends StatelessWidget {
|
|||
borderRadius: borderRadius,
|
||||
// #Pangea
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
pangeaMessageEvent:
|
||||
toolbarController?.pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
// Pangea#
|
||||
|
|
@ -362,7 +390,9 @@ class Message extends StatelessWidget {
|
|||
RelationshipTypes.edit,
|
||||
) // #Pangea
|
||||
||
|
||||
(pangeaMessageEvent?.showUseType ??
|
||||
(toolbarController
|
||||
?.pangeaMessageEvent
|
||||
.showUseType ??
|
||||
false)
|
||||
// Pangea#
|
||||
)
|
||||
|
|
@ -374,10 +404,12 @@ class Message extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
if (pangeaMessageEvent
|
||||
?.showUseType ??
|
||||
if (toolbarController
|
||||
?.pangeaMessageEvent
|
||||
.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!.useType
|
||||
toolbarController!
|
||||
.pangeaMessageEvent.useType
|
||||
.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
|
|
@ -448,25 +480,34 @@ class Message extends StatelessWidget {
|
|||
BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
event.originServerTs.localizedTime(context),
|
||||
style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
row,
|
||||
if (showReceiptsRow)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4.0,
|
||||
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
||||
right: 12.0,
|
||||
),
|
||||
child: MessageReactions(event, timeline),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child: !showReceiptsRow
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 4.0,
|
||||
left: (ownMessage ? 0 : Avatar.defaultSize) + 12.0,
|
||||
right: ownMessage ? 0 : 12.0,
|
||||
),
|
||||
child: MessageReactions(event, timeline),
|
||||
),
|
||||
),
|
||||
if (displayReadMarker)
|
||||
Row(
|
||||
children: [
|
||||
|
|
@ -526,16 +567,18 @@ class Message extends StatelessWidget {
|
|||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
top: nextEventSameSender ? 1.0 : 4.0,
|
||||
bottom: previousEventSameSender ? 1.0 : 4.0,
|
||||
),
|
||||
child: container,
|
||||
),
|
||||
Positioned(
|
||||
left: ownMessage ? null : 48,
|
||||
right: ownMessage ? 4 : null,
|
||||
bottom: showReceiptsRow ? 28 : 0,
|
||||
top: displayTime ? 38 : 0,
|
||||
child: AnimatedScale(
|
||||
duration: Duration(
|
||||
milliseconds:
|
||||
|
|
@ -548,28 +591,50 @@ class Message extends StatelessWidget {
|
|||
child: Material(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.tertiaryContainer
|
||||
.secondaryContainer
|
||||
.withOpacity(0.9),
|
||||
elevation:
|
||||
Theme.of(context).appBarTheme.scrolledUnderElevation ??
|
||||
4,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor,
|
||||
child: SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
// #Pangea
|
||||
// Icons.adaptive.more_outlined,
|
||||
Icons.add_reaction_outlined,
|
||||
// Pangea#
|
||||
size: 16,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onTertiaryContainer,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (event.room.canSendDefaultMessages)
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.reply_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onTertiaryContainer,
|
||||
),
|
||||
tooltip: L10n.of(context)!.reply,
|
||||
onPressed: event.room.canSendDefaultMessages
|
||||
? () => onSwipe()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
size: 16,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onTertiaryContainer,
|
||||
),
|
||||
tooltip: L10n.of(context)!.select,
|
||||
onPressed: () => onSelect(event),
|
||||
),
|
||||
),
|
||||
onPressed: () => onSelect(event),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:fluffychat/pages/chat/events/html_message.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
|
|
@ -19,10 +20,10 @@ import '../../../utils/platform_infos.dart';
|
|||
import '../../../utils/url_launcher.dart';
|
||||
import 'audio_player.dart';
|
||||
import 'cute_events.dart';
|
||||
import 'html_message.dart';
|
||||
import 'image_bubble.dart';
|
||||
import 'map_bubble.dart';
|
||||
import 'message_download_content.dart';
|
||||
import 'sticker.dart';
|
||||
|
||||
class MessageContent extends StatelessWidget {
|
||||
final Event event;
|
||||
|
|
@ -59,11 +60,7 @@ class MessageContent extends StatelessWidget {
|
|||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
event.type == EventTypes.Encrypted
|
||||
? l10n.needPantalaimonWarning
|
||||
: event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(l10n),
|
||||
),
|
||||
event.calcLocalizedBodyFallback(MatrixLocals(l10n)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -124,21 +121,54 @@ class MessageContent extends StatelessWidget {
|
|||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
final buttonTextColor = textColor;
|
||||
switch (event.type) {
|
||||
case EventTypes.Message:
|
||||
// #Pangea
|
||||
// case EventTypes.Message:
|
||||
// Pangea#
|
||||
case EventTypes.Encrypted:
|
||||
// #Pangea
|
||||
return _ButtonContent(
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () {},
|
||||
icon: '🔒',
|
||||
label: L10n.of(context)!.encrypted,
|
||||
fontSize: fontSize,
|
||||
);
|
||||
case EventTypes.Message:
|
||||
// Pangea#
|
||||
case EventTypes.Sticker:
|
||||
switch (event.messageType) {
|
||||
case MessageTypes.Image:
|
||||
return ImageBubble(
|
||||
event,
|
||||
width: 400,
|
||||
height: 300,
|
||||
fit: BoxFit.cover,
|
||||
borderRadius: borderRadius,
|
||||
);
|
||||
case MessageTypes.Sticker:
|
||||
if (event.redacted) continue textmessage;
|
||||
return Sticker(event);
|
||||
const maxSize = 256.0;
|
||||
final w = event.content
|
||||
.tryGetMap<String, Object?>('info')
|
||||
?.tryGet<int>('w');
|
||||
final h = event.content
|
||||
.tryGetMap<String, Object?>('info')
|
||||
?.tryGet<int>('h');
|
||||
var width = maxSize;
|
||||
var height = maxSize;
|
||||
var fit = event.messageType == MessageTypes.Sticker
|
||||
? BoxFit.contain
|
||||
: BoxFit.cover;
|
||||
if (w != null && h != null) {
|
||||
fit = BoxFit.contain;
|
||||
if (w > h) {
|
||||
width = maxSize;
|
||||
height = max(32, maxSize * (h / w));
|
||||
} else {
|
||||
height = maxSize;
|
||||
width = max(32, maxSize * (w / h));
|
||||
}
|
||||
}
|
||||
return ImageBubble(
|
||||
event,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
borderRadius: borderRadius,
|
||||
);
|
||||
case CuteEventContent.eventType:
|
||||
return CuteContent(event);
|
||||
case MessageTypes.Audio:
|
||||
|
|
@ -191,14 +221,16 @@ class MessageContent extends StatelessWidget {
|
|||
// else we fall through to the normal message rendering
|
||||
continue textmessage;
|
||||
case MessageTypes.BadEncrypted:
|
||||
case EventTypes.Encrypted:
|
||||
return _ButtonContent(
|
||||
textColor: buttonTextColor,
|
||||
onPressed: () => _verifyOrRequestKey(context),
|
||||
icon: '🔒',
|
||||
label: L10n.of(context)!.encrypted,
|
||||
fontSize: fontSize,
|
||||
);
|
||||
// #Pangea
|
||||
// case EventTypes.Encrypted:
|
||||
// return _ButtonContent(
|
||||
// textColor: buttonTextColor,
|
||||
// onPressed: () => _verifyOrRequestKey(context),
|
||||
// icon: '🔒',
|
||||
// label: L10n.of(context)!.encrypted,
|
||||
// fontSize: fontSize,
|
||||
// );
|
||||
// Pangea#
|
||||
case MessageTypes.Location:
|
||||
final geoUri =
|
||||
Uri.tryParse(event.content.tryGet<String>('geo_uri')!);
|
||||
|
|
|
|||
|
|
@ -43,9 +43,11 @@ class MessageReactions extends StatelessWidget {
|
|||
|
||||
final reactionList = reactionMap.values.toList();
|
||||
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
|
||||
final ownMessage = event.senderId == event.room.client.userID;
|
||||
return Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
alignment: ownMessage ? WrapAlignment.end : WrapAlignment.start,
|
||||
children: [
|
||||
...reactionList.map(
|
||||
(r) => _Reaction(
|
||||
|
|
@ -77,8 +79,8 @@ class MessageReactions extends StatelessWidget {
|
|||
),
|
||||
if (allReactionEvents.any((e) => e.status.isSending))
|
||||
const SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
||||
|
|
@ -91,17 +93,17 @@ class MessageReactions extends StatelessWidget {
|
|||
|
||||
class _Reaction extends StatelessWidget {
|
||||
final String? reactionKey;
|
||||
final int? count;
|
||||
final int count;
|
||||
final bool? reacted;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onLongPress;
|
||||
|
||||
const _Reaction({
|
||||
this.reactionKey,
|
||||
this.count,
|
||||
this.reacted,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
required this.reactionKey,
|
||||
required this.count,
|
||||
required this.reacted,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -109,7 +111,7 @@ class _Reaction extends StatelessWidget {
|
|||
final textColor = Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.white
|
||||
: Colors.black;
|
||||
final color = Theme.of(context).scaffoldBackgroundColor;
|
||||
final color = Theme.of(context).colorScheme.background;
|
||||
final fontSize = DefaultTextStyle.of(context).style.fontSize;
|
||||
Widget content;
|
||||
if (reactionKey!.startsWith('mxc://')) {
|
||||
|
|
@ -121,14 +123,16 @@ class _Reaction extends StatelessWidget {
|
|||
width: 9999,
|
||||
height: fontSize,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
if (count > 1) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
} else {
|
||||
|
|
@ -137,7 +141,7 @@ class _Reaction extends StatelessWidget {
|
|||
renderKey = renderKey.getRange(0, 9) + Characters('…');
|
||||
}
|
||||
content = Text(
|
||||
'$renderKey $count',
|
||||
renderKey.toString() + (count > 1 ? ' $count' : ''),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
|
|
@ -147,19 +151,19 @@ class _Reaction extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: () => onTap != null ? onTap!() : null,
|
||||
onLongPress: () => onLongPress != null ? onLongPress!() : null,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: reacted!
|
||||
? Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: reacted!
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.primaryContainer,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ 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(
|
||||
|
|
@ -29,9 +31,16 @@ class ReplyContent extends StatelessWidget {
|
|||
final displayEvent =
|
||||
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
final color = ownMessage
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.background.withOpacity(0.33),
|
||||
color: backgroundColor ??
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(ownMessage ? 0.2 : 0.33),
|
||||
borderRadius: borderRadius,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
|
@ -39,7 +48,7 @@ class ReplyContent extends StatelessWidget {
|
|||
Container(
|
||||
width: 3,
|
||||
height: fontSize * 2 + 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
|
|
@ -56,7 +65,7 @@ class ReplyContent extends StatelessWidget {
|
|||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
);
|
||||
|
|
@ -72,7 +81,7 @@ class ReplyContent extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: fontSize,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,15 +13,12 @@ class StateMessage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
|
|
@ -34,8 +31,7 @@ class StateMessage extends StatelessWidget {
|
|||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14 * AppConfig.fontSizeFactor,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
fontSize: 12 * AppConfig.fontSizeFactor,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
import 'image_bubble.dart';
|
||||
|
||||
class Sticker extends StatefulWidget {
|
||||
final Event event;
|
||||
|
||||
const Sticker(this.event, {super.key});
|
||||
|
||||
@override
|
||||
StickerState createState() => StickerState();
|
||||
}
|
||||
|
||||
class StickerState extends State<Sticker> {
|
||||
bool? animated;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImageBubble(
|
||||
widget.event,
|
||||
width: 400,
|
||||
height: 400,
|
||||
fit: BoxFit.contain,
|
||||
onTap: () {
|
||||
setState(() => animated = true);
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
message: widget.event.body,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
);
|
||||
},
|
||||
animated: animated ?? AppConfig.autoplayImages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -14,6 +13,7 @@ import 'package:video_player/video_player.dart';
|
|||
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
|
||||
import 'package:fluffychat/widgets/blur_hash.dart';
|
||||
import '../../../utils/error_reporter.dart';
|
||||
|
||||
class EventVideoPlayer extends StatefulWidget {
|
||||
|
|
@ -112,7 +112,7 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
|
|||
),
|
||||
)
|
||||
else
|
||||
BlurHash(hash: blurHash),
|
||||
BlurHash(blurhash: blurHash, width: 300, height: 300),
|
||||
Center(
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import 'package:emojis/emoji.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:emojis/emoji.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/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class InputBar extends StatelessWidget {
|
||||
|
|
@ -455,16 +455,34 @@ class InputBar extends StatelessWidget {
|
|||
link: controller!.choreographer.inputLayerLinkAndKey.link,
|
||||
// Pangea#
|
||||
child: TypeAheadField<Map<String, String?>>(
|
||||
direction: AxisDirection.up,
|
||||
direction: VerticalDirection.up,
|
||||
hideOnEmpty: true,
|
||||
hideOnLoading: true,
|
||||
keepSuggestionsOnSuggestionSelected: true,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
hideOnSelect: false,
|
||||
debounceDuration: const Duration(milliseconds: 50),
|
||||
// show suggestions after 50ms idle time (default is 300)
|
||||
// #Pangea
|
||||
key: controller!.choreographer.inputLayerLinkAndKey.key,
|
||||
// Pangea#
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
// builder: (context, controller, focusNode) => TextField(
|
||||
builder: (context, _, focusNode) => TextField(
|
||||
// Pangea#
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
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,
|
||||
).detectFileType;
|
||||
room.sendFileEvent(file, shrinkImageMaxDimension: 1600);
|
||||
},
|
||||
),
|
||||
minLines: minLines,
|
||||
maxLines: maxLines,
|
||||
keyboardType: keyboardType!,
|
||||
|
|
@ -479,13 +497,11 @@ class InputBar extends StatelessWidget {
|
|||
onTap: () {
|
||||
controller!.onInputTap(
|
||||
context,
|
||||
fNode: focusNode!,
|
||||
fNode: focusNode,
|
||||
);
|
||||
},
|
||||
// Pangea#
|
||||
controller: controller,
|
||||
decoration: decoration!,
|
||||
focusNode: focusNode,
|
||||
onChanged: (text) {
|
||||
// fix for the library for now
|
||||
// it sets the types for the callback incorrectly
|
||||
|
|
@ -496,13 +512,13 @@ class InputBar extends StatelessWidget {
|
|||
suggestionsCallback: getSuggestions,
|
||||
itemBuilder: (c, s) =>
|
||||
buildSuggestion(c, s, Matrix.of(context).client),
|
||||
onSuggestionSelected: (Map<String, String?> suggestion) =>
|
||||
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
|
||||
noItemsFoundBuilder: (BuildContext context) => const SizedBox
|
||||
emptyBuilder: (BuildContext context) => const SizedBox
|
||||
.shrink(), // fix loading briefly showing no suggestions
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
|
||||
class PinnedEvents extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -65,80 +63,32 @@ class PinnedEvents extends StatelessWidget {
|
|||
future: controller.room.getEventById(pinnedEventIds.last),
|
||||
builder: (context, snapshot) {
|
||||
final event = snapshot.data;
|
||||
|
||||
if (event == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
shape: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
return FutureBuilder<String>(
|
||||
future: event?.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _displayPinnedEventsDialog(context),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
splashRadius: 20,
|
||||
iconSize: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
tooltip: L10n.of(context)!.unpin,
|
||||
onPressed:
|
||||
controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
|
||||
? () => controller.unpinEvent(event.eventId)
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FutureBuilder<String>(
|
||||
future: event.calcLocalizedBody(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
return Linkify(
|
||||
text: snapshot.data ??
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: fontSize,
|
||||
decoration: event.redacted
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
builder: (context, snapshot) => ChatAppBarListTile(
|
||||
title: snapshot.data ??
|
||||
event?.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
withSenderNamePrefix: true,
|
||||
hideReply: true,
|
||||
) ??
|
||||
L10n.of(context)!.loadingPleaseWait,
|
||||
leading: IconButton(
|
||||
splashRadius: 20,
|
||||
iconSize: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
icon: const Icon(Icons.push_pin),
|
||||
tooltip: L10n.of(context)!.unpin,
|
||||
onPressed:
|
||||
controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
|
||||
? () => controller.unpinEvent(event!.eventId)
|
||||
: null,
|
||||
),
|
||||
onTap: () => _displayPinnedEventsDialog(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class ReactionsPicker extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
|
|
@ -92,7 +92,7 @@ class ReactionsPicker extends StatelessWidget {
|
|||
width: 36,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.add_outlined),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
|
|||
import 'events/audio_player.dart';
|
||||
|
||||
class RecordingDialog extends StatefulWidget {
|
||||
static const String recordingFileType = 'm4a';
|
||||
static const String recordingFileType = 'wav';
|
||||
const RecordingDialog({
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -27,10 +27,7 @@ class RecordingDialogState extends State<RecordingDialog> {
|
|||
|
||||
bool error = false;
|
||||
String? _recordedPath;
|
||||
// #Pangea
|
||||
// final _audioRecorder = Record();
|
||||
final _audioRecorder = AudioRecorder();
|
||||
// Pangea#
|
||||
final _audioRecorder = Record();
|
||||
final List<double> amplitudeTimeline = [];
|
||||
|
||||
static const int bitRate = 64000;
|
||||
|
|
@ -48,28 +45,12 @@ class RecordingDialogState extends State<RecordingDialog> {
|
|||
return;
|
||||
}
|
||||
await WakelockPlus.enable();
|
||||
|
||||
// We try to pick Opus where supported, since that is a codec optimized
|
||||
// for speech as well as what the voice messages MSC uses.
|
||||
final audioCodec =
|
||||
(await _audioRecorder.isEncoderSupported(AudioEncoder.opus))
|
||||
? AudioEncoder.opus
|
||||
: AudioEncoder.aacLc;
|
||||
// #Pangea
|
||||
// await _audioRecorder.start(
|
||||
// path: _recordedPath,
|
||||
// bitRate: bitRate,
|
||||
// samplingRate: samplingRate,
|
||||
// );
|
||||
await _audioRecorder.start(
|
||||
RecordConfig(
|
||||
encoder: audioCodec,
|
||||
bitRate: bitRate,
|
||||
// samplingRate: samplingRate,
|
||||
),
|
||||
path: _recordedPath!,
|
||||
path: _recordedPath,
|
||||
bitRate: bitRate,
|
||||
samplingRate: samplingRate,
|
||||
encoder: AudioEncoder.wav,
|
||||
);
|
||||
// Pangea#
|
||||
setState(() => _duration = Duration.zero);
|
||||
_recorderSubscription?.cancel();
|
||||
_recorderSubscription =
|
||||
|
|
|
|||
|
|
@ -21,29 +21,28 @@ class ReplyDisplay extends StatelessWidget {
|
|||
? 56
|
||||
: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Material(
|
||||
color: Theme.of(context).secondaryHeaderColor,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.close,
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.cancelReplyEventAction,
|
||||
),
|
||||
Expanded(
|
||||
child: controller.replyEvent != null
|
||||
? ReplyContent(
|
||||
controller.replyEvent!,
|
||||
timeline: controller.timeline!,
|
||||
)
|
||||
: _EditContent(
|
||||
controller.editEvent
|
||||
?.getDisplayEvent(controller.timeline!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
tooltip: L10n.of(context)!.close,
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.cancelReplyEventAction,
|
||||
),
|
||||
Expanded(
|
||||
child: controller.replyEvent != null
|
||||
? ReplyContent(
|
||||
controller.replyEvent!,
|
||||
timeline: controller.timeline!,
|
||||
backgroundColor: Colors.transparent,
|
||||
)
|
||||
: _EditContent(
|
||||
controller.editEvent?.getDisplayEvent(controller.timeline!),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
file = await file.resizeVideo();
|
||||
file = origImage ? file : await file.resizeVideo();
|
||||
thumbnail = await file.getVideoThumbnail();
|
||||
},
|
||||
);
|
||||
|
|
@ -126,6 +126,38 @@ class SendFileDialogState extends State<SendFileDialog> {
|
|||
),
|
||||
],
|
||||
);
|
||||
} else if (widget.files.every((file) => file is MatrixVideoFile)) {
|
||||
contentWidget = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(fileName),
|
||||
const SizedBox(height: 16),
|
||||
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CupertinoSwitch(
|
||||
value: origImage,
|
||||
onChanged: (v) => setState(() => origImage = v),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.sendOriginal,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(sizeString),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
contentWidget = Text('$fileName ($sizeString)');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../widgets/avatar.dart';
|
||||
import 'events/image_bubble.dart';
|
||||
|
||||
class StickerPickerDialog extends StatefulWidget {
|
||||
final Room room;
|
||||
final void Function(ImagePackImageContent) onSelected;
|
||||
|
||||
const StickerPickerDialog({required this.room, super.key});
|
||||
const StickerPickerDialog({
|
||||
required this.onSelected,
|
||||
required this.room,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
StickerPickerDialogState createState() => StickerPickerDialogState();
|
||||
|
|
@ -58,24 +63,14 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||
GridView.builder(
|
||||
itemCount: imageKeys.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 100,
|
||||
maxCrossAxisExtent: 128,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int imageIndex) {
|
||||
final image = pack.images[imageKeys[imageIndex]]!;
|
||||
final fakeEvent = Event(
|
||||
type: EventTypes.Sticker,
|
||||
content: {
|
||||
'url': image.url.toString(),
|
||||
'info': image.info,
|
||||
},
|
||||
originServerTs: DateTime.now(),
|
||||
room: widget.room,
|
||||
eventId: 'fake_event',
|
||||
senderId: widget.room.client.userID!,
|
||||
);
|
||||
return InkWell(
|
||||
radius: AppConfig.borderRadius,
|
||||
key: ValueKey(image.url.toString()),
|
||||
onTap: () {
|
||||
// copy the image
|
||||
|
|
@ -83,17 +78,16 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||
ImagePackImageContent.fromJson(image.toJson().copy());
|
||||
// set the body, if it doesn't exist, to the key
|
||||
imageCopy.body ??= imageKeys[imageIndex];
|
||||
Navigator.of(context, rootNavigator: false)
|
||||
.pop<ImagePackImageContent>(imageCopy);
|
||||
widget.onSelected(imageCopy);
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
absorbing: true,
|
||||
child: ImageBubble(
|
||||
fakeEvent,
|
||||
tapToView: false,
|
||||
child: MxcImage(
|
||||
uri: image.url,
|
||||
fit: BoxFit.contain,
|
||||
width: 100,
|
||||
height: 100,
|
||||
width: 128,
|
||||
height: 128,
|
||||
animated: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -104,6 +98,7 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||
};
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.onInverseSurface,
|
||||
body: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: CustomScrollView(
|
||||
|
|
@ -112,28 +107,49 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
|
|||
floating: true,
|
||||
pinned: true,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
backgroundColor: Theme.of(context).dialogBackgroundColor,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context, rootNavigator: false).pop,
|
||||
),
|
||||
title: TextField(
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.search,
|
||||
suffix: const Icon(Icons.search_outlined),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
backgroundColor: Colors.transparent,
|
||||
title: SizedBox(
|
||||
height: 42,
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.search,
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onChanged: (s) => setState(() => searchFilter = s),
|
||||
),
|
||||
onChanged: (s) => setState(() => searchFilter = s),
|
||||
),
|
||||
),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
packBuilder,
|
||||
childCount: packSlugs.length,
|
||||
if (packSlugs.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(L10n.of(context)!.noEmotesFound),
|
||||
// #Pangea
|
||||
// const SizedBox(height: 12),
|
||||
// OutlinedButton.icon(
|
||||
// onPressed: () => UrlLauncher(
|
||||
// context,
|
||||
// 'https://matrix.to/#/#fluffychat-stickers:janian.de',
|
||||
// ).launchUrl(),
|
||||
// icon: const Icon(Icons.explore_outlined),
|
||||
// label: Text(L10n.of(context)!.discover),
|
||||
// ),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
packBuilder,
|
||||
childCount: packSlugs.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'chat.dart';
|
||||
|
||||
class TombstoneDisplay extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
const TombstoneDisplay(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.room.getState(EventTypes.RoomTombstone) == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 72,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
elevation: 1,
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: const Icon(Icons.upgrade_outlined),
|
||||
),
|
||||
title: Text(
|
||||
controller.room
|
||||
.getState(EventTypes.RoomTombstone)!
|
||||
.parsedTombstoneContent
|
||||
.body,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
subtitle: Text(L10n.of(context)!.goToTheNewRoom),
|
||||
onTap: controller.goToNewRoomAction,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -70,10 +72,7 @@ class TypingIndicators extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: topPadding),
|
||||
child: Material(
|
||||
color: Theme.of(context).appBarTheme.backgroundColor,
|
||||
elevation: 6,
|
||||
shadowColor:
|
||||
Theme.of(context).secondaryHeaderColor.withAlpha(100),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
|
|
@ -81,14 +80,8 @@ class TypingIndicators extends StatelessWidget {
|
|||
bottomRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: typingUsers.isEmpty
|
||||
? null
|
||||
: Image.asset(
|
||||
'assets/typing.gif',
|
||||
height: 30,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: typingUsers.isEmpty ? null : const _TypingDots(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -98,3 +91,66 @@ class TypingIndicators extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TypingDots extends StatefulWidget {
|
||||
const _TypingDots();
|
||||
|
||||
@override
|
||||
State<_TypingDots> createState() => __TypingDotsState();
|
||||
}
|
||||
|
||||
class __TypingDotsState extends State<_TypingDots> {
|
||||
int _tick = 0;
|
||||
|
||||
late final Timer _timer;
|
||||
|
||||
static const Duration animationDuration = Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_timer = Timer.periodic(
|
||||
animationDuration,
|
||||
(_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_tick = (_tick + 1) % 4;
|
||||
});
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const size = 8.0;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 1; i <= 3; i++)
|
||||
AnimatedContainer(
|
||||
duration: animationDuration * 1.5,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
width: size,
|
||||
height: _tick == i ? size * 2 : size,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 2,
|
||||
vertical: _tick == i ? 4 : 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(size * 2),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ enum AliasActions { copy, delete, setCanonical }
|
|||
|
||||
class ChatDetails extends StatefulWidget {
|
||||
final String roomId;
|
||||
final Widget? embeddedCloseButton;
|
||||
|
||||
const ChatDetails({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
this.embeddedCloseButton,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -84,34 +86,19 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
void editAliases() async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||
|
||||
// The current endpoint doesnt seem to be implemented in Synapse. This may
|
||||
// change in the future and then we just need to switch to this api call:
|
||||
//
|
||||
// final aliases = await showFutureLoadingDialog(
|
||||
// context: context,
|
||||
// future: () => room.client.requestRoomAliases(room.id),
|
||||
// );
|
||||
//
|
||||
// While this is not working we use the unstable api:
|
||||
final aliases = await showFutureLoadingDialog(
|
||||
final aliasesResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room!.client
|
||||
.request(
|
||||
RequestType.GET,
|
||||
'/client/unstable/org.matrix.msc2432/rooms/${Uri.encodeComponent(room.id)}/aliases',
|
||||
)
|
||||
.then(
|
||||
(response) => List<String>.from(response['aliases'] as Iterable),
|
||||
),
|
||||
future: () => room!.client.getLocalAliases(room.id),
|
||||
);
|
||||
// Switch to the stable api once it is implemented.
|
||||
|
||||
if (aliases.error != null) return;
|
||||
final adminMode = room!.canSendEvent('m.room.canonical_alias');
|
||||
if (aliases.result!.isEmpty && (room.canonicalAlias.isNotEmpty)) {
|
||||
aliases.result!.add(room.canonicalAlias);
|
||||
final aliases = aliasesResult.result;
|
||||
|
||||
if (aliases == null) return;
|
||||
final adminMode = room!.canSendEvent(EventTypes.RoomCanonicalAlias);
|
||||
if (aliases.isEmpty && (room.canonicalAlias.isNotEmpty)) {
|
||||
aliases.add(room.canonicalAlias);
|
||||
}
|
||||
if (aliases.result!.isEmpty && adminMode) {
|
||||
if (aliases.isEmpty && adminMode) {
|
||||
return setAliasAction();
|
||||
}
|
||||
final select = await showConfirmationDialog(
|
||||
|
|
@ -123,8 +110,7 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
actions: [
|
||||
if (adminMode)
|
||||
AlertDialogAction(label: L10n.of(context)!.create, key: 'new'),
|
||||
...aliases.result!
|
||||
.map((alias) => AlertDialogAction(key: alias, label: alias)),
|
||||
...aliases.map((alias) => AlertDialogAction(key: alias, label: alias)),
|
||||
],
|
||||
);
|
||||
if (select == null) return;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ class ChatDetailsView extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final isEmbedded = GoRouterState.of(context).fullPath == '/rooms/:roomid';
|
||||
|
||||
return StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
|
|
@ -62,36 +60,35 @@ class ChatDetailsView extends StatelessWidget {
|
|||
MatrixLocals(L10n.of(context)!),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: isEmbedded
|
||||
? null
|
||||
: AppBar(
|
||||
leading: const Center(child: BackButton()),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
actions: <Widget>[
|
||||
// #Pangeas
|
||||
// if (room.canonicalAlias.isNotEmpty)
|
||||
// IconButton(
|
||||
// tooltip: L10n.of(context)!.share,
|
||||
// icon: Icon(Icons.adaptive.share_outlined),
|
||||
// onPressed: () => FluffyShare.share(
|
||||
// AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
// context,
|
||||
// ),
|
||||
// ),
|
||||
if (!room.isSpace)
|
||||
// Pangea#
|
||||
ChatSettingsPopupMenu(room, false),
|
||||
],
|
||||
// #Pangea
|
||||
title: ClassNameHeader(
|
||||
controller: controller,
|
||||
room: room,
|
||||
),
|
||||
// title: Text(L10n.of(context)!.chatDetails),
|
||||
// Pangea#
|
||||
backgroundColor:
|
||||
Theme.of(context).appBarTheme.backgroundColor,
|
||||
),
|
||||
appBar: AppBar(
|
||||
leading: controller.widget.embeddedCloseButton ??
|
||||
const Center(child: BackButton()),
|
||||
elevation: Theme.of(context).appBarTheme.elevation,
|
||||
actions: <Widget>[
|
||||
// #Pangeas
|
||||
//if (room.canonicalAlias.isNotEmpty)
|
||||
// IconButton(
|
||||
// tooltip: L10n.of(context)!.share,
|
||||
// icon: Icon(Icons.adaptive.share_outlined),
|
||||
// onPressed: () => FluffyShare.share(
|
||||
// L10n.of(context)!.youInvitedToBy(
|
||||
// AppConfig.inviteLinkPrefix + room.canonicalAlias,
|
||||
// ),
|
||||
// context,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.widget.embeddedCloseButton == null)
|
||||
ChatSettingsPopupMenu(room, false),
|
||||
],
|
||||
// #Pangea
|
||||
title: ClassNameHeader(
|
||||
controller: controller,
|
||||
room: room,
|
||||
),
|
||||
// title: Text(L10n.of(context)!.chatDetails),
|
||||
// Pangea#
|
||||
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
|
|
@ -124,7 +121,9 @@ class ChatDetailsView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
child: Hero(
|
||||
tag: isEmbedded
|
||||
tag: controller
|
||||
.widget.embeddedCloseButton !=
|
||||
null
|
||||
? 'embedded_content_banner'
|
||||
: 'content_banner',
|
||||
child: Avatar(
|
||||
|
|
@ -511,9 +510,7 @@ class ChatDetailsView extends StatelessWidget {
|
|||
: AddToClassMode.chat,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (!room.isDirectChat &&
|
||||
(!room.isSpace ||
|
||||
(room.isSpace && room.isRoomAdmin)))
|
||||
if (!room.isDirectChat)
|
||||
ListTile(
|
||||
title: Text(
|
||||
room.isSpace
|
||||
|
|
|
|||
|
|
@ -70,6 +70,15 @@ class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
|||
}
|
||||
|
||||
void startVerification() async {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.verifyOtherUser,
|
||||
message: L10n.of(context)!.verifyOtherUserDescription,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
fullyCapitalizedForMaterial: false,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
|
||||
.startVerification();
|
||||
req.onUpdate = () {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/add_to_space.dart';
|
||||
|
|
@ -23,6 +22,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_shortcuts/flutter_shortcuts.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -30,6 +30,7 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|||
import 'package:uni_links/uni_links.dart';
|
||||
|
||||
import '../../../utils/account_bundles.dart';
|
||||
import '../../config/setting_keys.dart';
|
||||
import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import '../../utils/url_launcher.dart';
|
||||
import '../../utils/voip/callkeep_manager.dart';
|
||||
|
|
@ -281,12 +282,34 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
SearchUserDirectoryResponse? userSearchResult;
|
||||
QueryPublicRoomsResponse? roomSearchResult;
|
||||
final searchQuery = searchController.text.trim();
|
||||
try {
|
||||
roomSearchResult = await client.queryPublicRooms(
|
||||
server: searchServer,
|
||||
filter: PublicRoomQueryFilter(genericSearchTerm: searchController.text),
|
||||
filter: PublicRoomQueryFilter(genericSearchTerm: searchQuery),
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
if (searchQuery.isValidMatrixId &&
|
||||
searchQuery.sigil == '#' &&
|
||||
roomSearchResult.chunk
|
||||
.any((room) => room.canonicalAlias == searchQuery) ==
|
||||
false) {
|
||||
final response = await client.getRoomIdByAlias(searchQuery);
|
||||
final roomId = response.roomId;
|
||||
if (roomId != null) {
|
||||
roomSearchResult.chunk.add(
|
||||
PublicRoomsChunk(
|
||||
name: searchQuery,
|
||||
guestCanJoin: false,
|
||||
numJoinedMembers: 0,
|
||||
roomId: roomId,
|
||||
worldReadable: false,
|
||||
canonicalAlias: searchQuery,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
userSearchResult = await client.searchUserDirectory(
|
||||
searchController.text,
|
||||
limit: 20,
|
||||
|
|
@ -309,7 +332,7 @@ class ChatListController extends State<ChatList>
|
|||
});
|
||||
}
|
||||
|
||||
void onSearchEnter(String text) {
|
||||
void onSearchEnter(String text, {bool globalSearch = true}) {
|
||||
if (text.isEmpty) {
|
||||
cancelSearch(unfocus: false);
|
||||
return;
|
||||
|
|
@ -319,7 +342,9 @@ class ChatListController extends State<ChatList>
|
|||
isSearchMode = true;
|
||||
});
|
||||
_coolDown?.cancel();
|
||||
_coolDown = Timer(const Duration(milliseconds: 500), _search);
|
||||
if (globalSearch) {
|
||||
_coolDown = Timer(const Duration(milliseconds: 500), _search);
|
||||
}
|
||||
}
|
||||
|
||||
void startSearch() {
|
||||
|
|
@ -445,6 +470,16 @@ class ChatListController extends State<ChatList>
|
|||
FluffyChatApp.gotInitialLink = true;
|
||||
getInitialLink().then(_processIncomingUris);
|
||||
}
|
||||
|
||||
if (PlatformInfos.isAndroid) {
|
||||
final shortcuts = FlutterShortcuts();
|
||||
shortcuts.initialize().then(
|
||||
(_) => shortcuts.listenAction((action) {
|
||||
if (!mounted) return;
|
||||
UrlLauncher(context, action).launchUrl();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//#Pangea
|
||||
|
|
@ -620,6 +655,18 @@ class ChatListController extends State<ChatList>
|
|||
// Pangea#
|
||||
}
|
||||
|
||||
void dismissStatusList() async {
|
||||
final result = await showOkCancelAlertDialog(
|
||||
title: L10n.of(context)!.hidePresences,
|
||||
context: context,
|
||||
);
|
||||
if (result == OkCancelResult.ok) {
|
||||
await Matrix.of(context).store.setBool(SettingKeys.showPresences, false);
|
||||
AppConfig.showPresences = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void setStatus() async {
|
||||
final client = Matrix.of(context).client;
|
||||
final currentPresence = await client.fetchCurrentPresence(client.userID!);
|
||||
|
|
@ -746,6 +793,7 @@ class ChatListController extends State<ChatList>
|
|||
final client = Matrix.of(context).client;
|
||||
await client.roomsLoading;
|
||||
await client.accountDataLoading;
|
||||
await client.userDeviceKeysLoading;
|
||||
if (client.prevBatch == null) {
|
||||
await client.onSync.stream.first;
|
||||
// #Pangea
|
||||
|
|
@ -770,6 +818,7 @@ class ChatListController extends State<ChatList>
|
|||
await pangeaController.subscriptionController.initialize();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
} else {
|
||||
ErrorHandler.logError(
|
||||
m: "didn't run afterSyncAndFirstLoginInitialization because not mounted",
|
||||
|
|
@ -896,8 +945,7 @@ class ChatListController extends State<ChatList>
|
|||
isTorBrowser = isTor;
|
||||
}
|
||||
|
||||
Future<void> dehydrate() =>
|
||||
SettingsSecurityController.dehydrateDevice(context);
|
||||
Future<void> dehydrate() => Matrix.of(context).dehydrateAction();
|
||||
}
|
||||
|
||||
enum EditBundleAction { addToBundle, removeFromBundle }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
|
|
@ -130,10 +131,14 @@ class ChatListViewBody extends StatelessWidget {
|
|||
],
|
||||
// #Pangea
|
||||
// if (!controller.isSearchMode &&
|
||||
// controller.activeFilter != ActiveFilter.groups)
|
||||
// StatusMessageList(
|
||||
// onStatusEdit: controller.setStatus,
|
||||
// ),
|
||||
// controller.activeFilter != ActiveFilter.groups &&
|
||||
// AppConfig.showPresences)
|
||||
// GestureDetector(
|
||||
// onLongPress: () => controller.dismissStatusList(),
|
||||
// child: StatusMessageList(
|
||||
// onStatusEdit: controller.setStatus,
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
const ConnectionStatusHeader(),
|
||||
AnimatedContainer(
|
||||
|
|
@ -253,6 +258,7 @@ class ChatListViewBody extends StatelessWidget {
|
|||
)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final activeChat = controller.activeChat == rooms[i].id;
|
||||
return ChatListItem(
|
||||
rooms[i],
|
||||
key: Key('chat_list_item_${rooms[i].id}'),
|
||||
|
|
@ -260,10 +266,10 @@ class ChatListViewBody extends StatelessWidget {
|
|||
controller.selectedRoomIds.contains(rooms[i].id),
|
||||
onTap: controller.selectMode == SelectMode.select
|
||||
? () => controller.toggleSelection(rooms[i].id)
|
||||
: null,
|
||||
: () => onChatTap(rooms[i], context),
|
||||
onLongPress: () =>
|
||||
controller.toggleSelection(rooms[i].id),
|
||||
activeChat: controller.activeChat == rooms[i].id,
|
||||
activeChat: activeChat,
|
||||
);
|
||||
},
|
||||
childCount: rooms.length,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
|
||||
class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ChatListController controller;
|
||||
final bool globalSearch;
|
||||
|
||||
const ChatListHeader({super.key, required this.controller});
|
||||
const ChatListHeader({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.globalSearch = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -140,8 +145,8 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
tooltip: L10n.of(context)!.toggleUnread,
|
||||
icon: Icon(
|
||||
controller.anySelectedRoomNotMarkedUnread
|
||||
? Icons.mark_chat_read_outlined
|
||||
: Icons.mark_chat_unread_outlined,
|
||||
? Icons.mark_chat_unread_outlined
|
||||
: Icons.mark_chat_read_outlined,
|
||||
),
|
||||
onPressed: controller.toggleUnread,
|
||||
),
|
||||
|
|
@ -149,8 +154,8 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||
tooltip: L10n.of(context)!.toggleFavorite,
|
||||
icon: Icon(
|
||||
controller.anySelectedRoomNotFavorite
|
||||
? Icons.push_pin_outlined
|
||||
: Icons.push_pin,
|
||||
? Icons.push_pin
|
||||
: Icons.push_pin_outlined,
|
||||
),
|
||||
onPressed: controller.toggleFavouriteRoom,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ import 'package:fluffychat/widgets/hover_builder.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../config/themes.dart';
|
||||
import '../../utils/date_time_extension.dart';
|
||||
import '../../widgets/avatar.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../chat/send_file_dialog.dart';
|
||||
|
||||
enum ArchivedRoomAction { delete, rejoin }
|
||||
|
||||
|
|
@ -23,142 +21,20 @@ class ChatListItem extends StatelessWidget {
|
|||
final Room room;
|
||||
final bool activeChat;
|
||||
final bool selected;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onLongPress;
|
||||
final void Function()? onForget;
|
||||
final void Function() onTap;
|
||||
|
||||
const ChatListItem(
|
||||
this.room, {
|
||||
this.activeChat = false,
|
||||
this.selected = false,
|
||||
this.onTap,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.onForget,
|
||||
super.key,
|
||||
});
|
||||
|
||||
void clickAction(BuildContext context) async {
|
||||
if (onTap != null) return onTap!();
|
||||
if (activeChat) return;
|
||||
if (room.membership == Membership.invite) {
|
||||
final inviterId =
|
||||
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
|
||||
final inviteAction = await showModalActionSheet<InviteActions>(
|
||||
context: context,
|
||||
message: room.isDirectChat
|
||||
? L10n.of(context)!.invitePrivateChat
|
||||
: L10n.of(context)!.inviteGroupChat,
|
||||
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
actions: [
|
||||
SheetAction(
|
||||
key: InviteActions.accept,
|
||||
label: L10n.of(context)!.accept,
|
||||
icon: Icons.check_outlined,
|
||||
isDefaultAction: true,
|
||||
),
|
||||
SheetAction(
|
||||
key: InviteActions.decline,
|
||||
label: L10n.of(context)!.decline,
|
||||
icon: Icons.close_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
SheetAction(
|
||||
key: InviteActions.block,
|
||||
label: L10n.of(context)!.block,
|
||||
icon: Icons.block_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (inviteAction == null) return;
|
||||
if (inviteAction == InviteActions.block) {
|
||||
context.go('/rooms/settings/security/ignorelist', extra: inviterId);
|
||||
return;
|
||||
}
|
||||
if (inviteAction == InviteActions.decline) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.leave,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.ban) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.leave) {
|
||||
context.go('/rooms/archive/${room.id}');
|
||||
}
|
||||
|
||||
if (room.membership == Membership.join) {
|
||||
// Share content into this room
|
||||
// #Pangea
|
||||
// final shareContent = Matrix.of(context).shareContent;
|
||||
Map<String, dynamic>? shareContent;
|
||||
try {
|
||||
shareContent = Matrix.of(context).shareContent;
|
||||
} catch (e) {
|
||||
shareContent = null;
|
||||
}
|
||||
// Pangea#
|
||||
if (shareContent != null) {
|
||||
final shareFile = shareContent.tryGet<MatrixFile>('file');
|
||||
if (shareContent.tryGet<String>('msgtype') ==
|
||||
'chat.fluffy.shared_file' &&
|
||||
shareFile != null) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
files: [shareFile],
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
Matrix.of(context).shareContent = null;
|
||||
} else {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.forward,
|
||||
message: L10n.of(context)!.forwardMessageTo(
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
),
|
||||
okLabel: L10n.of(context)!.forward,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
);
|
||||
if (consent == OkCancelResult.cancel) {
|
||||
Matrix.of(context).shareContent = null;
|
||||
return;
|
||||
}
|
||||
if (consent == OkCancelResult.ok) {
|
||||
room.sendEvent(shareContent);
|
||||
Matrix.of(context).shareContent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.go('/rooms/${room.id}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> archiveAction(BuildContext context) async {
|
||||
{
|
||||
if ([Membership.leave, Membership.ban].contains(room.membership)) {
|
||||
|
|
@ -239,7 +115,7 @@ class ChatListItem extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: unread
|
||||
style: unread || room.hasNewMessages
|
||||
? const TextStyle(fontWeight: FontWeight.bold)
|
||||
: null,
|
||||
),
|
||||
|
|
@ -359,7 +235,9 @@ class ChatListItem extends StatelessWidget {
|
|||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: unread ? FontWeight.w600 : null,
|
||||
fontWeight: unread || room.hasNewMessages
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
|
|
@ -424,7 +302,7 @@ class ChatListItem extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
onTap: () => clickAction(context),
|
||||
onTap: onTap,
|
||||
trailing: onForget == null
|
||||
? hovered || selected
|
||||
? IconButton(
|
||||
|
|
@ -450,9 +328,3 @@ class ChatListItem extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum InviteActions {
|
||||
accept,
|
||||
decline,
|
||||
block,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension.dart';
|
||||
|
|
@ -7,12 +5,10 @@ import 'package:fluffychat/pangea/utils/class_code.dart';
|
|||
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
|
||||
import 'package:fluffychat/pangea/utils/logout.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../utils/fluffy_share.dart';
|
||||
|
|
@ -113,17 +109,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.findAClass,
|
||||
enabled: false,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.class_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Expanded(child: Text(L10n.of(context)!.findAClass)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.pangeaController.permissionsController.isUser18())
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.findAConversationPartner,
|
||||
|
|
@ -279,38 +264,40 @@ class ClientChooserButton extends StatelessWidget {
|
|||
builder: (context, snapshot) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
...List.generate(
|
||||
clientCount,
|
||||
(index) => KeyBoardShortcuts(
|
||||
keysToPress: _buildKeyboardShortcut(index + 1),
|
||||
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
|
||||
onKeysPressed: () => _handleKeyboardShortcut(
|
||||
matrix,
|
||||
index,
|
||||
context,
|
||||
),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.tab,
|
||||
},
|
||||
helpLabel: L10n.of(context)!.nextAccount,
|
||||
onKeysPressed: () => _nextAccount(matrix, context),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
KeyBoardShortcuts(
|
||||
keysToPress: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.shiftLeft,
|
||||
LogicalKeyboardKey.tab,
|
||||
},
|
||||
helpLabel: L10n.of(context)!.previousAccount,
|
||||
onKeysPressed: () => _previousAccount(matrix, context),
|
||||
child: const SizedBox.shrink(),
|
||||
),
|
||||
// #Pangea
|
||||
// ...List.generate(
|
||||
// clientCount,
|
||||
// (index) => KeyBoardShortcuts(
|
||||
// keysToPress: _buildKeyboardShortcut(index + 1),
|
||||
// helpLabel: L10n.of(context)!.switchToAccount(index + 1),
|
||||
// onKeysPressed: () => _handleKeyboardShortcut(
|
||||
// matrix,
|
||||
// index,
|
||||
// context,
|
||||
// ),
|
||||
// child: const SizedBox.shrink(),
|
||||
// ),
|
||||
// ),
|
||||
// KeyBoardShortcuts(
|
||||
// keysToPress: {
|
||||
// LogicalKeyboardKey.controlLeft,
|
||||
// LogicalKeyboardKey.tab,
|
||||
// },
|
||||
// helpLabel: L10n.of(context)!.nextAccount,
|
||||
// onKeysPressed: () => _nextAccount(matrix, context),
|
||||
// child: const SizedBox.shrink(),
|
||||
// ),
|
||||
// KeyBoardShortcuts(
|
||||
// keysToPress: {
|
||||
// LogicalKeyboardKey.controlLeft,
|
||||
// LogicalKeyboardKey.shiftLeft,
|
||||
// LogicalKeyboardKey.tab,
|
||||
// },
|
||||
// helpLabel: L10n.of(context)!.previousAccount,
|
||||
// onKeysPressed: () => _previousAccount(matrix, context),
|
||||
// child: const SizedBox.shrink(),
|
||||
// ),
|
||||
// Pangea#
|
||||
PopupMenuButton<Object>(
|
||||
onSelected: (o) => _clientSelected(o, context),
|
||||
itemBuilder: _bundleMenuItems,
|
||||
|
|
@ -406,7 +393,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
ClassCodeUtil.joinWithClassCodeDialog(
|
||||
context,
|
||||
controller.pangeaController,
|
||||
null,
|
||||
);
|
||||
break;
|
||||
case SettingsAction.findAConversationPartner:
|
||||
|
|
@ -421,9 +407,6 @@ class ClientChooserButton extends StatelessWidget {
|
|||
case SettingsAction.myAnalytics:
|
||||
context.go('/rooms/mylearning');
|
||||
break;
|
||||
case SettingsAction.findAClass:
|
||||
debugger(when: kDebugMode, message: "left to implement");
|
||||
break;
|
||||
case SettingsAction.logout:
|
||||
pLogoutAction(context);
|
||||
break;
|
||||
|
|
@ -514,7 +497,6 @@ enum SettingsAction {
|
|||
joinWithClassCode,
|
||||
classAnalytics,
|
||||
myAnalytics,
|
||||
findAClass,
|
||||
findAConversationPartner,
|
||||
logout,
|
||||
newClass,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/utils/on_chat_tap.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/sync_update_extension.dart';
|
||||
|
|
@ -44,6 +45,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
bool loading = false;
|
||||
// #Pangea
|
||||
StreamSubscription<SyncUpdate>? _roomSubscription;
|
||||
bool refreshing = false;
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
|
|
@ -55,21 +57,25 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// #Pangea
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_roomSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
void _refresh() {
|
||||
// #Pangea
|
||||
// _lastResponse.remove(widget.controller.activseSpaceId);
|
||||
if (mounted) {
|
||||
// Pangea#
|
||||
loadHierarchy();
|
||||
// #Pangea
|
||||
}
|
||||
// Pangea#
|
||||
loadHierarchy();
|
||||
}
|
||||
|
||||
Future<GetSpaceHierarchyResponse> loadHierarchy([String? prevBatch]) async {
|
||||
// #Pangea
|
||||
if (widget.controller.activeSpaceId == null) {
|
||||
if (widget.controller.activeSpaceId == null || loading) {
|
||||
return GetSpaceHierarchyResponse(
|
||||
rooms: [],
|
||||
nextBatch: null,
|
||||
|
|
@ -371,6 +377,34 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
_refresh();
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
Future<void> refreshOnUpdate(SyncUpdate event) async {
|
||||
/* refresh on leave, invite, and space child update
|
||||
not join events, because there's already a listener on
|
||||
onTapSpaceChild, and they interfere with each other */
|
||||
if (widget.controller.activeSpaceId == null || !mounted || refreshing) {
|
||||
return;
|
||||
}
|
||||
setState(() => refreshing = true);
|
||||
final client = Matrix.of(context).client;
|
||||
if (mounted &&
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.leave,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.invite,
|
||||
client.userID!,
|
||||
) ||
|
||||
event.isSpaceChildUpdate(
|
||||
widget.controller.activeSpaceId!,
|
||||
)) {
|
||||
await loadHierarchy();
|
||||
}
|
||||
setState(() => refreshing = false);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = Matrix.of(context).client;
|
||||
|
|
@ -385,11 +419,18 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final rootSpaces = allSpaces
|
||||
// #Pangea
|
||||
// .where(
|
||||
// (space) => !allSpaces.any(
|
||||
// (parentSpace) => parentSpace.spaceChildren
|
||||
// .any((child) => child.roomId == space.id),
|
||||
// ),
|
||||
// )
|
||||
// (space) =>
|
||||
// !allSpaces.any(
|
||||
// (parentSpace) => parentSpace.spaceChildren
|
||||
// .any((child) => child.roomId == space.id),
|
||||
// ) &&
|
||||
// space
|
||||
// .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
|
||||
// .toLowerCase()
|
||||
// .contains(
|
||||
// widget.controller.searchController.text.toLowerCase(),
|
||||
// ),
|
||||
//)
|
||||
// Pangea#
|
||||
.toList();
|
||||
|
||||
|
|
@ -468,23 +509,6 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
void refreshOnUpdate(SyncUpdate event) {
|
||||
/* refresh on leave, invite, and space child update
|
||||
not join events, because there's already a listener on
|
||||
onTapSpaceChild, and they interfere with each other */
|
||||
if (event.isMembershipUpdateByType(
|
||||
Membership.leave,
|
||||
Matrix.of(context).client.userID!,
|
||||
) ||
|
||||
event.isMembershipUpdateByType(
|
||||
Membership.invite,
|
||||
Matrix.of(context).client.userID!,
|
||||
) ||
|
||||
event.isSpaceChildUpdate(activeSpaceId)) {
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
_roomSubscription ??= client.onSync.stream
|
||||
.where((event) => event.hasRoomUpdate)
|
||||
.listen(refreshOnUpdate);
|
||||
|
|
@ -506,7 +530,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
child: CustomScrollView(
|
||||
controller: widget.scrollController,
|
||||
slivers: [
|
||||
ChatListHeader(controller: widget.controller),
|
||||
ChatListHeader(controller: widget.controller, globalSearch: false),
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
primary: false,
|
||||
|
|
@ -650,6 +674,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
onLongPress: () =>
|
||||
_onSpaceChildContextMenu(spaceChild, room),
|
||||
activeChat: widget.controller.activeChat == room.id,
|
||||
onTap: () => onChatTap(room, context),
|
||||
);
|
||||
}
|
||||
final isSpace = spaceChild.roomType == 'm.space';
|
||||
|
|
@ -719,7 +744,8 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
L10n.of(context)!.chat;
|
||||
if (widget.controller.isSearchMode &&
|
||||
!name.toLowerCase().contains(
|
||||
widget.controller.searchController.text,
|
||||
widget.controller.searchController.text
|
||||
.toLowerCase(),
|
||||
)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
|
@ -734,15 +760,20 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
// #Pangea
|
||||
// Expanded(
|
||||
// child:
|
||||
// Pangea#
|
||||
Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
// ),
|
||||
// Pangea#
|
||||
if (!isSpace) ...[
|
||||
const Icon(
|
||||
Icons.people_outline,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class PresenceAvatar extends StatelessWidget {
|
|||
final statusMsgBubbleElevation =
|
||||
Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4;
|
||||
final statusMsgBubbleShadowColor =
|
||||
Theme.of(context).appBarTheme.shadowColor;
|
||||
Theme.of(context).colorScheme.onBackground;
|
||||
final statusMsgBubbleColor = Colors.white.withAlpha(245);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
|
|
@ -187,53 +187,57 @@ class PresenceAvatar extends StatelessWidget {
|
|||
left: 0,
|
||||
top: 0,
|
||||
right: 8,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Material(
|
||||
elevation: statusMsgBubbleElevation,
|
||||
shadowColor: statusMsgBubbleShadowColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
color: statusMsgBubbleColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: Text(
|
||||
statusMsg,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 10.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
elevation: statusMsgBubbleElevation,
|
||||
shadowColor: statusMsgBubbleShadowColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
color: statusMsgBubbleColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: Text(
|
||||
statusMsg,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 10.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 26.0,
|
||||
top: 4.0,
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: Material(
|
||||
elevation:
|
||||
statusMsgBubbleElevation,
|
||||
shadowColor:
|
||||
statusMsgBubbleShadowColor,
|
||||
borderRadius:
|
||||
BorderRadius.circular(99),
|
||||
color: statusMsgBubbleColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
top: 32,
|
||||
child: Material(
|
||||
color: statusMsgBubbleColor,
|
||||
elevation: statusMsgBubbleElevation,
|
||||
shadowColor: statusMsgBubbleShadowColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: 8,
|
||||
height: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 14,
|
||||
top: 40,
|
||||
child: Material(
|
||||
color: statusMsgBubbleColor,
|
||||
elevation: statusMsgBubbleElevation,
|
||||
shadowColor: statusMsgBubbleShadowColor,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: 4,
|
||||
height: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
127
lib/pages/chat_list/utils/on_chat_tap.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/send_file_dialog.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
void onChatTap(Room room, BuildContext context) async {
|
||||
if (room.membership == Membership.invite) {
|
||||
final inviterId =
|
||||
room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId;
|
||||
final inviteAction = await showModalActionSheet<InviteActions>(
|
||||
context: context,
|
||||
message: room.isDirectChat
|
||||
? L10n.of(context)!.invitePrivateChat
|
||||
: L10n.of(context)!.inviteGroupChat,
|
||||
title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
actions: [
|
||||
SheetAction(
|
||||
key: InviteActions.accept,
|
||||
label: L10n.of(context)!.accept,
|
||||
icon: Icons.check_outlined,
|
||||
isDefaultAction: true,
|
||||
),
|
||||
SheetAction(
|
||||
key: InviteActions.decline,
|
||||
label: L10n.of(context)!.decline,
|
||||
icon: Icons.close_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
SheetAction(
|
||||
key: InviteActions.block,
|
||||
label: L10n.of(context)!.block,
|
||||
icon: Icons.block_outlined,
|
||||
isDestructiveAction: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (inviteAction == null) return;
|
||||
if (inviteAction == InviteActions.block) {
|
||||
context.go('/rooms/settings/security/ignorelist', extra: inviterId);
|
||||
return;
|
||||
}
|
||||
if (inviteAction == InviteActions.decline) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.leave,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.ban) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.youHaveBeenBannedFromThisChat),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.leave) {
|
||||
context.go('/rooms/archive/${room.id}');
|
||||
return;
|
||||
}
|
||||
|
||||
// Share content into this room
|
||||
final shareContent = Matrix.of(context).shareContent;
|
||||
if (shareContent != null) {
|
||||
final shareFile = shareContent.tryGet<MatrixFile>('file');
|
||||
if (shareContent.tryGet<String>('msgtype') == 'chat.fluffy.shared_file' &&
|
||||
shareFile != null) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (c) => SendFileDialog(
|
||||
files: [shareFile],
|
||||
room: room,
|
||||
),
|
||||
);
|
||||
Matrix.of(context).shareContent = null;
|
||||
} else {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.forward,
|
||||
message: L10n.of(context)!.forwardMessageTo(
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||
),
|
||||
okLabel: L10n.of(context)!.forward,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
);
|
||||
if (consent == OkCancelResult.cancel) {
|
||||
Matrix.of(context).shareContent = null;
|
||||
return;
|
||||
}
|
||||
if (consent == OkCancelResult.ok) {
|
||||
room.sendEvent(shareContent);
|
||||
Matrix.of(context).shareContent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.go('/rooms/${room.id}');
|
||||
}
|
||||
|
||||
enum InviteActions {
|
||||
accept,
|
||||
decline,
|
||||
block,
|
||||
}
|
||||
|
|
@ -31,14 +31,14 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
if (room == null) {
|
||||
return Center(child: Text(L10n.of(context)!.noRoomsFound));
|
||||
}
|
||||
final powerLevelsContent = Map<String, dynamic>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels)!.content,
|
||||
final powerLevelsContent = Map<String, Object?>.from(
|
||||
room.getState(EventTypes.RoomPowerLevels)?.content ?? {},
|
||||
);
|
||||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
..removeWhere((k, v) => v is! int);
|
||||
final eventsPowerLevels =
|
||||
Map<String, dynamic>.from(powerLevelsContent['events'] ?? {})
|
||||
..removeWhere((k, v) => v is! int);
|
||||
final eventsPowerLevels = Map<String, int?>.from(
|
||||
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
|
||||
)..removeWhere((k, v) => v is! int);
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
|
|
@ -67,9 +67,12 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
Builder(
|
||||
builder: (context) {
|
||||
const key = 'rooms';
|
||||
final int value = powerLevelsContent
|
||||
final value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent['notifications']['rooms'] ?? 0
|
||||
? powerLevelsContent
|
||||
.tryGetMap<String, Object?>('notifications')
|
||||
?.tryGet<int>('rooms') ??
|
||||
0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
|
|
@ -98,11 +101,11 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
category: 'events',
|
||||
permission: entry.value,
|
||||
permission: entry.value ?? 0,
|
||||
onTap: () => controller.editPowerLevel(
|
||||
context,
|
||||
entry.key,
|
||||
entry.value,
|
||||
entry.value ?? 0,
|
||||
category: 'events',
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -91,6 +91,15 @@ class DevicesSettingsController extends State<DevicesSettings> {
|
|||
}
|
||||
|
||||
void verifyDeviceAction(Device device) async {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.verifyOtherDevice,
|
||||
message: L10n.of(context)!.verifyOtherDeviceDescription,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
fullyCapitalizedForMaterial: false,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
final req = await Matrix.of(context)
|
||||
.client
|
||||
.userDeviceKeys[Matrix.of(context).client.userID!]!
|
||||
|
|
|
|||
|
|
@ -19,20 +19,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'pip/pip_view.dart';
|
||||
|
||||
class _StreamView extends StatelessWidget {
|
||||
|
|
@ -264,11 +263,7 @@ class MyCallingPage extends State<Calling> {
|
|||
void _handleCallState(CallState state) {
|
||||
Logs().v('CallingPage::handleCallState: ${state.toString()}');
|
||||
if ({CallState.kConnected, CallState.kEnded}.contains(state)) {
|
||||
try {
|
||||
Vibration.vibrate(duration: 200);
|
||||
} catch (e) {
|
||||
Logs().e('[Dialer] could not vibrate for call updates');
|
||||
}
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/public_homeserver.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
|
||||
import 'homeserver_bottom_sheet.dart';
|
||||
import 'homeserver_picker.dart';
|
||||
|
||||
|
|
@ -18,13 +17,17 @@ class HomeserverAppBar extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TypeAheadField<PublicHomeserver>(
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||
shadowColor: Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
|
||||
decorationBuilder: (context, child) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation ?? 4,
|
||||
shadowColor:
|
||||
Theme.of(context).appBarTheme.shadowColor ?? Colors.black,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
noItemsFoundBuilder: (context) => ListTile(
|
||||
emptyBuilder: (context) => ListTile(
|
||||
leading: const Icon(Icons.search_outlined),
|
||||
title: Text(L10n.of(context)!.nothingFound),
|
||||
),
|
||||
|
|
@ -35,8 +38,7 @@ class HomeserverAppBar extends StatelessWidget {
|
|||
errorBuilder: (context, error) => ListTile(
|
||||
leading: const Icon(Icons.error_outlined),
|
||||
title: Text(
|
||||
error?.toLocalizedString(context) ??
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
error.toLocalizedString(context),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, homeserver) => ListTile(
|
||||
|
|
@ -72,13 +74,15 @@ class HomeserverAppBar extends StatelessWidget {
|
|||
}
|
||||
return matches;
|
||||
},
|
||||
onSuggestionSelected: (suggestion) {
|
||||
onSelected: (suggestion) {
|
||||
controller.homeserverController.text = suggestion.name;
|
||||
controller.checkHomeserverAction();
|
||||
},
|
||||
textFieldConfiguration: TextFieldConfiguration(
|
||||
controller: controller.homeserverController,
|
||||
builder: (context, textEditingController, focusNode) => TextField(
|
||||
enabled: !controller.isLoggingIn,
|
||||
controller: controller.homeserverController,
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Navigator.of(context).canPop()
|
||||
? IconButton(
|
||||
|
|
|
|||
|
|
@ -121,14 +121,15 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||
void ssoLoginAction(IdentityProvider provider) async {
|
||||
//Pangea#
|
||||
final redirectUrl = kIsWeb
|
||||
// #Pangea
|
||||
// ? '${html.window.origin!}/web/auth.html'
|
||||
// : isDefaultPlatform
|
||||
// ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
||||
// : 'http://localhost:3001//login';
|
||||
? '${html.window.origin!}/auth.html'
|
||||
: '${AppConfig.appOpenUrlScheme.toLowerCase()}://login';
|
||||
//Pangea#
|
||||
? Uri.parse(html.window.location.href)
|
||||
.resolveUri(
|
||||
Uri(pathSegments: ['auth.html']),
|
||||
)
|
||||
.toString()
|
||||
: isDefaultPlatform
|
||||
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
||||
: 'http://localhost:3001//login';
|
||||
|
||||
final url = Matrix.of(context).getLoginClient().homeserver!.replace(
|
||||
// #Pangea
|
||||
// path: '/_matrix/client/v3/login/sso/redirect${id == null ? '' : '/$id'}',
|
||||
|
|
@ -199,15 +200,16 @@ class HomeserverPickerController extends State<HomeserverPicker> {
|
|||
List<IdentityProvider>? get identityProviders {
|
||||
final loginTypes = _rawLoginTypes;
|
||||
if (loginTypes == null) return null;
|
||||
final List? rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
|
||||
(flow) => flow['type'] == AuthenticationTypes.sso,
|
||||
)['identity_providers'] ??
|
||||
[
|
||||
{'id': null},
|
||||
];
|
||||
final list = (rawProviders as List)
|
||||
.map((json) => IdentityProvider.fromJson(json))
|
||||
.toList();
|
||||
final List? rawProviders =
|
||||
loginTypes.tryGetList('flows')?.singleWhereOrNull(
|
||||
(flow) => flow['type'] == AuthenticationTypes.sso,
|
||||
)['identity_providers'] ??
|
||||
[
|
||||
{'id': null},
|
||||
];
|
||||
if (rawProviders == null) return null;
|
||||
final list =
|
||||
rawProviders.map((json) => IdentityProvider.fromJson(json)).toList();
|
||||
if (PlatformInfos.isCupertinoStyle) {
|
||||
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,7 +181,9 @@ class InvitationSelectionController extends State<InvitationSelection> {
|
|||
if (spaceChild.roomId == null) continue;
|
||||
final spaceChildRoom =
|
||||
Matrix.of(context).client.getRoomById(spaceChild.roomId!);
|
||||
if (spaceChildRoom != null) {
|
||||
if (spaceChildRoom != null &&
|
||||
!(await spaceChildRoom.isBotDM) &&
|
||||
!spaceChildRoom.isDirectChat) {
|
||||
await spaceChildRoom.invite(id);
|
||||
await spaceChildRoom.setPower(
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -114,6 +114,10 @@ class NewGroupController extends State<NewGroup> {
|
|||
// content: {'url': avatarUrl.toString()},
|
||||
// ),
|
||||
// ],
|
||||
initialState: [
|
||||
if (addConversationBotKey.currentState?.addBot ?? false)
|
||||
addConversationBotKey.currentState!.botOptions.toStateEvent,
|
||||
],
|
||||
groupName: nameController.text,
|
||||
preset: sdk.CreateRoomPreset.publicChat,
|
||||
powerLevelContentOverride:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,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:qr_flutter/qr_flutter.dart';
|
||||
import 'package:pretty_qr_code/pretty_qr_code.dart';
|
||||
|
||||
class NewPrivateChatView extends StatelessWidget {
|
||||
final NewPrivateChatController controller;
|
||||
|
|
@ -159,11 +159,23 @@ class NewPrivateChatView extends StatelessWidget {
|
|||
shadowColor:
|
||||
Theme.of(context).appBarTheme.shadowColor,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: QrImageView(
|
||||
data:
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}',
|
||||
version: QrVersions.auto,
|
||||
// size: qrCodeSize,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: PrettyQrView.data(
|
||||
data:
|
||||
'https://matrix.to/#/${Matrix.of(context).client.userID}',
|
||||
decoration: PrettyQrDecoration(
|
||||
shape: PrettyQrSmoothSymbol(
|
||||
roundFactor: 1,
|
||||
color: Theme.of(context).brightness ==
|
||||
Brightness.light
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
|
||||
import 'package:fluffychat/widgets/app_lock.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../bootstrap/bootstrap_dialog.dart';
|
||||
|
|
@ -120,36 +115,7 @@ class SettingsSecurityController extends State<SettingsSecurity> {
|
|||
).show(context);
|
||||
}
|
||||
|
||||
Future<void> dehydrateAction() => dehydrateDevice(context);
|
||||
|
||||
static Future<void> dehydrateDevice(BuildContext context) async {
|
||||
final response = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
isDestructiveAction: true,
|
||||
title: L10n.of(context)!.dehydrate,
|
||||
message: L10n.of(context)!.dehydrateWarning,
|
||||
);
|
||||
if (response != OkCancelResult.ok) {
|
||||
return;
|
||||
}
|
||||
final file = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final export = await Matrix.of(context).client.exportDump();
|
||||
if (export == null) throw Exception('Export data is null.');
|
||||
|
||||
final exportBytes = Uint8List.fromList(
|
||||
const Utf8Codec().encode(export),
|
||||
);
|
||||
|
||||
final exportFileName =
|
||||
'fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup';
|
||||
|
||||
return MatrixFile(bytes: exportBytes, name: exportFileName);
|
||||
},
|
||||
);
|
||||
file.result?.save(context);
|
||||
}
|
||||
Future<void> dehydrateAction() => Matrix.of(context).dehydrateAction();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsSecurityView(this);
|
||||
|
|
|
|||