diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2b993ea05..7e75afa95 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,20 +1,22 @@
-version: 2
-updates:
- - package-ecosystem: "pub"
- directory: "/"
- schedule:
- interval: "daily"
- allow:
- - dependency-name: "*"
- commit-message:
- prefix: "build: "
- include: "scope"
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
- allow:
- - dependency-name: "*"
- commit-message:
- prefix: "build: "
- include: "scope"
+# #Pangea
+# version: 2
+# updates:
+# - package-ecosystem: "pub"
+# directory: "/"
+# schedule:
+# interval: "daily"
+# allow:
+# - dependency-name: "*"
+# commit-message:
+# prefix: "build: "
+# include: "scope"
+# - package-ecosystem: "github-actions"
+# directory: "/"
+# schedule:
+# interval: "daily"
+# allow:
+# - dependency-name: "*"
+# commit-message:
+# prefix: "build: "
+# include: "scope"
+# Pangea#
diff --git a/.github/workflows/auto_pull_request.yaml b/.github/workflows/auto_pull_request.yaml
deleted file mode 100644
index cf6bb228c..000000000
--- a/.github/workflows/auto_pull_request.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-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
diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml
deleted file mode 100644
index aa7a13d70..000000000
--- a/.github/workflows/build-ios.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: build-ios
-on:
- workflow_call:
- inputs:
- screenshot:
- type: string
- required: true
- ipa:
- description: 'Run IPA build'
- type: string
- required: true
- workflow_dispatch:
- inputs:
- screenshot:
- description: 'Run screenshot build'
- type: choice
- options: ['true', 'false']
- required: true
- ipa:
- description: 'Run IPA build'
- type: choice
- options: ['true', 'false']
- required: true
-
-
-jobs:
- build-ios:
- runs-on: macos-latest
- timeout-minutes: 20
- defaults:
- run:
- working-directory: ios
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - run: 'echo "$API_KEY" | base64 --decode > AuthKey.p8'
- shell: bash
- env:
- API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
-
- - run: bundle install
-
- - run: bundle exec fastlane versioning
-
- - name: Flutter
- uses: subosito/flutter-action@v2
- with:
- cache: true
- cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
- - run: flutter build ios --simulator --target=integration_test/screenshot_test.dart
- if: ${{ inputs.screenshot == 'true' }}
-
- - name: Archive integration ipa
- if: ${{ inputs.screenshot == 'true' }}
- uses: actions/upload-artifact@v4
- with:
- name: app-simulator-build
- path: build/ios/iphonesimulator/Runner.app
- if-no-files-found: error
- retention-days: 3
-
- # Build ios Release
- - run: flutter build ios --release --config-only --no-codesign --target=lib/main.dart
- if: ${{ inputs.ipa == 'true' }}
-
- - run: bundle exec fastlane build
- if: ${{ inputs.ipa == 'true' }}
-
- - name: Archive ipa
- if: ${{ inputs.ipa == 'true' }}
- uses: actions/upload-artifact@v4
- with:
- name: Runner.ipa
- path: ios/Runner.ipa
- if-no-files-found: error
- retention-days: 3
\ No newline at end of file
diff --git a/.github/workflows/dart_format.yaml b/.github/workflows/dart_format.yaml
deleted file mode 100644
index 5e1a142c5..000000000
--- a/.github/workflows/dart_format.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
-# name: Dart Code Formatter
-
-# on:
-# pull_request:
-# push:
-# branches: main
-
-# jobs:
-# format:
-# runs-on: ubuntu-latest
-
-# steps:
-# - name: Checkout code
-# uses: actions/checkout@v3
-# with:
-# ref: ${{ github.head_ref }}
-
-# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
-# - uses: subosito/flutter-action@v2
-# with:
-# flutter-version: ${{ env.FLUTTER_VERSION }}
-# cache: true
-
-# - name: Auto-format Dart code
-# run: |
-# dart format lib/ test/
-# dart run import_sorter:main --no-comments
-# if ! git diff --exit-code; then
-# git config user.name "github-actions[bot]"
-# git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-# git add .
-# git commit -m "generated"
-# git push
-# fi
-
-# - name: Check for unformatted files
-# if: ${{ failure() }}
-# run: |
-# echo "Code was formatted. Please verify the changes in the PR."
diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml
index c4307fadf..dc4df96cf 100644
--- a/.github/workflows/integrate.yaml
+++ b/.github/workflows/integrate.yaml
@@ -1,107 +1,105 @@
-name: Pull Request Workflow
+# #Pangea
+# name: Pull Request Workflow
-on:
- pull_request:
- merge_group:
+# on:
+# pull_request:
+# merge_group:
-jobs:
- code_tests:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - run: ./scripts/generate-locale-config.sh
- - run: git diff --exit-code
- - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: true
- - run: flutter pub get
- - run: flutter gen-l10n
- - name: Check formatting
- 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
- # #Pangea - Commented out the following lines, we already have fcm enabled by default
- # - name: Apply google services patch
- # run: git apply ./scripts/enable-android-google-services.patch
- # Pangea#
- - run: flutter analyze
- - run: flutter test
+# jobs:
+# code_tests:
+# runs-on: ubuntu-latest
+# steps:
+# - uses: actions/checkout@v4
+# - run: ./scripts/generate-locale-config.sh
+# - run: git diff --exit-code
+# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+# - uses: subosito/flutter-action@v2
+# with:
+# flutter-version: ${{ env.FLUTTER_VERSION }}
+# cache: true
+# - run: flutter pub get
+# - run: flutter gen-l10n
+# - name: Check formatting
+# 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
- build_apk:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- - uses: actions/setup-java@v4
- with:
- java-version: ${{ env.JAVA_VERSION }}
- distribution: "zulu"
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: false
- - run: flutter pub get
- - name: Free Disk Space (Ubuntu)
- uses: jlumbroso/free-disk-space@main
- with:
- # this might remove tools that are actually needed,
- # if set to "true" but frees about 6 GB
- tool-cache: false
- android: false
- - run: flutter build apk --debug
+# build_apk:
+# runs-on: ubuntu-latest
+# steps:
+# - uses: actions/checkout@v4
+# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+# - uses: actions/setup-java@v4
+# with:
+# java-version: ${{ env.JAVA_VERSION }}
+# distribution: "zulu"
+# - uses: subosito/flutter-action@v2
+# with:
+# flutter-version: ${{ env.FLUTTER_VERSION }}
+# cache: false
+# - run: flutter pub get
+# - name: Free Disk Space (Ubuntu)
+# uses: jlumbroso/free-disk-space@main
+# with:
+# # this might remove tools that are actually needed,
+# # if set to "true" but frees about 6 GB
+# tool-cache: false
+# android: false
+# - run: flutter build apk --debug
- build_web:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: false
- - run: flutter pub get
- - name: Prepare web
- run: ./scripts/prepare-web.sh
- - run: flutter build web
+# build_web:
+# runs-on: ubuntu-latest
+# steps:
+# - uses: actions/checkout@v4
+# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+# - uses: subosito/flutter-action@v2
+# with:
+# flutter-version: ${{ env.FLUTTER_VERSION }}
+# cache: false
+# - run: flutter pub get
+# - name: Prepare web
+# run: ./scripts/prepare-web.sh
+# - run: flutter build web
- # #Pangea
- # commented out because we do not build Pangea Chat to linux
- # build_debug_linux:
- # strategy:
- # matrix:
- # arch: [ x64, arm64 ]
- # runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}}
- # steps:
- # - uses: actions/checkout@v4
- # - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- # - name: Install dependencies
- # run: sudo apt-get update && sudo apt-get install git wget 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 libwebkit2gtk-4.1-dev -y
- # - name: Install Flutter
- # run: |
- # git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
- # ./flutter/bin/flutter doctor
- # - run: ./flutter/bin/flutter pub get
- # - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
- # Pangea#
+# commented out because we do not build Pangea Chat to linux
+# build_debug_linux:
+# strategy:
+# matrix:
+# arch: [ x64, arm64 ]
+# runs-on: ${{ matrix.arch == 'arm64' && 'self-hosted' || 'ubuntu-latest'}}
+# steps:
+# - uses: actions/checkout@v4
+# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+# - name: Install dependencies
+# run: sudo apt-get update && sudo apt-get install git wget 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 libwebkit2gtk-4.1-dev -y
+# - name: Install Flutter
+# run: |
+# git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
+# ./flutter/bin/flutter doctor
+# - run: ./flutter/bin/flutter pub get
+# - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
- build_debug_ios:
- runs-on: macos-15
- steps:
- - uses: actions/checkout@v4
- - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- - uses: subosito/flutter-action@v2
- with:
- flutter-version: ${{ env.FLUTTER_VERSION }}
- cache: true
- - name: Setup Xcode version
- uses: maxim-lobanov/setup-xcode@v1.6.0
- with:
- xcode-version: latest
- - run: brew install sqlcipher
- - run: flutter pub get
- - run: flutter build ipa --no-codesign
+# build_debug_ios:
+# runs-on: macos-15
+# steps:
+# - uses: actions/checkout@v4
+# - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+# - uses: subosito/flutter-action@v2
+# with:
+# flutter-version: ${{ env.FLUTTER_VERSION }}
+# cache: true
+# - name: Setup Xcode version
+# uses: maxim-lobanov/setup-xcode@v1.6.0
+# with:
+# xcode-version: latest
+# - run: brew install sqlcipher
+# - run: flutter pub get
+# - run: flutter build ipa --no-codesign
+# Pangea#
diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml
index 1999c49d3..e9d809e60 100644
--- a/.github/workflows/manual.yml
+++ b/.github/workflows/manual.yml
@@ -77,13 +77,4 @@ jobs:
bundle install
bundle update fastlane
bundle exec fastlane deploy_internal_test
- cd ..
-
- deploy_ios_testflight: # stashed on old.yml
- environment:
- name: ${{ inputs.environment }}
- env:
- WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
\ No newline at end of file
+ cd ..
\ No newline at end of file
diff --git a/.github/workflows/matrix_notification.yaml b/.github/workflows/matrix_notification.yaml
deleted file mode 100644
index 1d6dd8085..000000000
--- a/.github/workflows/matrix_notification.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-# name: Matrix Notification
-
-# on:
-# issues:
-# types: [ opened ]
-# issue_comment:
-# types: [ created ]
-
-# jobs:
-# notify:
-# runs-on: ubuntu-latest
-
-# steps:
-# - name: Send Matrix Notification
-# env:
-# MATRIX_URL: https://matrix.janian.de/_matrix/client/v3/rooms/${{ secrets.MATRIX_MANAGEMENT_ROOM }}/send/m.room.message
-# run: |
-# if [ "${{ github.event.action }}" == "opened" ]; then
-# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Issue from ${{ github.event.issue.user.login }}\\n${{ github.event.issue.title }}\\n\\n${{ github.event.issue.body }}\\n\\nURL: ${{ github.event.issue.html_url }}\"}"
-# elif [ "${{ github.event.action }}" == "created" ]; then
-# PAYLOAD="{\"msgtype\": \"m.notice\", \"body\": \"New Comment from ${{ github.event.comment.user.login }}\\n\\n${{ github.event.comment.body }}\\n\\nURL: ${{ github.event.comment.html_url }}\"}"
-# fi
-# curl -X POST -H "Authorization: Bearer ${{ secrets.MATRIX_BOT_TOKEN }}" \
-# -H "Content-Type: application/json" \
-# -d "$PAYLOAD" \
-# $MATRIX_URL
diff --git a/.github/workflows/old.yml b/.github/workflows/old.yml
deleted file mode 100644
index 62d57416b..000000000
--- a/.github/workflows/old.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Old Release Workflow
-
-on:
- push:
- branches:
- - master-unused
-
-concurrency:
- group: release_workflow
- cancel-in-progress: true
-
-jobs:
- deploy_ios_internal:
- runs-on: macos-latest
- environment: staging
- steps:
- - uses: actions/checkout@v4
- - run: cat ../.github/workflows/versions.env >> $GITHUB_ENV
-
- - name: Flutter
- uses: subosito/flutter-action@v2
- with:
- cache: true
- cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:'
- cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
-
- # Build ios Release
- - run: flutter build ios --release --config-only --no-codesign --target=lib/main.dart
-
- - name: Deploy ios
- run: |
- mkdir -p build/ios
- cp build/app/outputs/bundle/release/app-release.aab build/ios/
- cd ios
- bundle install
- bundle update fastlane
- cd ..
- - name: Execute fastlane signing
- env:
- APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
- APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
- APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64 }}
- APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
- run: bundle exec fastlane ios beta
\ No newline at end of file
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index b6751456d..51705779a 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -142,36 +142,39 @@ jobs:
asset_path: build/app/outputs/flutter-apk/app-release.apk
asset_name: pangeachat.apk
asset_content_type: application/vnd.android.package-archive
- build_linux:
- strategy:
- matrix:
- arch: [ x64 ]
- runs-on: ubuntu-latest
- needs: create_release
- steps:
- - uses: actions/checkout@v4
- - run: cat .github/workflows/versions.env >> $GITHUB_ENV
- - 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 libssl-dev libwebkit2gtk-4.1-dev -y
- - name: Install dependencies for audio-player
- run: sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
- - name: Install Flutter
- run: |
- git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
- ./flutter/bin/flutter doctor
- - run: ./flutter/bin/flutter pub get
- - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
- - name: Create archive
- run: tar -czf pangeachat-linux-${{ matrix.arch }}.tar.gz -C build/linux/${{ matrix.arch }}/release/bundle/ .
- - name: Upload to release
- uses: actions/upload-release-asset@v1
- env:
- GITHUB_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }}
- with:
- upload_url: ${{ needs.create_release.outputs.upload_url }}
- asset_path: pangeachat-linux-${{ matrix.arch }}.tar.gz
- asset_name: pangeachat-linux-${{ matrix.arch }}.tar.gz
- asset_content_type: application/gzip
+
+ # #Pangea
+ # build_linux:
+ # strategy:
+ # matrix:
+ # arch: [ x64 ]
+ # runs-on: ubuntu-latest
+ # needs: create_release
+ # steps:
+ # - uses: actions/checkout@v4
+ # - run: cat .github/workflows/versions.env >> $GITHUB_ENV
+ # - 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 libssl-dev libwebkit2gtk-4.1-dev -y
+ # - name: Install dependencies for audio-player
+ # run: sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+ # - name: Install Flutter
+ # run: |
+ # git clone --branch ${{ env.FLUTTER_VERSION }} https://github.com/flutter/flutter.git
+ # ./flutter/bin/flutter doctor
+ # - run: ./flutter/bin/flutter pub get
+ # - run: ./flutter/bin/flutter build linux --target-platform linux-${{ matrix.arch }}
+ # - name: Create archive
+ # run: tar -czf pangeachat-linux-${{ matrix.arch }}.tar.gz -C build/linux/${{ matrix.arch }}/release/bundle/ .
+ # - name: Upload to release
+ # uses: actions/upload-release-asset@v1
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }}
+ # with:
+ # upload_url: ${{ needs.create_release.outputs.upload_url }}
+ # asset_path: pangeachat-linux-${{ matrix.arch }}.tar.gz
+ # asset_name: pangeachat-linux-${{ matrix.arch }}.tar.gz
+ # asset_content_type: application/gzip
+ # Pangea#
deploy_web:
runs-on: ubuntu-latest
diff --git a/.github/workflows/upload-release-ios.yml b/.github/workflows/upload-release-ios.yml
deleted file mode 100644
index aa70cd9ca..000000000
--- a/.github/workflows/upload-release-ios.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: upload-release-ios
-on:
- workflow_call:
- inputs:
- new_release:
- required: true
- type: string
- description: "The new release version number"
- new_release_notes:
- required: true
- type: string
- description: "The release notes for the new release"
-
-jobs:
- build:
- runs-on: macos-latest
- timeout-minutes: 10
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Download app
- uses: actions/download-artifact@v4
- with:
- name: Runner.ipa
- path: ios/
-
- - run: 'echo "$API_KEY" | base64 --decode > AuthKey.p8'
- shell: bash
- env:
- API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
-
- - run: bundle install
-
- - run: bundle exec fastlane upload_testflight
- env:
- RELEASE_NOTES: ${{ inputs.new_release_notes }}
- - run: bundle exec fastlane upload_metadata_app_store
\ No newline at end of file
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb
index 813eb9c76..00ebd337f 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -5017,6 +5017,10 @@
"newDirectMessage": "New direct message",
"speakingExercisesTooltip": "Speaking practice",
"noChatsFoundHereYet": "No chats found here yet",
+ "endNow": "End now",
+ "setDuration": "Set duration",
+ "activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!",
+ "duration": "Duration",
"transcriptionFailed": "Failed to transcribe audio",
"aUserIsKnocking": "1 user is requesting to join your space",
"usersAreKnocking": "{users} users are requesting to join your space",
@@ -5027,5 +5031,6 @@
"type": "int"
}
}
- }
+ },
+ "failedToFetchTranscription": "Failed to fetch transcription"
}
\ No newline at end of file
diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart
index 1436cb00b..2f90e7f59 100644
--- a/lib/pages/chat/chat_view.dart
+++ b/lib/pages/chat/chat_view.dart
@@ -13,9 +13,11 @@ 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/pangea/activities/pinned_activity_message.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
+import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/account_config.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@@ -188,6 +190,13 @@ class ChatView extends StatelessWidget {
if (scrollUpBannerEventId != null) {
appbarBottomHeight += ChatAppBarListTile.fixedHeight;
}
+ // #Pangea
+ if (controller.room.activityPlan != null &&
+ controller.room.activityPlan!.endAt != null &&
+ controller.room.activityPlan!.endAt!.isAfter(DateTime.now())) {
+ appbarBottomHeight += ChatAppBarListTile.fixedHeight;
+ }
+ // Pangea#
return Scaffold(
appBar: AppBar(
actionsIconTheme: IconThemeData(
@@ -226,6 +235,9 @@ class ChatView extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
PinnedEvents(controller),
+ // #Pangea
+ PinnedActivityMessage(controller),
+ // Pangea#
if (scrollUpBannerEventId != null)
ChatAppBarListTile(
leading: IconButton(
diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart
index ddfa041b3..268abbcac 100644
--- a/lib/pages/chat/events/html_message.dart
+++ b/lib/pages/chat/events/html_message.dart
@@ -185,6 +185,23 @@ class HtmlMessage extends StatelessWidget {
result.add(html.substring(lastEnd)); // Remaining text after last tag
}
+ final replyTagIndex = result.indexWhere(
+ (string) => string.contains(''),
+ );
+ if (replyTagIndex != -1) {
+ final closingReplyTagIndex = result.indexWhere(
+ (string) => string.contains(''),
+ replyTagIndex,
+ );
+ if (closingReplyTagIndex != -1) {
+ result.replaceRange(
+ replyTagIndex,
+ closingReplyTagIndex + 1,
+ [result.sublist(replyTagIndex, closingReplyTagIndex + 1).join()],
+ );
+ }
+ }
+
for (final PangeaToken token in tokens ?? []) {
final String tokenText = token.text.content;
final substringIndex = result.indexWhere(
@@ -313,7 +330,6 @@ class HtmlMessage extends StatelessWidget {
);
return WidgetSpan(
- alignment: PlaceholderAlignment.middle,
child: CompositedTransformTarget(
link: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
@@ -409,7 +425,7 @@ class HtmlMessage extends StatelessWidget {
outerContext: context,
// #Pangea
// fontSize: fontSize,
- fontSize: renderer.fontSize(context),
+ fontSize: renderer.fontSize(context) ?? fontSize,
// Pangea#
color: linkStyle.color,
// #Pangea
@@ -433,7 +449,7 @@ class HtmlMessage extends StatelessWidget {
outerContext: context,
// #Pangea
// fontSize: fontSize,
- fontSize: renderer.fontSize(context),
+ fontSize: renderer.fontSize(context) ?? fontSize,
// Pangea#
color: linkStyle.color,
),
diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart
index ada6cceb4..0fd7e79bc 100644
--- a/lib/pages/chat/events/message.dart
+++ b/lib/pages/chat/events/message.dart
@@ -9,7 +9,9 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
+import 'package:fluffychat/pangea/activities/activity_state_event.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
+import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/file_description.dart';
@@ -121,6 +123,18 @@ class Message extends StatelessWidget {
if (event.type == EventTypes.RoomCreate) {
return RoomCreationStateEvent(event: event);
}
+ // #Pangea
+ if (event.type == PangeaEventTypes.activityPlan) {
+ final state = event.room.getState(PangeaEventTypes.activityPlan);
+ if (state == null || state is! Event) {
+ return const SizedBox.shrink();
+ }
+
+ return state.originServerTs == event.originServerTs
+ ? ActivityStateEvent(event: event)
+ : const SizedBox();
+ }
+ // Pangea#
return StateMessage(event);
}
diff --git a/lib/pages/chat/pinned_events.dart b/lib/pages/chat/pinned_events.dart
index ef9dd29a1..46c17d815 100644
--- a/lib/pages/chat/pinned_events.dart
+++ b/lib/pages/chat/pinned_events.dart
@@ -79,10 +79,20 @@ class PinnedEvents extends StatelessWidget {
iconSize: 18,
color: theme.colorScheme.onSurfaceVariant,
icon: const Icon(Icons.push_pin),
- tooltip: L10n.of(context).unpin,
- onPressed: controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
- ? () => controller.unpinEvent(event!.eventId)
- : null,
+ // #Pangea
+ // tooltip: L10n.of(context).unpin,
+ // onPressed: controller.room.canSendEvent(EventTypes.RoomPinnedEvents)
+ // ? () => controller.unpinEvent(event!.eventId)
+ // : null,
+ tooltip:
+ controller.room.canChangeStateEvent(EventTypes.RoomPinnedEvents)
+ ? L10n.of(context).unpin
+ : null,
+ onPressed:
+ controller.room.canChangeStateEvent(EventTypes.RoomPinnedEvents)
+ ? () => controller.unpinEvent(event!.eventId)
+ : null,
+ // Pangea#
),
onTap: () => _displayPinnedEventsDialog(context),
);
diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart
index 5756cf402..78c5fecb7 100644
--- a/lib/pages/chat_list/chat_list.dart
+++ b/lib/pages/chat_list/chat_list.dart
@@ -743,20 +743,24 @@ class ChatListController extends State
value: ChatContextAction.open,
child: Row(
mainAxisSize: MainAxisSize.min,
+ spacing: 12.0,
children: [
Avatar(
mxContent: room.avatar,
- size: Avatar.defaultSize / 2,
name: displayname,
// #Pangea
userId: room.directChatMatrixID,
// Pangea#
),
- const SizedBox(width: 12),
- Text(
- displayname,
- style:
- TextStyle(color: Theme.of(context).colorScheme.onSurface),
+ ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 128),
+ child: Text(
+ displayname,
+ style:
+ TextStyle(color: Theme.of(context).colorScheme.onSurface),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
),
],
),
diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart
index c59817b8c..59c6f6ad0 100644
--- a/lib/pages/invitation_selection/invitation_selection_view.dart
+++ b/lib/pages/invitation_selection/invitation_selection_view.dart
@@ -345,7 +345,20 @@ class _InviteContactListTile extends StatelessWidget {
// color: theme.colorScheme.secondary,
// ),
// ),
- subtitle: LevelDisplayName(userId: profile.userId),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // https://github.com/pangeachat/client/issues/3047
+ const SizedBox(height: 2.0),
+ Text(
+ profile.userId,
+ style: const TextStyle(
+ fontSize: 12.0,
+ ),
+ ),
+ LevelDisplayName(userId: profile.userId),
+ ],
+ ),
// Pangea#
trailing: TextButton.icon(
onPressed: isMember ? null : onTap,
diff --git a/lib/pages/new_group/new_group.dart b/lib/pages/new_group/new_group.dart
index b918f532d..395804047 100644
--- a/lib/pages/new_group/new_group.dart
+++ b/lib/pages/new_group/new_group.dart
@@ -38,6 +38,8 @@ class NewGroupController extends State {
TextEditingController nameController = TextEditingController();
// #Pangea
+ // bool publicGroup = false;
+ // bool groupCanBeFound = false;
ActivityPlanModel? selectedActivity;
Uint8List? selectedActivityImage;
String? selectedActivityImageFilename;
@@ -45,12 +47,8 @@ class NewGroupController extends State {
final GlobalKey formKey = GlobalKey();
final FocusNode focusNode = FocusNode();
- bool requiredCodeToJoin = false;
- // bool publicGroup = false;
-
bool get canSubmit => nameController.text.trim().isNotEmpty;
// Pangea#
- bool groupCanBeFound = false;
Uint8List? avatar;
@@ -71,7 +69,6 @@ class NewGroupController extends State {
// #Pangea
// void setPublicGroup(bool b) =>
// setState(() => publicGroup = groupCanBeFound = b);
- void setRequireCode(bool b) => setState(() => requiredCodeToJoin = b);
void setSelectedActivity(
ActivityPlanModel? activity,
@@ -103,7 +100,9 @@ class NewGroupController extends State {
}
// Pangea#
- void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
+ // #Pangea
+ // void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
+ // Pangea#
void selectPhoto() async {
final photo = await selectFiles(
@@ -230,10 +229,8 @@ class NewGroupController extends State {
name: nameController.text,
introChatName: L10n.of(context).introductions,
announcementsChatName: L10n.of(context).announcements,
- visibility:
- groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private,
- joinRules:
- requiredCodeToJoin ? sdk.JoinRules.knock : sdk.JoinRules.public,
+ visibility: sdk.Visibility.private,
+ joinRules: sdk.JoinRules.knock,
avatar: avatar,
avatarUrl: avatarUrl,
);
diff --git a/lib/pages/new_group/new_group_view.dart b/lib/pages/new_group/new_group_view.dart
index c6c825c10..f07747ac2 100644
--- a/lib/pages/new_group/new_group_view.dart
+++ b/lib/pages/new_group/new_group_view.dart
@@ -4,7 +4,6 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/new_group/new_group.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_carousel.dart';
-import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@@ -38,20 +37,6 @@ class NewGroupView extends StatelessWidget {
: L10n.of(context).newChat,
// Pangea#
),
- actions: [
- if (controller.createGroupType == CreateGroupType.space)
- TextButton(
- onPressed: controller.loading
- ? null
- : () => SpaceCodeUtil.joinWithSpaceCodeDialog(context),
- child: Text(
- L10n.of(context).joinByCode,
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- ),
- ),
- ),
- ],
),
body: MaxWidthBody(
// #Pangea
@@ -141,51 +126,33 @@ class NewGroupView extends StatelessWidget {
),
const SizedBox(height: 16),
// #Pangea
- if (controller.createGroupType == CreateGroupType.space)
- // Pangea#
- SwitchListTile.adaptive(
- contentPadding: const EdgeInsets.symmetric(horizontal: 32),
- secondary: const Icon(Icons.public_outlined),
- // #Pangea
- // title: Text(
- // controller.createGroupType == CreateGroupType.space
- // ? L10n.of(context).spaceIsPublic
- // : L10n.of(context).groupIsPublic,
- // ),
- title: Text(L10n.of(context).requireCodeToJoin),
- // value: controller.publicGroup,
- // onChanged:
- // controller.loading ? null : controller.setPublicGroup,
- value: controller.requiredCodeToJoin,
- onChanged:
- controller.loading ? null : controller.setRequireCode,
- // Pangea#
- ),
- // #Pangea
- if (controller.createGroupType == CreateGroupType.space)
- // Pangea#
- AnimatedSize(
- duration: FluffyThemes.animationDuration,
- curve: FluffyThemes.animationCurve,
- child:
- // #Pangea
- // controller.publicGroup ?
- // Pangea#
- SwitchListTile.adaptive(
- contentPadding: const EdgeInsets.symmetric(horizontal: 32),
- secondary: const Icon(Icons.search_outlined),
- // #Pangea
- // title: Text(L10n.of(context).groupCanBeFoundViaSearch),
- title: Text(L10n.of(context).canFindInSearch),
- // Pangea#
- value: controller.groupCanBeFound,
- onChanged:
- controller.loading ? null : controller.setGroupCanBeFound,
- ),
- // #Pangea
- // : const SizedBox.shrink(),
- // Pangea#
- ),
+ // SwitchListTile.adaptive(
+ // contentPadding: const EdgeInsets.symmetric(horizontal: 32),
+ // secondary: const Icon(Icons.public_outlined),
+ // title: Text(
+ // controller.createGroupType == CreateGroupType.space
+ // ? L10n.of(context).spaceIsPublic
+ // : L10n.of(context).groupIsPublic,
+ // ),
+ // value: controller.publicGroup,
+ // onChanged: controller.loading ? null : controller.setPublicGroup,
+ // ),
+ // AnimatedSize(
+ // duration: FluffyThemes.animationDuration,
+ // curve: FluffyThemes.animationCurve,
+ // child: controller.publicGroup
+ // ? SwitchListTile.adaptive(
+ // contentPadding:
+ // const EdgeInsets.symmetric(horizontal: 32),
+ // secondary: const Icon(Icons.search_outlined),
+ // title: Text(L10n.of(context).groupCanBeFoundViaSearch),
+ // value: controller.groupCanBeFound,
+ // onChanged: controller.loading
+ // ? null
+ // : controller.setGroupCanBeFound,
+ // )
+ // : const SizedBox.shrink(),
+ // ),
// AnimatedSize(
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
diff --git a/lib/pangea/activities/activity_aware_builder.dart b/lib/pangea/activities/activity_aware_builder.dart
new file mode 100644
index 000000000..68eea54af
--- /dev/null
+++ b/lib/pangea/activities/activity_aware_builder.dart
@@ -0,0 +1,62 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class ActivityAwareBuilder extends StatefulWidget {
+ final DateTime? deadline;
+ final Widget Function(bool) builder;
+
+ const ActivityAwareBuilder({
+ super.key,
+ required this.builder,
+ this.deadline,
+ });
+
+ @override
+ State createState() => ActivityAwareBuilderState();
+}
+
+class ActivityAwareBuilderState extends State {
+ Timer? _timer;
+
+ @override
+ void initState() {
+ super.initState();
+ _setTimer();
+ }
+
+ @override
+ void didUpdateWidget(covariant ActivityAwareBuilder oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.deadline != widget.deadline) {
+ _setTimer();
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ void _setTimer() {
+ final now = DateTime.now();
+ final delay = widget.deadline?.difference(now);
+
+ if (delay != null && delay > Duration.zero) {
+ _timer?.cancel();
+ _timer = Timer(delay, () {
+ _timer?.cancel();
+ _timer = null;
+ if (mounted) setState(() {});
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return widget.builder(
+ widget.deadline != null && widget.deadline!.isAfter(DateTime.now()),
+ );
+ }
+}
diff --git a/lib/pangea/activities/activity_constants.dart b/lib/pangea/activities/activity_constants.dart
new file mode 100644
index 000000000..41858d53a
--- /dev/null
+++ b/lib/pangea/activities/activity_constants.dart
@@ -0,0 +1,3 @@
+class ActivityConstants {
+ static const String activityFinishedAsset = "EndActivityMsg.png";
+}
diff --git a/lib/pangea/activities/activity_duration_popup.dart b/lib/pangea/activities/activity_duration_popup.dart
new file mode 100644
index 000000000..c6cbb56ab
--- /dev/null
+++ b/lib/pangea/activities/activity_duration_popup.dart
@@ -0,0 +1,282 @@
+import 'package:flutter/material.dart';
+
+import 'package:fluffychat/config/themes.dart';
+import 'package:fluffychat/l10n/l10n.dart';
+
+class ActivityDurationPopup extends StatefulWidget {
+ final Duration initialValue;
+ const ActivityDurationPopup({
+ super.key,
+ required this.initialValue,
+ });
+
+ @override
+ State createState() => ActivityDurationPopupState();
+}
+
+class ActivityDurationPopupState extends State {
+ final TextEditingController _daysController = TextEditingController();
+ final TextEditingController _hoursController = TextEditingController();
+ final TextEditingController _minutesController = TextEditingController();
+
+ String? error;
+
+ final List _durations = [
+ const Duration(minutes: 15),
+ const Duration(minutes: 30),
+ const Duration(minutes: 45),
+ const Duration(minutes: 60),
+ const Duration(hours: 1, minutes: 30),
+ const Duration(hours: 2),
+ const Duration(hours: 24),
+ const Duration(days: 2),
+ const Duration(days: 7),
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ _daysController.text = widget.initialValue.inDays.toString();
+ _hoursController.text =
+ widget.initialValue.inHours.remainder(24).toString();
+ _minutesController.text =
+ widget.initialValue.inMinutes.remainder(60).toString();
+
+ _daysController.addListener(() => setState(() => error = null));
+ _hoursController.addListener(() => setState(() => error = null));
+ _minutesController.addListener(() => setState(() => error = null));
+ }
+
+ @override
+ void dispose() {
+ _daysController.dispose();
+ _hoursController.dispose();
+ _minutesController.dispose();
+ super.dispose();
+ }
+
+ void _setDuration({int? days, int? hours, int? minutes}) {
+ setState(() {
+ if (days != null) _daysController.text = days.toString();
+ if (hours != null) _hoursController.text = hours.toString();
+ if (minutes != null) _minutesController.text = minutes.toString();
+ });
+ }
+
+ String _formatDuration(Duration duration) {
+ final days = duration.inDays;
+ final hours = duration.inHours.remainder(24);
+ final minutes = duration.inMinutes.remainder(60);
+
+ final List parts = [];
+ if (days > 0) parts.add("${days}d");
+ if (hours > 0) parts.add("${hours}h");
+ if (minutes > 0) parts.add("${minutes}m");
+ if (parts.isEmpty) return "0m";
+
+ return parts.join(" ");
+ }
+
+ Duration get _duration {
+ final days = int.tryParse(_daysController.text) ?? 0;
+ final hours = int.tryParse(_hoursController.text) ?? 0;
+ final minutes = int.tryParse(_minutesController.text) ?? 0;
+ return Duration(days: days, hours: hours, minutes: minutes);
+ }
+
+ void _submit() {
+ final days = int.tryParse(_daysController.text);
+ final hours = int.tryParse(_hoursController.text);
+ final minutes = int.tryParse(_minutesController.text);
+
+ if (days == null || hours == null || minutes == null) {
+ setState(() {
+ error = "Invalid duration";
+ });
+ return;
+ }
+
+ Navigator.of(context).pop(_duration);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Dialog(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(
+ maxWidth: 350.0,
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(20),
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ spacing: 12.0,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ L10n.of(context).setDuration,
+ style: const TextStyle(fontSize: 20.0, height: 1.2),
+ ),
+ Column(
+ children: [
+ Container(
+ decoration: ShapeDecoration(
+ shape: RoundedRectangleBorder(
+ side: BorderSide(
+ width: 2,
+ color: theme.colorScheme.primary.withAlpha(100),
+ ),
+ borderRadius: BorderRadius.circular(20),
+ ),
+ ),
+ padding: const EdgeInsets.only(
+ top: 12.0,
+ bottom: 12.0,
+ right: 24.0,
+ left: 8.0,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ SelectionArea(
+ child: Row(
+ spacing: 12.0,
+ children: [
+ _DatePickerInput(
+ type: "d",
+ controller: _daysController,
+ ),
+ _DatePickerInput(
+ type: "h",
+ controller: _hoursController,
+ ),
+ _DatePickerInput(
+ type: "m",
+ controller: _minutesController,
+ ),
+ ],
+ ),
+ ),
+ const Icon(
+ Icons.alarm,
+ size: 24,
+ ),
+ ],
+ ),
+ ),
+ AnimatedSize(
+ duration: FluffyThemes.animationDuration,
+ child: error != null
+ ? Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Text(
+ error!,
+ style: TextStyle(
+ color: theme.colorScheme.error,
+ fontSize: 14.0,
+ ),
+ ),
+ )
+ : const SizedBox.shrink(),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0,
+ horizontal: 24.0,
+ ),
+ child: Wrap(
+ spacing: 10.0,
+ runSpacing: 10.0,
+ children: _durations
+ .map(
+ (d) => InkWell(
+ borderRadius: BorderRadius.circular(12),
+ onTap: () {
+ _setDuration(
+ days: d.inDays,
+ hours: d.inHours.remainder(24),
+ minutes: d.inMinutes.remainder(60),
+ );
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ vertical: 0.0,
+ ),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.primaryContainer
+ .withAlpha(_duration == d ? 200 : 100),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(_formatDuration(d)),
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ TextButton(
+ onPressed: _submit,
+ child: Text(L10n.of(context).confirm),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _DatePickerInput extends StatelessWidget {
+ final String type;
+ final TextEditingController controller;
+
+ const _DatePickerInput({
+ required this.type,
+ required this.controller,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ SizedBox(
+ width: 35.0,
+ child: TextField(
+ controller: controller,
+ textAlign: TextAlign.end,
+ decoration: InputDecoration(
+ isDense: true,
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.all(0.0),
+ hintText: "0",
+ hintStyle: TextStyle(
+ fontSize: 20.0,
+ color: theme.colorScheme.onSurfaceVariant.withAlpha(100),
+ ),
+ ),
+ style: const TextStyle(
+ fontSize: 20.0,
+ ),
+ keyboardType: TextInputType.number,
+ ),
+ ),
+ Text(type, style: const TextStyle(fontSize: 20.0)),
+ ],
+ );
+ }
+}
diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart
new file mode 100644
index 000000000..1f7a250e5
--- /dev/null
+++ b/lib/pangea/activities/activity_state_event.dart
@@ -0,0 +1,272 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:matrix/matrix.dart';
+
+import 'package:fluffychat/config/app_config.dart';
+import 'package:fluffychat/config/themes.dart';
+import 'package:fluffychat/l10n/l10n.dart';
+import 'package:fluffychat/pangea/activities/activity_constants.dart';
+import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
+import 'package:fluffychat/pangea/activities/countdown.dart';
+import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
+import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
+import 'package:fluffychat/widgets/future_loading_dialog.dart';
+import 'package:fluffychat/widgets/mxc_image.dart';
+
+class ActivityStateEvent extends StatefulWidget {
+ final Event event;
+
+ const ActivityStateEvent({required this.event, super.key});
+
+ @override
+ State createState() => ActivityStateEventState();
+}
+
+class ActivityStateEventState extends State {
+ Timer? _timer;
+
+ @override
+ void initState() {
+ super.initState();
+ final now = DateTime.now();
+ final delay = activityPlan?.endAt != null
+ ? activityPlan!.endAt!.difference(now)
+ : null;
+
+ if (delay != null && delay > Duration.zero) {
+ _timer = Timer(delay, () {
+ setState(() {});
+ });
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ ActivityPlanModel? get activityPlan {
+ try {
+ return ActivityPlanModel.fromJson(widget.event.content);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ bool get _activityIsOver {
+ return activityPlan?.endAt != null &&
+ DateTime.now().isAfter(activityPlan!.endAt!);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (activityPlan == null) {
+ return const SizedBox.shrink();
+ }
+
+ final theme = Theme.of(context);
+ final isColumnMode = FluffyThemes.isColumnMode(context);
+
+ final double imageWidth = isColumnMode ? 240.0 : 175.0;
+
+ return Center(
+ child: Container(
+ constraints: const BoxConstraints(
+ maxWidth: 400.0,
+ ),
+ margin: const EdgeInsets.all(18.0),
+ child: Column(
+ spacing: 12.0,
+ children: [
+ Container(
+ padding: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(18),
+ ),
+ child: AnimatedSize(
+ duration: FluffyThemes.animationDuration,
+ child: _activityIsOver
+ ? Column(
+ spacing: 12.0,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ L10n.of(context).activityEnded,
+ style: TextStyle(
+ color: theme.colorScheme.onPrimaryContainer,
+ fontSize: 16.0,
+ ),
+ ),
+ CachedNetworkImage(
+ width: 120.0,
+ imageUrl:
+ "${AppConfig.assetsBaseURL}/${ActivityConstants.activityFinishedAsset}",
+ fit: BoxFit.cover,
+ placeholder: (context, url) => const Center(
+ child: CircularProgressIndicator(),
+ ),
+ errorWidget: (context, url, error) =>
+ const SizedBox(),
+ ),
+ ],
+ )
+ : Text(
+ activityPlan!.markdown,
+ style: TextStyle(
+ color: theme.colorScheme.onPrimaryContainer,
+ fontSize: AppConfig.fontSizeFactor *
+ AppConfig.messageFontSize,
+ ),
+ ),
+ ),
+ ),
+ AnimatedSize(
+ duration: FluffyThemes.animationDuration,
+ child: _activityIsOver
+ ? const SizedBox()
+ : IntrinsicHeight(
+ child: Row(
+ spacing: 12.0,
+ children: [
+ Container(
+ height: imageWidth,
+ width: imageWidth,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(16),
+ child: activityPlan!.imageURL != null
+ ? activityPlan!.imageURL!.startsWith("mxc")
+ ? MxcImage(
+ uri: Uri.parse(
+ activityPlan!.imageURL!,
+ ),
+ width: imageWidth,
+ height: imageWidth,
+ cacheKey: activityPlan!.bookmarkId,
+ fit: BoxFit.cover,
+ )
+ : CachedNetworkImage(
+ imageUrl: activityPlan!.imageURL!,
+ fit: BoxFit.cover,
+ placeholder: (context, url) =>
+ const Center(
+ child: CircularProgressIndicator(),
+ ),
+ errorWidget: (
+ context,
+ url,
+ error,
+ ) =>
+ const SizedBox(),
+ )
+ : const SizedBox(),
+ ),
+ ),
+ Expanded(
+ child: Column(
+ spacing: 9.0,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Expanded(
+ child: SizedBox.expand(
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ shape: RoundedRectangleBorder(
+ borderRadius:
+ BorderRadius.circular(20),
+ ),
+ backgroundColor:
+ theme.colorScheme.primaryContainer,
+ foregroundColor: theme
+ .colorScheme.onPrimaryContainer,
+ ),
+ onPressed: () async {
+ final Duration? duration =
+ await showDialog(
+ context: context,
+ builder: (context) {
+ return ActivityDurationPopup(
+ initialValue:
+ activityPlan?.duration ??
+ const Duration(days: 1),
+ );
+ },
+ );
+
+ if (duration == null) return;
+
+ showFutureLoadingDialog(
+ context: context,
+ future: () => widget.event.room
+ .sendActivityPlan(
+ activityPlan!.copyWith(
+ endAt:
+ DateTime.now().add(duration),
+ duration: duration,
+ ),
+ ),
+ );
+ },
+ child: CountDown(
+ deadline: activityPlan!.endAt,
+ iconSize: 20.0,
+ textSize: 16.0,
+ ),
+ ),
+ ),
+ ), // Optional spacing between buttons
+ Expanded(
+ child: SizedBox.expand(
+ child: ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ shape: RoundedRectangleBorder(
+ borderRadius:
+ BorderRadius.circular(20),
+ ),
+ backgroundColor:
+ theme.colorScheme.error,
+ foregroundColor:
+ theme.colorScheme.onPrimary,
+ ),
+ onPressed: () {
+ showFutureLoadingDialog(
+ context: context,
+ future: () => widget.event.room
+ .sendActivityPlan(
+ activityPlan!.copyWith(
+ endAt: DateTime.now(),
+ duration: Duration.zero,
+ ),
+ ),
+ );
+ },
+ child: Text(
+ L10n.of(context).endNow,
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pangea/activities/countdown.dart b/lib/pangea/activities/countdown.dart
new file mode 100644
index 000000000..17516cb23
--- /dev/null
+++ b/lib/pangea/activities/countdown.dart
@@ -0,0 +1,98 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+import 'package:fluffychat/l10n/l10n.dart';
+
+class CountDown extends StatefulWidget {
+ final DateTime? deadline;
+
+ final double? iconSize;
+ final double? textSize;
+
+ const CountDown({
+ super.key,
+ required this.deadline,
+ this.iconSize,
+ this.textSize,
+ });
+
+ @override
+ State createState() => CountDownState();
+}
+
+class CountDownState extends State {
+ Timer? _timer;
+
+ @override
+ void initState() {
+ super.initState();
+ _timer = Timer.periodic(const Duration(seconds: 1), (_) {
+ setState(() {});
+ });
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ String? _formatDuration(Duration duration) {
+ final days = duration.inDays;
+ final hours = duration.inHours.remainder(24);
+ final minutes = duration.inMinutes.remainder(60);
+ final seconds = duration.inSeconds.remainder(60);
+
+ final List parts = [];
+ if (days > 0) parts.add("${days}d");
+ if (hours > 0) parts.add("${hours}h");
+ if (minutes > 0) parts.add("${minutes}m");
+ if (seconds > 0 && minutes <= 0) parts.add("${seconds}s");
+ if (parts.isEmpty) return null;
+
+ return parts.join(" ");
+ }
+
+ Duration? get _remainingTime {
+ if (widget.deadline == null) {
+ return null;
+ }
+
+ final now = DateTime.now();
+ return widget.deadline!.difference(now);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final remainingTime = _remainingTime;
+ final durationString = _formatDuration(remainingTime ?? Duration.zero);
+
+ return ConstrainedBox(
+ constraints: const BoxConstraints(
+ maxWidth: 250.0,
+ ),
+ child: Row(
+ spacing: 4.0,
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ Icons.timer_outlined,
+ size: widget.iconSize ?? 28.0,
+ ),
+ Flexible(
+ child: Text(
+ remainingTime != null &&
+ remainingTime >= Duration.zero &&
+ durationString != null
+ ? durationString
+ : L10n.of(context).duration,
+ style: TextStyle(fontSize: widget.textSize ?? 20),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/pangea/activities/pinned_activity_message.dart b/lib/pangea/activities/pinned_activity_message.dart
new file mode 100644
index 000000000..8be5a492f
--- /dev/null
+++ b/lib/pangea/activities/pinned_activity_message.dart
@@ -0,0 +1,100 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+import 'package:collection/collection.dart';
+import 'package:matrix/matrix.dart';
+
+import 'package:fluffychat/pages/chat/chat.dart';
+import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
+import 'package:fluffychat/pangea/activities/activity_aware_builder.dart';
+import 'package:fluffychat/pangea/activities/activity_duration_popup.dart';
+import 'package:fluffychat/pangea/activities/countdown.dart';
+import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
+import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
+import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
+import 'package:fluffychat/widgets/future_loading_dialog.dart';
+
+class PinnedActivityMessage extends StatelessWidget {
+ final ChatController controller;
+
+ const PinnedActivityMessage(this.controller, {super.key});
+
+ Future _scrollToEvent() async {
+ final eventId = _activityPlanEvent?.eventId;
+ if (eventId != null) controller.scrollToEventId(eventId);
+ }
+
+ Event? get _activityPlanEvent => controller.timeline?.events.firstWhereOrNull(
+ (event) => event.type == PangeaEventTypes.activityPlan,
+ );
+
+ ActivityPlanModel? get _activityPlan => controller.room.activityPlan;
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ return ActivityAwareBuilder(
+ deadline: _activityPlan?.endAt,
+ builder: (isActive) {
+ if (!isActive || _activityPlan == null) {
+ return const SizedBox.shrink();
+ }
+
+ return ChatAppBarListTile(
+ title: _activityPlan!.title,
+ leading: IconButton(
+ splashRadius: 18,
+ iconSize: 18,
+ color: theme.colorScheme.onSurfaceVariant,
+ icon: const Icon(Icons.push_pin),
+ onPressed: () {},
+ ),
+ trailing: Padding(
+ padding: const EdgeInsets.only(right: 16.0),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(20),
+ onTap: () async {
+ final Duration? duration = await showDialog(
+ context: context,
+ builder: (context) {
+ return ActivityDurationPopup(
+ initialValue:
+ _activityPlan?.duration ?? const Duration(days: 1),
+ );
+ },
+ );
+
+ if (duration == null) return;
+
+ showFutureLoadingDialog(
+ context: context,
+ future: () => controller.room.sendActivityPlan(
+ _activityPlan!.copyWith(
+ endAt: DateTime.now().add(duration),
+ duration: duration,
+ ),
+ ),
+ );
+ },
+ child: Container(
+ padding: const EdgeInsets.all(4.0),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: CountDown(
+ deadline: _activityPlan!.endAt,
+ iconSize: 16.0,
+ textSize: 14.0,
+ ),
+ ),
+ ),
+ ),
+ onTap: _scrollToEvent,
+ );
+ },
+ );
+ }
+}
diff --git a/lib/pangea/activity_planner/activity_plan_model.dart b/lib/pangea/activity_planner/activity_plan_model.dart
index bd37039fb..7ea09a68a 100644
--- a/lib/pangea/activity_planner/activity_plan_model.dart
+++ b/lib/pangea/activity_planner/activity_plan_model.dart
@@ -11,6 +11,8 @@ class ActivityPlanModel {
final String instructions;
final List vocab;
final String? imageURL;
+ final DateTime? endAt;
+ final Duration? duration;
ActivityPlanModel({
required this.req,
@@ -19,31 +21,70 @@ class ActivityPlanModel {
required this.instructions,
required this.vocab,
this.imageURL,
+ this.endAt,
+ this.duration,
}) : bookmarkId =
"${title.hashCode ^ learningObjective.hashCode ^ instructions.hashCode ^ imageURL.hashCode ^ vocab.map((v) => v.hashCode).reduce((a, b) => a ^ b)}";
+ ActivityPlanModel copyWith({
+ String? title,
+ String? learningObjective,
+ String? instructions,
+ List? vocab,
+ String? imageURL,
+ DateTime? endAt,
+ Duration? duration,
+ }) {
+ return ActivityPlanModel(
+ req: req,
+ title: title ?? this.title,
+ learningObjective: learningObjective ?? this.learningObjective,
+ instructions: instructions ?? this.instructions,
+ vocab: vocab ?? this.vocab,
+ imageURL: imageURL ?? this.imageURL,
+ endAt: endAt ?? this.endAt,
+ duration: duration ?? this.duration,
+ );
+ }
+
factory ActivityPlanModel.fromJson(Map json) {
return ActivityPlanModel(
+ imageURL: json[ModelKey.activityPlanImageURL],
+ instructions: json[ModelKey.activityPlanInstructions],
req: ActivityPlanRequest.fromJson(json[ModelKey.activityPlanRequest]),
title: json[ModelKey.activityPlanTitle],
learningObjective: json[ModelKey.activityPlanLearningObjective],
- instructions: json[ModelKey.activityPlanInstructions],
vocab: List.from(
json[ModelKey.activityPlanVocab].map((vocab) => Vocab.fromJson(vocab)),
),
- imageURL: json[ModelKey.activityPlanImageURL],
+ endAt: json[ModelKey.activityPlanEndAt] != null
+ ? DateTime.parse(json[ModelKey.activityPlanEndAt])
+ : null,
+ duration: json[ModelKey.activityPlanDuration] != null
+ ? Duration(
+ days: json[ModelKey.activityPlanDuration]['days'] ?? 0,
+ hours: json[ModelKey.activityPlanDuration]['hours'] ?? 0,
+ minutes: json[ModelKey.activityPlanDuration]['minutes'] ?? 0,
+ )
+ : null,
);
}
Map toJson() {
return {
+ ModelKey.activityPlanBookmarkId: bookmarkId,
+ ModelKey.activityPlanImageURL: imageURL,
+ ModelKey.activityPlanInstructions: instructions,
ModelKey.activityPlanRequest: req.toJson(),
ModelKey.activityPlanTitle: title,
ModelKey.activityPlanLearningObjective: learningObjective,
- ModelKey.activityPlanInstructions: instructions,
ModelKey.activityPlanVocab: vocab.map((vocab) => vocab.toJson()).toList(),
- ModelKey.activityPlanImageURL: imageURL,
- ModelKey.activityPlanBookmarkId: bookmarkId,
+ ModelKey.activityPlanEndAt: endAt?.toIso8601String(),
+ ModelKey.activityPlanDuration: {
+ 'days': duration?.inDays ?? 0,
+ 'hours': duration?.inHours.remainder(24) ?? 0,
+ 'minutes': duration?.inMinutes.remainder(60) ?? 0,
+ },
};
}
diff --git a/lib/pangea/activity_suggestions/activity_room_selection.dart b/lib/pangea/activity_suggestions/activity_room_selection.dart
index 4ca587ce5..d0d91a78c 100644
--- a/lib/pangea/activity_suggestions/activity_room_selection.dart
+++ b/lib/pangea/activity_suggestions/activity_room_selection.dart
@@ -69,6 +69,13 @@ class ActivityRoomSelectionState extends State {
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
});
+ final room = widget.controller.room;
+ if (room != null && room.isSpace) {
+ _launchableRooms = _launchableRooms.where((r) {
+ return room.spaceChildren.any((child) => child.roomId == r.id);
+ }).toList();
+ }
+
_hasBotDM = Matrix.of(context).client.rooms.any((room) {
if (room.isDirectChat &&
room.directChatMatrixID == BotName.byEnvironment) {
diff --git a/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart b/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart
index 5c9dabef8..a24d8d7e7 100644
--- a/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart
+++ b/lib/pangea/analytics_summary/progress_bar/progress_bar_background.dart
@@ -13,14 +13,17 @@ class ProgressBarBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container(
- height: details.height,
- width: details.totalWidth,
- decoration: BoxDecoration(
- borderRadius: const BorderRadius.all(
- Radius.circular(AppConfig.borderRadius),
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 2),
+ child: Container(
+ height: details.height,
+ width: details.totalWidth,
+ decoration: BoxDecoration(
+ borderRadius: const BorderRadius.all(
+ Radius.circular(AppConfig.borderRadius),
+ ),
+ color: details.borderColor.withAlpha(50),
),
- color: details.borderColor.withAlpha(50),
),
);
}
diff --git a/lib/pangea/common/constants/model_keys.dart b/lib/pangea/common/constants/model_keys.dart
index 781667c34..22b360c8d 100644
--- a/lib/pangea/common/constants/model_keys.dart
+++ b/lib/pangea/common/constants/model_keys.dart
@@ -163,6 +163,8 @@ class ModelKey {
static const String activityPlanVocab = "vocab";
static const String activityPlanImageURL = "image_url";
static const String activityPlanBookmarkId = "bookmark_id";
+ static const String activityPlanEndAt = "end_at";
+ static const String activityPlanDuration = "duration";
static const String activityRequestTopic = "topic";
static const String activityRequestMode = "mode";
diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart
index 316ef7199..58616de36 100644
--- a/lib/pangea/extensions/pangea_room_extension.dart
+++ b/lib/pangea/extensions/pangea_room_extension.dart
@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:html_unescape/html_unescape.dart';
-import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/markdown.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart
index 3da8c1272..e24427ef3 100644
--- a/lib/pangea/extensions/room_events_extension.dart
+++ b/lib/pangea/extensions/room_events_extension.dart
@@ -277,68 +277,13 @@ extension EventsRoomExtension on Room {
}) async {
BookmarkedActivitiesRepo.save(activity);
- String? imageURL = activity.imageURL;
- final eventId = await pangeaSendTextEvent(
- activity.markdown,
- messageTag: ModelKey.messageTagActivityPlan,
- );
-
- Uint8List? bytes = avatar;
- if (imageURL != null && bytes == null) {
- try {
- final resp = await http
- .get(Uri.parse(imageURL))
- .timeout(const Duration(seconds: 5));
- bytes = resp.bodyBytes;
- } catch (e, s) {
- ErrorHandler.logError(
- e: e,
- s: s,
- data: {
- "avatarURL": imageURL,
- },
- );
- }
- }
-
- if (bytes != null && imageURL == null) {
- final url = await client.uploadContent(
- bytes,
- filename: filename,
- );
- imageURL = url.toString();
- }
-
- MatrixFile? file;
- if (filename != null && bytes != null) {
- file = MatrixFile(
- bytes: bytes,
- name: filename,
- );
- }
-
- if (file != null) {
- final content = {
- 'msgtype': file.msgType,
- 'body': file.name,
- 'filename': file.name,
- 'url': imageURL,
- ModelKey.messageTags: ModelKey.messageTagActivityPlan,
- };
- await sendEvent(content);
- }
-
- if (canSendDefaultStates) {
+ if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
await client.setRoomStateWithKey(
id,
PangeaEventTypes.activityPlan,
"",
activity.toJson(),
);
-
- if (eventId != null) {
- await setPinnedEvents([eventId]);
- }
}
}
diff --git a/lib/pangea/lemmas/construct_xp_widget.dart b/lib/pangea/lemmas/construct_xp_widget.dart
index 34a73a92c..22bb38d18 100644
--- a/lib/pangea/lemmas/construct_xp_widget.dart
+++ b/lib/pangea/lemmas/construct_xp_widget.dart
@@ -91,7 +91,7 @@ class ConstructXpWidgetState extends State
Stream get stream =>
MatrixState.pangeaController.getAnalytics.analyticsStream.stream;
- Widget get svg => constructLemmaCategory?.icon() ?? const SizedBox();
+ Widget? get svg => constructLemmaCategory?.icon();
@override
void dispose() {
@@ -106,9 +106,10 @@ class ConstructXpWidgetState extends State
width: widget.size,
height: widget.size,
child: GestureDetector(
- onTap: widget.onTap,
+ onTap: svg != null ? widget.onTap : null,
child: MouseRegion(
- cursor: SystemMouseCursors.click,
+ cursor:
+ svg != null ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: Stack(
alignment: Alignment.center,
children: [
diff --git a/lib/pangea/onboarding/onboarding_complete.dart b/lib/pangea/onboarding/onboarding_complete.dart
index 796602d28..fcef4d8ab 100644
--- a/lib/pangea/onboarding/onboarding_complete.dart
+++ b/lib/pangea/onboarding/onboarding_complete.dart
@@ -41,7 +41,7 @@ class OnboardingComplete extends StatelessWidget {
L10n.of(context).getStartedComplete,
textAlign: TextAlign.center,
style: const TextStyle(
- fontSize: 32.0,
+ fontSize: 14,
),
)
: Column(
diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart
index b1fe93f75..eb5cc59f5 100644
--- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart
+++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart
@@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart';
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
+import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PhoneticTranscriptionWidget extends StatefulWidget {
@@ -34,51 +35,61 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
class _PhoneticTranscriptionWidgetState
extends State {
- late Future _transcriptionFuture;
- bool _hovering = false;
bool _isPlaying = false;
bool _isLoading = false;
- late final StreamSubscription _loadingChoreoSubscription;
+ Object? _error;
+
+ String? _transcription;
@override
void initState() {
super.initState();
- _transcriptionFuture = _fetchTranscription();
- _loadingChoreoSubscription =
- TtsController.loadingChoreoStream.stream.listen((val) {
- if (mounted) setState(() => _isLoading = val);
- });
+ _fetchTranscription();
}
- @override
- void dispose() {
- TtsController.stop();
- _loadingChoreoSubscription.cancel();
- super.dispose();
- }
+ Future _fetchTranscription() async {
+ try {
+ setState(() {
+ _isLoading = true;
+ _error = null;
+ _transcription = null;
+ });
- Future _fetchTranscription() async {
- if (MatrixState.pangeaController.languageController.userL1 == null) {
+ if (MatrixState.pangeaController.languageController.userL1 == null) {
+ ErrorHandler.logError(
+ e: Exception('User L1 is not set'),
+ data: {
+ 'text': widget.text,
+ 'textLanguageCode': widget.textLanguage.langCode,
+ },
+ );
+ _error = Exception('User L1 is not set');
+ return;
+ }
+ final req = PhoneticTranscriptionRequest(
+ arc: LanguageArc(
+ l1: MatrixState.pangeaController.languageController.userL1!,
+ l2: widget.textLanguage,
+ ),
+ content: PangeaTokenText.fromString(widget.text),
+ // arc can be omitted for default empty map
+ );
+ final res = await PhoneticTranscriptionRepo.get(req);
+ _transcription = res.phoneticTranscriptionResult.phoneticTranscription
+ .first.phoneticL1Transcription.content;
+ } catch (e, s) {
+ _error = e;
ErrorHandler.logError(
- e: Exception('User L1 is not set'),
+ e: e,
+ s: s,
data: {
'text': widget.text,
'textLanguageCode': widget.textLanguage.langCode,
},
);
- return widget.text; // Fallback to original text if no L1 is set
+ } finally {
+ if (mounted) setState(() => _isLoading = false);
}
- final req = PhoneticTranscriptionRequest(
- arc: LanguageArc(
- l1: MatrixState.pangeaController.languageController.userL1!,
- l2: widget.textLanguage,
- ),
- content: PangeaTokenText.fromString(widget.text),
- // arc can be omitted for default empty map
- );
- final res = await PhoneticTranscriptionRepo.get(req);
- return res.phoneticTranscriptionResult.phoneticTranscription.first
- .phoneticL1Transcription.content;
}
Future _handleAudioTap(BuildContext context) async {
@@ -103,65 +114,71 @@ class _PhoneticTranscriptionWidgetState
@override
Widget build(BuildContext context) {
- return FutureBuilder(
- future: _transcriptionFuture,
- builder: (context, snapshot) {
- final transcription = snapshot.data ?? '';
- return MouseRegion(
- onEnter: (_) => setState(() => _hovering = true),
- onExit: (_) => setState(() => _hovering = false),
- child: GestureDetector(
- onTap: () => _handleAudioTap(context),
- child: AnimatedContainer(
- duration: const Duration(milliseconds: 150),
- decoration: BoxDecoration(
- color: _hovering
- ? Colors.grey.withAlpha((0.2 * 255).round())
- : Colors.transparent,
- borderRadius: BorderRadius.circular(6),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
+ return HoverBuilder(
+ builder: (context, hovering) {
+ return GestureDetector(
+ onTap: () => _handleAudioTap(context),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 150),
+ decoration: BoxDecoration(
+ color: hovering
+ ? Colors.grey.withAlpha((0.2 * 255).round())
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(6),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_error != null)
+ Row(
+ spacing: 8.0,
+ children: [
+ Icon(
+ Icons.error_outline,
+ size: widget.iconSize ?? 24,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ Text(
+ L10n.of(context).failedToFetchTranscription,
+ style: widget.style,
+ ),
+ ],
+ )
+ else if (_isLoading || _transcription == null)
+ const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator.adaptive(),
+ )
+ else
Flexible(
child: Text(
- "/${transcription.isNotEmpty ? transcription : widget.text}/",
+ "/$_transcription/",
style: widget.style ??
Theme.of(context).textTheme.bodyMedium,
),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 8),
+ if (_transcription != null && _error == null)
Tooltip(
message: _isPlaying
? L10n.of(context).stop
: L10n.of(context).playAudio,
- child: _isLoading
- ? const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 3),
- )
- : Icon(
- _isPlaying ? Icons.pause_outlined : Icons.volume_up,
- size: widget.iconSize ?? 24,
- color: widget.lightBackground ??
- Theme.of(context).brightness ==
- Brightness.light
- ? _isPlaying
- ? Theme.of(context)
- .colorScheme
- .onPrimaryFixed
- : Theme.of(context)
- .colorScheme
- .onTertiaryFixed
- : _isPlaying
- ? Theme.of(context).colorScheme.primary
- : Theme.of(context).iconTheme.color,
- ),
+ child: Icon(
+ _isPlaying ? Icons.pause_outlined : Icons.volume_up,
+ size: widget.iconSize ?? 24,
+ color: widget.lightBackground ??
+ Theme.of(context).brightness == Brightness.light
+ ? _isPlaying
+ ? Theme.of(context).colorScheme.onPrimaryFixed
+ : Theme.of(context).colorScheme.onTertiaryFixed
+ : _isPlaying
+ ? Theme.of(context).colorScheme.primary
+ : Theme.of(context).iconTheme.color,
+ ),
),
- ],
- ),
+ ],
),
),
);
diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart
index 2fe80c418..c58bdd4e2 100644
--- a/lib/pangea/practice_activities/activity_type_enum.dart
+++ b/lib/pangea/practice_activities/activity_type_enum.dart
@@ -190,4 +190,20 @@ extension ActivityTypeExtension on ActivityTypeEnum {
return null; // TODO: Add to L10n
}
}
+
+ /// The minimum number of tokens in a message for this activity type to be available.
+ /// Matching activities don't make sense for a single-word message.
+ int get minTokensForMatchActivity {
+ switch (this) {
+ case ActivityTypeEnum.wordMeaning:
+ case ActivityTypeEnum.lemmaId:
+ case ActivityTypeEnum.wordFocusListening:
+ return 2;
+ case ActivityTypeEnum.hiddenWordListening:
+ case ActivityTypeEnum.emoji:
+ case ActivityTypeEnum.morphId:
+ case ActivityTypeEnum.messageMeaning:
+ return 1;
+ }
+ }
}
diff --git a/lib/pangea/practice_activities/practice_selection.dart b/lib/pangea/practice_activities/practice_selection.dart
index de2aa3709..e7b60be33 100644
--- a/lib/pangea/practice_activities/practice_selection.dart
+++ b/lib/pangea/practice_activities/practice_selection.dart
@@ -155,6 +155,11 @@ class PracticeSelection {
return [];
}
+ if (tokens.length < activityType.minTokensForMatchActivity) {
+ // if we only have one token, we don't need to do an emoji activity
+ return [];
+ }
+
return [
PracticeTarget(
activityType: activityType,
diff --git a/lib/pangea/toolbar/controllers/tts_controller.dart b/lib/pangea/toolbar/controllers/tts_controller.dart
index 57c42ace4..32e575ae1 100644
--- a/lib/pangea/toolbar/controllers/tts_controller.dart
+++ b/lib/pangea/toolbar/controllers/tts_controller.dart
@@ -35,6 +35,8 @@ class TtsController {
static final StreamController loadingChoreoStream =
StreamController.broadcast();
+ static final audioPlayer = AudioPlayer();
+
static bool get _useAlternativeTTS {
return PlatformInfos.isWindows;
}
@@ -120,6 +122,7 @@ class TtsController {
// https://pub.dev/packages/flutter_tts
final result =
await (_useAlternativeTTS ? _alternativeTTS.stop() : _tts.stop());
+ audioPlayer.stop();
if (!_useAlternativeTTS && result != 1) {
ErrorHandler.logError(
@@ -187,6 +190,8 @@ class TtsController {
VoidCallback? onStop,
}) async {
chatController?.stopMediaStream.add(null);
+ MatrixState.pangeaController.matrixState.audioPlayer?.stop();
+
await _setSpeakingLanguage(langCode);
final enableTTS = MatrixState
@@ -306,7 +311,6 @@ class TtsController {
if (ttsRes == null) return;
- final audioPlayer = AudioPlayer();
try {
Logs().i('Speaking from choreo: $text, langCode: $langCode');
final audioContent = base64Decode(ttsRes.audioContent);
@@ -326,8 +330,6 @@ class TtsController {
'text': text,
},
);
- } finally {
- await audioPlayer.dispose();
}
}
diff --git a/lib/pangea/toolbar/widgets/overlay_header.dart b/lib/pangea/toolbar/widgets/overlay_header.dart
index cb4db5d21..ff85e6b0b 100644
--- a/lib/pangea/toolbar/widgets/overlay_header.dart
+++ b/lib/pangea/toolbar/widgets/overlay_header.dart
@@ -36,6 +36,9 @@ class OverlayHeaderState extends State {
Widget build(BuildContext context) {
final l10n = L10n.of(context);
final theme = Theme.of(context);
+ final pinned = controller.room.pinnedEventIds.contains(
+ controller.selectedEvents.first.eventId,
+ );
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
@@ -102,9 +105,11 @@ class OverlayHeaderState extends State {
),
if (controller.canPinSelectedEvents)
IconButton(
- icon: const Icon(Icons.push_pin_outlined),
+ icon: pinned
+ ? const Icon(Icons.push_pin)
+ : const Icon(Icons.push_pin_outlined),
onPressed: controller.pinEvent,
- tooltip: l10n.pinMessage,
+ tooltip: pinned ? l10n.unpin : l10n.pinMessage,
color: theme.colorScheme.primary,
),
if (controller.canEditSelectedEvents &&
diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart
index 07f5d3722..7be70a778 100644
--- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart
+++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart
@@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
+import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/widgets/matrix.dart';
@@ -76,7 +77,6 @@ class SelectModeButtonsState extends State {
SelectMode? _selectedMode;
- final AudioPlayer _audioPlayer = AudioPlayer();
bool _isLoadingAudio = false;
PangeaAudioFile? _audioBytes;
File? _audioFile;
@@ -93,17 +93,26 @@ class SelectModeButtonsState extends State {
Completer? _transcriptionCompleter;
+ AudioPlayer? get _audioPlayer => Matrix.of(context).audioPlayer!;
+
@override
void initState() {
super.initState();
- _onPlayerStateChanged = _audioPlayer.playerStateStream.listen((state) {
+
+ final matrix = Matrix.of(context);
+ matrix.audioPlayer?.dispose();
+ matrix.audioPlayer = AudioPlayer();
+ matrix.voiceMessageEventId.value =
+ widget.overlayController.pangeaMessageEvent?.eventId;
+
+ _onPlayerStateChanged = _audioPlayer?.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
_updateMode(null);
}
setState(() {});
});
- _onAudioPositionChanged ??= _audioPlayer.positionStream.listen((state) {
+ _onAudioPositionChanged ??= _audioPlayer?.positionStream.listen((state) {
if (_audioBytes != null) {
widget.overlayController.highlightCurrentText(
state.inMilliseconds,
@@ -119,7 +128,10 @@ class SelectModeButtonsState extends State {
@override
void dispose() {
- _audioPlayer.dispose();
+ _audioPlayer?.dispose();
+ Matrix.of(context).audioPlayer = null;
+ Matrix.of(context).voiceMessageEventId.value = null;
+
_onPlayerStateChanged?.cancel();
_onAudioPositionChanged?.cancel();
super.dispose();
@@ -150,8 +162,8 @@ class SelectModeButtonsState extends State {
if (mode == null) {
setState(() {
- _audioPlayer.stop();
- _audioPlayer.seek(null);
+ _audioPlayer?.stop();
+ _audioPlayer?.seek(null);
_selectedMode = null;
});
return;
@@ -166,8 +178,8 @@ class SelectModeButtonsState extends State {
_playAudio();
return;
} else {
- _audioPlayer.stop();
- _audioPlayer.seek(null);
+ _audioPlayer?.stop();
+ _audioPlayer?.seek(null);
}
if (_selectedMode == SelectMode.practice) {
@@ -232,11 +244,12 @@ class SelectModeButtonsState extends State {
Future _playAudio() async {
try {
- if (_audioPlayer.playerState.playing) {
- await _audioPlayer.pause();
+ if (_audioPlayer != null && _audioPlayer!.playerState.playing) {
+ await _audioPlayer?.pause();
return;
- } else if (_audioPlayer.position != Duration.zero) {
- await _audioPlayer.play();
+ } else if (_audioPlayer?.position != Duration.zero) {
+ TtsController.stop();
+ await _audioPlayer?.play();
return;
}
@@ -247,16 +260,18 @@ class SelectModeButtonsState extends State {
if (_audioBytes == null) return;
if (_audioFile != null) {
- await _audioPlayer.setFilePath(_audioFile!.path);
+ await _audioPlayer?.setFilePath(_audioFile!.path);
} else {
- await _audioPlayer.setAudioSource(
+ await _audioPlayer?.setAudioSource(
BytesAudioSource(
_audioBytes!.bytes,
_audioBytes!.mimeType,
),
);
}
- _audioPlayer.play();
+
+ TtsController.stop();
+ _audioPlayer?.play();
} catch (e, s) {
setState(() => _audioError = e.toString());
ErrorHandler.logError(
@@ -426,7 +441,7 @@ class SelectModeButtonsState extends State {
if (mode == SelectMode.audio) {
return Icon(
- _audioPlayer.playerState.playing == true
+ _audioPlayer?.playerState.playing == true
? Icons.pause_outlined
: Icons.volume_up,
size: 20,
diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart
index fee1b3c62..f074a43f1 100644
--- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart
+++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart
@@ -1,6 +1,7 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
+import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import '../../config/app_config.dart';
extension VisibleInGuiExtension on List {
@@ -46,7 +47,12 @@ extension IsStateExtension on Event {
// if we enabled to hide all redacted events, don't show those
(!AppConfig.hideRedactedEvents || !redacted) &&
// if we enabled to hide all unknown events, don't show those
- (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
+ // #Pangea
+ // (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
+ (!AppConfig.hideUnknownEvents ||
+ isEventTypeKnown ||
+ importantStateEvents.contains(type)) &&
+ // Pangea#
// remove state events that we don't want to render
(isState || !AppConfig.hideAllStateEvents) &&
// #Pangea
@@ -82,6 +88,7 @@ extension IsStateExtension on Event {
EventTypes.RoomMember,
EventTypes.RoomTombstone,
EventTypes.CallInvite,
+ PangeaEventTypes.activityPlan,
};
// Pangea#
}