Merge branch 'main' of https://github.com/pangeachat/client into phonetic-audio-color

This commit is contained in:
Kelrap 2025-06-18 15:03:12 -04:00
commit 19fa984a52
40 changed files with 1332 additions and 686 deletions

View file

@ -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#

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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#

View file

@ -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
cd ..

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5017,6 +5017,10 @@
"newDirectMessage": "New direct message",
"speakingExercisesTooltip": "Speaking practice",
"noChatsFoundHereYet": "No chats found here yet",
"endNow": "End now",
"setDuration": "Set duration",
"activityEnded": "Thats 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 dont 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"
}

View file

@ -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(

View file

@ -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('<mx-reply>'),
);
if (replyTagIndex != -1) {
final closingReplyTagIndex = result.indexWhere(
(string) => string.contains('</mx-reply>'),
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,
),

View file

@ -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);
}

View file

@ -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),
);

View file

@ -743,20 +743,24 @@ class ChatListController extends State<ChatList>
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,
),
),
],
),

View file

@ -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,

View file

@ -38,6 +38,8 @@ class NewGroupController extends State<NewGroup> {
TextEditingController nameController = TextEditingController();
// #Pangea
// bool publicGroup = false;
// bool groupCanBeFound = false;
ActivityPlanModel? selectedActivity;
Uint8List? selectedActivityImage;
String? selectedActivityImageFilename;
@ -45,12 +47,8 @@ class NewGroupController extends State<NewGroup> {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
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<NewGroup> {
// #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<NewGroup> {
}
// 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<NewGroup> {
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,
);

View file

@ -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,

View file

@ -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<ActivityAwareBuilder> createState() => ActivityAwareBuilderState();
}
class ActivityAwareBuilderState extends State<ActivityAwareBuilder> {
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()),
);
}
}

View file

@ -0,0 +1,3 @@
class ActivityConstants {
static const String activityFinishedAsset = "EndActivityMsg.png";
}

View file

@ -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<ActivityDurationPopup> createState() => ActivityDurationPopupState();
}
class ActivityDurationPopupState extends State<ActivityDurationPopup> {
final TextEditingController _daysController = TextEditingController();
final TextEditingController _hoursController = TextEditingController();
final TextEditingController _minutesController = TextEditingController();
String? error;
final List<Duration> _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<String> 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)),
],
);
}
}

View file

@ -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<ActivityStateEvent> createState() => ActivityStateEventState();
}
class ActivityStateEventState extends State<ActivityStateEvent> {
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,
),
),
),
),
),
],
),
),
],
),
),
),
],
),
),
);
}
}

View file

@ -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<CountDown> createState() => CountDownState();
}
class CountDownState extends State<CountDown> {
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<String> 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),
),
),
],
),
);
}
}

View file

@ -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<void> _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,
);
},
);
}
}

View file

@ -11,6 +11,8 @@ class ActivityPlanModel {
final String instructions;
final List<Vocab> 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>? 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<String, dynamic> 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<Vocab>.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<String, dynamic> 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,
},
};
}

View file

@ -69,6 +69,13 @@ class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
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) {

View file

@ -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),
),
);
}

View file

@ -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";

View file

@ -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';

View file

@ -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 = <String, dynamic>{
'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]);
}
}
}

View file

@ -91,7 +91,7 @@ class ConstructXpWidgetState extends State<ConstructXpWidget>
Stream<AnalyticsStreamUpdate> 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<ConstructXpWidget>
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: [

View file

@ -41,7 +41,7 @@ class OnboardingComplete extends StatelessWidget {
L10n.of(context).getStartedComplete,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 32.0,
fontSize: 14,
),
)
: Column(

View file

@ -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<PhoneticTranscriptionWidget> {
late Future<String?> _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<void> _fetchTranscription() async {
try {
setState(() {
_isLoading = true;
_error = null;
_transcription = null;
});
Future<String?> _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<void> _handleAudioTap(BuildContext context) async {
@ -103,65 +114,71 @@ class _PhoneticTranscriptionWidgetState
@override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
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,
),
),
],
),
],
),
),
);

View file

@ -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;
}
}
}

View file

@ -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,

View file

@ -35,6 +35,8 @@ class TtsController {
static final StreamController<bool> loadingChoreoStream =
StreamController<bool>.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();
}
}

View file

@ -36,6 +36,9 @@ class OverlayHeaderState extends State<OverlayHeader> {
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<OverlayHeader> {
),
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 &&

View file

@ -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<SelectModeButtons> {
SelectMode? _selectedMode;
final AudioPlayer _audioPlayer = AudioPlayer();
bool _isLoadingAudio = false;
PangeaAudioFile? _audioBytes;
File? _audioFile;
@ -93,17 +93,26 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
Completer<String>? _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<SelectModeButtons> {
@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<SelectModeButtons> {
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<SelectModeButtons> {
_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<SelectModeButtons> {
Future<void> _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<SelectModeButtons> {
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<SelectModeButtons> {
if (mode == SelectMode.audio) {
return Icon(
_audioPlayer.playerState.playing == true
_audioPlayer?.playerState.playing == true
? Icons.pause_outlined
: Icons.volume_up,
size: 20,

View file

@ -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<Event> {
@ -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#
}