diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 55123234c..734869ecc 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -54,7 +54,7 @@ abstract class AppConfig { final bigEmotes = event != null && event.onlyEmotes && event.numberEmotes > 0 && - event.numberEmotes <= 10; + event.numberEmotes <= 3; return TextStyle( color: textColor, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 86ebf6837..6371dc411 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5033,5 +5033,6 @@ } } }, - "failedToFetchTranscription": "Failed to fetch transcription" + "failedToFetchTranscription": "Failed to fetch transcription", + "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone." } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c028675c0..7d448025d 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -14,6 +14,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -26,6 +27,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; +import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -33,6 +35,7 @@ import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart'; import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; @@ -46,6 +49,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; @@ -149,6 +153,7 @@ class ChatController extends State StreamSubscription? _levelSubscription; StreamSubscription? _analyticsSubscription; + StreamSubscription? _botAudioSubscription; // Pangea# Room get room => sendingClient.getRoomById(roomId) ?? widget.room; @@ -473,6 +478,43 @@ class ChatController extends State ignorePointer: true, ); }); + + _botAudioSubscription = room.client.onSync.stream + .where( + (update) => update.rooms?.join?[roomId]?.timeline?.events != null, + ) + .listen((update) async { + final timeline = update.rooms!.join![roomId]!.timeline!; + final botAudioEvent = timeline.events!.firstWhereOrNull( + (e) => + e.senderId == BotName.byEnvironment && + e.content.tryGet('msgtype') == MessageTypes.Audio && + DateTime.now().difference(e.originServerTs) < + const Duration(seconds: 10), + ); + if (botAudioEvent == null) return; + + final matrix = Matrix.of(context); + matrix.voiceMessageEventId.value = botAudioEvent.eventId; + matrix.audioPlayer?.dispose(); + matrix.audioPlayer = AudioPlayer(); + + final event = Event.fromMatrixEvent(botAudioEvent, room); + final audioFile = await event.getPangeaAudioFile(); + debugPrint( + "audiofile: ${audioFile?.mimeType} ${audioFile?.bytes.length}", + ); + if (audioFile == null) return; + + matrix.audioPlayer!.setAudioSource( + BytesAudioSource( + audioFile.bytes, + audioFile.mimeType, + ), + ); + + matrix.audioPlayer!.play(); + }); // Pangea# _tryLoadTimeline(); if (kIsWeb) { @@ -719,6 +761,7 @@ class ChatController extends State stopMediaStream.close(); _levelSubscription?.cancel(); _analyticsSubscription?.cancel(); + _botAudioSubscription?.cancel(); _router.routeInformationProvider.removeListener(_onRouteChanged); //Pangea# super.dispose(); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 948c0578b..030f81422 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -10,7 +11,9 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_message.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -40,6 +43,30 @@ class ChatEventList extends StatelessWidget { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; final events = timeline.events.filterByVisibleInGui(); + // #Pangea + if (timeline.room.activityPlan?.endAt != null && + timeline.room.activityPlan!.endAt!.isBefore(DateTime.now())) { + final eventIndex = events.indexWhere( + (e) => e.originServerTs.isBefore( + timeline.room.activityPlan!.endAt!, + ), + ); + + if (eventIndex != -1) { + events.insert( + eventIndex, + Event( + type: PangeaEventTypes.activityPlanEnd, + eventId: timeline.room.client.generateUniqueTransactionId(), + senderId: timeline.room.client.userID!, + originServerTs: timeline.room.activityPlan!.endAt!, + room: timeline.room, + content: {}, + ), + ); + } + } + // Pangea# final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index c7a67c37d..11bbfba52 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -254,10 +254,10 @@ class AudioPlayerState extends State { _onAudioPositionChanged = matrix.audioPlayer!.positionStream.listen((state) { // Pass current timestamp to overlay, so it can highlight as necessary - if (widget.matrixFile != null) { + if (widget.matrixFile?.tokens != null) { widget.overlayController?.highlightCurrentText( state.inMilliseconds, - widget.matrixFile!.tokens, + widget.matrixFile!.tokens!, ); } }); @@ -308,19 +308,34 @@ class AudioPlayerState extends State { final audioPlayer = matrix.audioPlayer; if (audioPlayer == null) return; switch (audioPlayer.speed) { + // #Pangea + // case 1.0: + // await audioPlayer.setSpeed(1.25); + // break; + // case 1.25: + // await audioPlayer.setSpeed(1.5); + // break; + // case 1.5: + // await audioPlayer.setSpeed(2.0); + // break; + // case 2.0: + // await audioPlayer.setSpeed(0.5); + // break; + // case 0.5: case 1.0: + await audioPlayer.setSpeed(0.75); + break; + case 0.75: + await audioPlayer.setSpeed(0.5); + break; + case 0.5: await audioPlayer.setSpeed(1.25); break; case 1.25: await audioPlayer.setSpeed(1.5); break; case 1.5: - await audioPlayer.setSpeed(2.0); - break; - case 2.0: - await audioPlayer.setSpeed(0.5); - break; - case 0.5: + // Pangea# default: await audioPlayer.setSpeed(1.0); break; @@ -522,11 +537,13 @@ class AudioPlayerState extends State { // #Pangea // thumbColor: widget.event.senderId == // widget.event.room.client.userID + // ? theme.colorScheme.onPrimary + // : theme.colorScheme.primary, thumbColor: widget.senderId == Matrix.of(context).client.userID - // Pangea# - ? theme.colorScheme.onPrimary - : theme.colorScheme.primary, + ? widget.color + : theme.colorScheme.onSurface, + // Pangea# activeColor: waveform == null ? widget.color : Colors.transparent, @@ -568,43 +585,68 @@ class AudioPlayerState extends State { ), // Pangea# const SizedBox(width: 8), - AnimatedCrossFade( - firstChild: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon( - Icons.mic_none_outlined, - color: widget.color, - ), - ), - secondChild: Material( - color: widget.color.withAlpha(64), + // #Pangea + Material( + color: widget.color.withAlpha(64), + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + child: InkWell( borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: InkWell( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - onTap: _toggleSpeed, - child: SizedBox( - width: 32, - height: 20, - child: Center( - child: Text( - '${audioPlayer?.speed.toString()}x', - style: TextStyle( - color: widget.color, - fontSize: 9, - ), + onTap: _toggleSpeed, + child: SizedBox( + width: 32, + height: 20, + child: Center( + child: Text( + '${audioPlayer?.speed.toString() ?? 1}x', + style: TextStyle( + color: widget.color, + fontSize: 9, ), ), ), ), ), - alignment: Alignment.center, - crossFadeState: audioPlayer == null - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: FluffyThemes.animationDuration, ), + // AnimatedCrossFade( + // firstChild: Padding( + // padding: const EdgeInsets.only(right: 8.0), + // child: Icon( + // Icons.mic_none_outlined, + // color: widget.color, + // ), + // ), + // secondChild: Material( + // color: widget.color.withAlpha(64), + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // child: InkWell( + // borderRadius: + // BorderRadius.circular(AppConfig.borderRadius), + // onTap: _toggleSpeed, + // child: SizedBox( + // width: 32, + // height: 20, + // child: Center( + // child: Text( + // '${audioPlayer?.speed.toString()}x', + // style: TextStyle( + // color: widget.color, + // fontSize: 9, + // ), + // ), + // ), + // ), + // ), + // ), + // alignment: Alignment.center, + // crossFadeState: audioPlayer == null + // ? CrossFadeState.showFirst + // : CrossFadeState.showSecond, + // duration: FluffyThemes.animationDuration, + // ), + // Pangea# ], ), ), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 0fd7e79bc..30d787343 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -134,6 +134,10 @@ class Message extends StatelessWidget { ? ActivityStateEvent(event: event) : const SizedBox(); } + + if (event.type == PangeaEventTypes.activityPlanEnd) { + return const ActivityFinishedEvent(); + } // Pangea# return StateMessage(event); } diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 028d86e66..b57d8472d 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -222,10 +222,11 @@ class MessageContent extends StatelessWidget { fontSize: fontSize, // #Pangea chatController: controller, - eventId: event.eventId, + eventId: + "${event.eventId}${overlayController != null ? '_overlay' : ''}", roomId: event.room.id, senderId: event.senderId, - autoplay: overlayController != null, + autoplay: overlayController != null && isTransitionAnimation, // Pangea# ); } diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart index 412c2de7d..93ef79f1a 100644 --- a/lib/pages/chat/recording_dialog.dart +++ b/lib/pages/chat/recording_dialog.dart @@ -40,14 +40,17 @@ class RecordingDialogState extends State { Future startRecording() async { final store = Matrix.of(context).store; try { - final codec = kIsWeb - // Web seems to create webm instead of ogg when using opus encoder - // which does not play on iOS right now. So we use wav for now: - ? AudioEncoder.wav - // Everywhere else we use opus if supported by the platform: - : await _audioRecorder.isEncoderSupported(AudioEncoder.opus) - ? AudioEncoder.opus - : AudioEncoder.aacLc; + // #Pangea + // final codec = kIsWeb + // // Web seems to create webm instead of ogg when using opus encoder + // // which does not play on iOS right now. So we use wav for now: + // ? AudioEncoder.wav + // // Everywhere else we use opus if supported by the platform: + // : await _audioRecorder.isEncoderSupported(AudioEncoder.opus) + // ? AudioEncoder.opus + // : AudioEncoder.aacLc; + const codec = AudioEncoder.wav; + // Pangea# fileName = 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; String? path; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 78c5fecb7..eccc19960 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -923,7 +923,7 @@ class ChatListController extends State ), ), // #Pangea - if (room.isRoomAdmin) + if (room.isRoomAdmin && !room.isDirectChat) PopupMenuItem( value: ChatContextAction.delete, child: Row( diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart index 1f7a250e5..b59b8c233 100644 --- a/lib/pangea/activities/activity_state_event.dart +++ b/lib/pangea/activities/activity_state_event.dart @@ -57,11 +57,6 @@ class ActivityStateEventState extends State { } } - bool get _activityIsOver { - return activityPlan?.endAt != null && - DateTime.now().isAfter(activityPlan!.endAt!); - } - @override Widget build(BuildContext context) { if (activityPlan == null) { @@ -83,186 +78,151 @@ class ActivityStateEventState extends State { spacing: 12.0, children: [ Container( - padding: EdgeInsets.all(_activityIsOver ? 24.0 : 16.0), + padding: const EdgeInsets.all(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, - ), - ), + child: 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, + child: 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: [ - 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, - ), - ), + child: SizedBox.expand( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), ), - ), // 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, - ), + 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, ), ), - ], + ), ), ), ], ), ), + ], + ), + ), ), ], ), @@ -270,3 +230,50 @@ class ActivityStateEventState extends State { ); } } + +class ActivityFinishedEvent extends StatelessWidget { + const ActivityFinishedEvent({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400.0, + ), + margin: const EdgeInsets.all(18.0), + child: Container( + padding: const EdgeInsets.all(24.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(18), + ), + child: 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(), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_planner/activity_planner_page_appbar.dart b/lib/pangea/activity_planner/activity_planner_page_appbar.dart index 619a0b523..94ee72890 100644 --- a/lib/pangea/activity_planner/activity_planner_page_appbar.dart +++ b/lib/pangea/activity_planner/activity_planner_page_appbar.dart @@ -29,6 +29,17 @@ class ActivityPlannerPageAppBar extends StatelessWidget final theme = Theme.of(context); return AppBar( + leadingWidth: FluffyThemes.isColumnMode(context) ? 150.0 : null, + leading: FluffyThemes.isColumnMode(context) + ? Row( + children: [ + const SizedBox(width: 8.0), + BackButton( + onPressed: Navigator.of(context).pop, + ), + ], + ) + : null, title: pageMode == PageMode.savedActivities ? Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart index 71804a5e5..a94916231 100644 --- a/lib/pangea/analytics_misc/level_up/level_up_banner.dart +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -145,6 +145,8 @@ class LevelUpBannerState extends State LevelUpManager.instance.markPopupSeen(); _showedDetails = true; + FocusScope.of(context).unfocus(); + await showDialog( context: context, builder: (context) => const LevelUpPopup(), diff --git a/lib/pangea/chat_settings/pages/pangea_chat_details.dart b/lib/pangea/chat_settings/pages/pangea_chat_details.dart index 8c30ebecb..e40b5cf74 100644 --- a/lib/pangea/chat_settings/pages/pangea_chat_details.dart +++ b/lib/pangea/chat_settings/pages/pangea_chat_details.dart @@ -475,7 +475,7 @@ class RoomDetailsButtonRowState extends State { context.go("/rooms?spaceId=clear"); } }, - visible: room.isRoomAdmin, + visible: room.isRoomAdmin && !room.isDirectChat, showInMainView: false, ), ]; diff --git a/lib/pangea/chat_settings/widgets/delete_space_dialog.dart b/lib/pangea/chat_settings/widgets/delete_space_dialog.dart index 483fdbe8f..6afb8ab26 100644 --- a/lib/pangea/chat_settings/widgets/delete_space_dialog.dart +++ b/lib/pangea/chat_settings/widgets/delete_space_dialog.dart @@ -115,6 +115,7 @@ class DeleteSpaceDialogState extends State { child: Container( constraints: const BoxConstraints( maxWidth: 400, + maxHeight: 600, ), padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( @@ -138,75 +139,78 @@ class DeleteSpaceDialogState extends State { vertical: 8.0, ), child: Text( - L10n.of(context).deleteSpaceDesc, + widget.space.spaceChildCount > 0 + ? L10n.of(context).deleteSpaceDesc + : L10n.of(context).deleteEmptySpaceDesc, textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), - SizedBox( - height: 300, - child: Builder( - builder: (context) { - if (_loadingRooms) { - return const Center( - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator.adaptive(), + Expanded( + child: SingleChildScrollView( + child: Builder( + builder: (context) { + if (_loadingRooms) { + return const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator.adaptive(), + ), + ); + } + + if (_roomLoadError != null) { + return Center( + child: Column( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + Text(L10n.of(context).oopsSomethingWentWrong), + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ListView.builder( + shrinkWrap: true, + itemCount: _rooms.length, + itemBuilder: (context, index) { + final chunk = _rooms[index]; + + final room = + widget.space.client.getRoomById(chunk.roomId); + final isMember = room != null && + room.membership == Membership.join && + room.isRoomAdmin; + + final displayname = chunk.name ?? + chunk.canonicalAlias ?? + L10n.of(context).emptyChat; + + return AnimatedOpacity( + duration: FluffyThemes.animationDuration, + opacity: isMember ? 1 : 0.5, + child: CheckboxListTile( + value: _roomsToDelete.contains(chunk), + onChanged: isMember + ? (value) => _onRoomSelected(value, chunk) + : null, + title: Text(displayname), + controlAffinity: ListTileControlAffinity.leading, + ), + ); + }, ), ); - } - - if (_roomLoadError != null) { - return Center( - child: Column( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.error_outline, - color: Theme.of(context).colorScheme.error, - ), - Text(L10n.of(context).oopsSomethingWentWrong), - ], - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ListView.builder( - shrinkWrap: true, - itemCount: _rooms.length, - itemBuilder: (context, index) { - final chunk = _rooms[index]; - - final room = - widget.space.client.getRoomById(chunk.roomId); - final isMember = room != null && - room.membership == Membership.join && - room.isRoomAdmin; - - final displayname = chunk.name ?? - chunk.canonicalAlias ?? - L10n.of(context).emptyChat; - - return AnimatedOpacity( - duration: FluffyThemes.animationDuration, - opacity: isMember ? 1 : 0.5, - child: CheckboxListTile( - value: _roomsToDelete.contains(chunk), - onChanged: isMember - ? (value) => _onRoomSelected(value, chunk) - : null, - title: Text(displayname), - controlAffinity: ListTileControlAffinity.leading, - ), - ); - }, - ), - ); - }, + }, + ), ), ), Padding( diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index b5b704d08..98f13510d 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; @@ -17,7 +18,6 @@ import '../../common/utils/overlay.dart'; import '../controllers/it_feedback_controller.dart'; import '../models/it_response_model.dart'; import 'choice_array.dart'; -import 'igc/word_data_card.dart'; class ITBar extends StatefulWidget { final Choreographer choreographer; @@ -333,7 +333,13 @@ class ITChoices extends StatelessWidget { fullTextLang: sourceText != null ? controller.sourceLangCode : controller.targetLangCode, - hasInfo: controller.currentITStep!.continuances[index].hasInfo, + // IMPORTANT COMMENT TO KEEP: We're going to forace hasInfo to false for now + // because we don't want to show the word data card for correct choices and the contextual definition + // for incorrect choices. This gives away the answer (if you're Kel at least). + // The reason hasInfo is false for incorrect choices is that we're not includng the tokens for distractors. + // Correct choices will have the tokens, but we don't want to show something different for them. + // hasInfo: controller.currentITStep!.continuances[index].hasInfo, + hasInfo: false, choiceFeedback: choiceFeedback, room: controller.choreographer.chatController.room, ) diff --git a/lib/pangea/common/utils/overlay.dart b/lib/pangea/common/utils/overlay.dart index 73dcc1042..e2d7f76e0 100644 --- a/lib/pangea/common/utils/overlay.dart +++ b/lib/pangea/common/utils/overlay.dart @@ -246,7 +246,7 @@ class TransparentBackdropState extends State curve: FluffyThemes.animationCurve, ), ); - _blurTween = Tween(begin: 0.0, end: 2.5).animate( + _blurTween = Tween(begin: 0.0, end: 3.0).animate( CurvedAnimation( parent: _controller, curve: FluffyThemes.animationCurve, diff --git a/lib/pangea/events/constants/pangea_event_types.dart b/lib/pangea/events/constants/pangea_event_types.dart index fe2a47e18..a385fe97d 100644 --- a/lib/pangea/events/constants/pangea_event_types.dart +++ b/lib/pangea/events/constants/pangea_event_types.dart @@ -26,6 +26,7 @@ class PangeaEventTypes { static const capacity = "pangea.capacity"; static const activityPlan = "pangea.activity_plan"; + static const activityPlanEnd = "pangea.activity.end"; static const userAge = "pangea.user_age"; diff --git a/lib/pangea/events/extensions/pangea_event_extension.dart b/lib/pangea/events/extensions/pangea_event_extension.dart index d7cf70fe9..4a70baec1 100644 --- a/lib/pangea/events/extensions/pangea_event_extension.dart +++ b/lib/pangea/events/extensions/pangea_event_extension.dart @@ -59,24 +59,22 @@ extension PangeaEvent on Event { content.tryGetMap(ModelKey.transcription); final audioContent = content.tryGetMap('org.matrix.msc1767.audio'); - if (transcription == null || audioContent == null) { - ErrorHandler.logError( - e: "Called getPangeaAudioFile on an audio message without transcription or audio content", - data: {}, - ); - return null; - } final matrixFile = await downloadAndDecryptAttachment(); - final duration = audioContent.tryGet('duration'); - final waveform = audioContent.tryGetList('waveform'); + + final duration = audioContent?.tryGet('duration') ?? + content.tryGetMap('info')?.tryGet('duration'); + + final waveform = audioContent?.tryGetList('waveform') ?? + content + .tryGetMap('org.matrix.msc1767.audio') + ?.tryGetList('waveform'); // old audio messages will not have tokens - final tokensContent = transcription.tryGetList(ModelKey.tokens); - if (tokensContent == null) return null; + final tokensContent = transcription?.tryGetList(ModelKey.tokens); final tokens = tokensContent - .map((e) => TTSToken.fromJson(e as Map)) + ?.map((e) => TTSToken.fromJson(e as Map)) .toList(); return PangeaAudioFile( diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index d343c5855..8486a9991 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -15,10 +15,13 @@ import 'package:fluffychat/widgets/matrix.dart'; class PhoneticTranscriptionWidget extends StatefulWidget { final String text; final LanguageModel textLanguage; + final TextStyle? style; final double? iconSize; final Color? iconColor; + final bool enabled; + const PhoneticTranscriptionWidget({ super.key, required this.text, @@ -26,6 +29,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget { this.style, this.iconSize, this.iconColor, + this.enabled = true, }); @override @@ -114,69 +118,74 @@ class _PhoneticTranscriptionWidgetState @override Widget build(BuildContext context) { - 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, + return IgnorePointer( + ignoring: !widget.enabled, + child: 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/", + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(width: 8), + if (_transcription != null && + _error == null && + widget.enabled) + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, size: widget.iconSize ?? 24, - color: Theme.of(context).colorScheme.error, + color: widget.iconColor ?? + Theme.of(context).iconTheme.color, ), - 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/", - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, ), - ), - const SizedBox(width: 8), - if (_transcription != null && _error == null) - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - size: widget.iconSize ?? 24, - color: - widget.iconColor ?? Theme.of(context).iconTheme.color, - ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/lib/pangea/toolbar/widgets/message_audio_card.dart b/lib/pangea/toolbar/widgets/message_audio_card.dart index b4a4b26eb..2cc529422 100644 --- a/lib/pangea/toolbar/widgets/message_audio_card.dart +++ b/lib/pangea/toolbar/widgets/message_audio_card.dart @@ -104,7 +104,7 @@ class MessageAudioCardState extends State { class PangeaAudioFile extends MatrixAudioFile { List? waveform; - List tokens; + List? tokens; PangeaAudioFile({ required super.bytes, diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index c58e4fae2..f32f4d3ba 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -550,7 +550,7 @@ class MessageSelectionPositionerState extends State return 0.8; case ReadingAssistanceMode.selectMode: case null: - return 0.4; + return 0.6; } } diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index fe783c503..e83e856d3 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_name.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/learning_settings/models/language_model.dart'; @@ -205,6 +206,8 @@ class OverlayMessage extends StatelessWidget { textColor, ), iconColor: textColor, + enabled: + event.senderId != BotName.byEnvironment, ), ], ), diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart index 7be70a778..43759a1a1 100644 --- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -93,34 +93,13 @@ class SelectModeButtonsState extends State { Completer? _transcriptionCompleter; - AudioPlayer? get _audioPlayer => Matrix.of(context).audioPlayer!; + MatrixState? matrix; @override void initState() { super.initState(); - 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) { - if (_audioBytes != null) { - widget.overlayController.highlightCurrentText( - state.inMilliseconds, - _audioBytes!.tokens, - ); - } - }); - + matrix = Matrix.of(context); if (messageEvent?.isAudioMessage == true) { _fetchTranscription(); } @@ -128,9 +107,9 @@ class SelectModeButtonsState extends State { @override void dispose() { - _audioPlayer?.dispose(); - Matrix.of(context).audioPlayer = null; - Matrix.of(context).voiceMessageEventId.value = null; + matrix?.audioPlayer?.dispose(); + matrix?.audioPlayer = null; + matrix?.voiceMessageEventId.value = null; _onPlayerStateChanged?.cancel(); _onAudioPositionChanged?.cancel(); @@ -162,8 +141,8 @@ class SelectModeButtonsState extends State { if (mode == null) { setState(() { - _audioPlayer?.stop(); - _audioPlayer?.seek(null); + matrix?.audioPlayer?.stop(); + matrix?.audioPlayer?.seek(null); _selectedMode = null; }); return; @@ -178,8 +157,8 @@ class SelectModeButtonsState extends State { _playAudio(); return; } else { - _audioPlayer?.stop(); - _audioPlayer?.seek(null); + matrix?.audioPlayer?.stop(); + matrix?.audioPlayer?.seek(null); } if (_selectedMode == SelectMode.practice) { @@ -243,13 +222,52 @@ class SelectModeButtonsState extends State { } Future _playAudio() async { - try { - if (_audioPlayer != null && _audioPlayer!.playerState.playing) { - await _audioPlayer?.pause(); + final playerID = + "${widget.overlayController.pangeaMessageEvent?.eventId}_button"; + + if (matrix?.audioPlayer != null && + matrix?.voiceMessageEventId.value == playerID) { + // If the audio player is already initialized and playing the same message, pause it + if (matrix!.audioPlayer!.playerState.playing) { + await matrix?.audioPlayer?.pause(); return; - } else if (_audioPlayer?.position != Duration.zero) { + } + // If the audio player is paused, resume it + await matrix?.audioPlayer?.play(); + return; + } + + matrix?.audioPlayer?.dispose(); + matrix?.audioPlayer = AudioPlayer(); + matrix?.voiceMessageEventId.value = + widget.overlayController.pangeaMessageEvent?.eventId; + + _onPlayerStateChanged = + matrix?.audioPlayer?.playerStateStream.listen((state) { + if (state.processingState == ProcessingState.completed) { + _updateMode(null); + } + setState(() {}); + }); + + _onAudioPositionChanged ??= + matrix?.audioPlayer?.positionStream.listen((state) { + if (_audioBytes?.tokens != null) { + widget.overlayController.highlightCurrentText( + state.inMilliseconds, + _audioBytes!.tokens!, + ); + } + }); + + try { + if (matrix?.audioPlayer != null && + matrix!.audioPlayer!.playerState.playing) { + await matrix?.audioPlayer?.pause(); + return; + } else if (matrix?.audioPlayer?.position != Duration.zero) { TtsController.stop(); - await _audioPlayer?.play(); + await matrix?.audioPlayer?.play(); return; } @@ -260,9 +278,9 @@ class SelectModeButtonsState extends State { if (_audioBytes == null) return; if (_audioFile != null) { - await _audioPlayer?.setFilePath(_audioFile!.path); + await matrix?.audioPlayer?.setFilePath(_audioFile!.path); } else { - await _audioPlayer?.setAudioSource( + await matrix?.audioPlayer?.setAudioSource( BytesAudioSource( _audioBytes!.bytes, _audioBytes!.mimeType, @@ -271,7 +289,7 @@ class SelectModeButtonsState extends State { } TtsController.stop(); - _audioPlayer?.play(); + matrix?.audioPlayer?.play(); } catch (e, s) { setState(() => _audioError = e.toString()); ErrorHandler.logError( @@ -441,7 +459,7 @@ class SelectModeButtonsState extends State { if (mode == SelectMode.audio) { return Icon( - _audioPlayer?.playerState.playing == true + matrix?.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 f074a43f1..d762a4dcc 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -5,33 +5,9 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import '../../config/app_config.dart'; extension VisibleInGuiExtension on List { - List filterByVisibleInGui({String? exceptionEventId}) { - final visibleEvents = - where((e) => e.isVisibleInGui || e.eventId == exceptionEventId) - .toList(); - - // Hide creation state events: - if (visibleEvents.isNotEmpty && - visibleEvents.last.type == EventTypes.RoomCreate) { - var i = visibleEvents.length - 2; - while (i > 0) { - final event = visibleEvents[i]; - if (!event.isState) break; - if (event.type == EventTypes.Encryption) { - i--; - continue; - } - if (event.type == EventTypes.RoomMember && - event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) { - i--; - continue; - } - visibleEvents.removeAt(i); - i--; - } - } - return visibleEvents; - } + List filterByVisibleInGui({String? exceptionEventId}) => where( + (event) => event.isVisibleInGui || event.eventId == exceptionEventId, + ).toList(); } extension IsStateExtension on Event { @@ -89,6 +65,7 @@ extension IsStateExtension on Event { EventTypes.RoomTombstone, EventTypes.CallInvite, PangeaEventTypes.activityPlan, + PangeaEventTypes.activityPlanEnd, }; // Pangea# }