diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index d60208739..51dfaa1fe 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -50,6 +50,11 @@ jobs: cp public/.env public/assets/.env touch public/assets/envs.json echo "$ENV_OVERRIDES" >> public/assets/envs.json + mkdir -p public/.well-known + curl https://app.pangea.chat/.well-known/apple-app-site-association \ + -o public/.well-known/apple-app-site-association + curl https://app.pangea.chat/.well-known/assetlinks.json \ + -o public/.well-known/assetlinks.json - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a4058486b..211d34df2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -108,6 +108,19 @@ + + + + + + + + + + + ???? CFBundleURLTypes + + CFBundleURLSchemes + + pangea + + CFBundleURLName + com.talktolearn.chat + CFBundleTypeRole Editor @@ -113,5 +121,7 @@ io.flutter.embedded_views_preview + FlutterDeepLinkingEnabled + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 2b2a88dd1..91e1a0719 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,18 +1,16 @@ - - aps-environment - development - com.apple.developer.associated-domains - - applinks:example.com - - com.apple.security.application-groups - - - group.com.talktolearn.chat - - - - \ No newline at end of file + + aps-environment + development + com.apple.developer.associated-domains + + applinks:app.pangea.chat + + com.apple.security.application-groups + + group.com.talktolearn.chat + + + diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 734869ecc..2106db95a 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -44,7 +44,6 @@ abstract class AppConfig { toolbarButtonsHeight + (chatInputRowOverlayPadding * 2) + toolbarSpacing; - static const double audioTranscriptionMaxHeight = 150.0; static TextStyle messageTextStyle( Event? event, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0f6b2899e..3f07c4cce 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -4630,9 +4630,9 @@ "meaningSectionHeader": "Meaning:", "formSectionHeader": "Forms used in chats:", "noEmojiSelectedTooltip": "No emoji selected", - "writingExercisesTooltip": "Writing practice", - "listeningExercisesTooltip": "Listening practice", - "readingExercisesTooltip": "Reading practice", + "writingExercisesTooltip": "Writing", + "listeningExercisesTooltip": "Listening", + "readingExercisesTooltip": "Reading", "meaningNotFound": "Meaning could not be found.", "formsNotFound": "Forms could not be found.", "chooseBaseForm": "Choose the base form", @@ -5001,6 +5001,7 @@ "canBeFoundViaKnock": "\u2022 request to join and admin approval", "anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!", "createYourSpace": "Create your space", + "youHaveLeveledUp": "You have leveled up!", "sendActivities": "Send activities", "getStarted": "Get Started", "getStartedBotChatDesc": "Chatting with AI is a great place to start and Pangea reading, writing, listening and speaking tools make it easy!", @@ -5015,11 +5016,8 @@ "groupChat": "Group Chat", "directMessage": "Direct Message", "newDirectMessage": "New direct message", - "speakingExercisesTooltip": "Speaking practice", + "speakingExercisesTooltip": "Speaking", "noChatsFoundHereYet": "No chats found here yet", - "endNow": "End now", - "setDuration": "Set duration", - "activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!", "duration": "Duration", "transcriptionFailed": "Failed to transcribe audio", "aUserIsKnocking": "1 user is requesting to join your space", @@ -5034,4 +5032,4 @@ }, "failedToFetchTranscription": "Failed to fetch transcription", "deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone." -} \ No newline at end of file +} diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index e2cd8b515..bc820b81a 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -33,7 +33,7 @@ import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; 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.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'; @@ -1856,7 +1856,10 @@ class ChatController extends State } } - void pinEvent() { + // #Pangea + // void pinEvent() { + Future pinEvent() async { + // Pangea# final pinnedEventIds = room.pinnedEventIds; final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet(); final unpin = selectedEventIds.length == 1 && @@ -1866,10 +1869,16 @@ class ChatController extends State } else { pinnedEventIds.addAll(selectedEventIds); } - showFutureLoadingDialog( + // #Pangea + // showFutureLoadingDialog( + // context: context, + // future: () => room.setPinnedEvents(pinnedEventIds), + // ); + await showFutureLoadingDialog( context: context, future: () => room.setPinnedEvents(pinnedEventIds), ); + // Pangea# } Timer? _storeInputTimeoutTimer; diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 030f81422..948c0578b 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:fluffychat/config/app_config.dart'; @@ -11,9 +10,7 @@ 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'; @@ -43,30 +40,6 @@ 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/chat_view.dart b/lib/pages/chat/chat_view.dart index 2f90e7f59..1436cb00b 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -13,11 +13,9 @@ 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'; @@ -190,13 +188,6 @@ 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( @@ -235,9 +226,6 @@ class ChatView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ PinnedEvents(controller), - // #Pangea - PinnedActivityMessage(controller), - // Pangea# if (scrollUpBannerEventId != null) ChatAppBarListTile( leading: IconButton( diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 11bbfba52..0831047f4 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -77,6 +77,8 @@ class AudioPlayerState extends State { // #Pangea StreamSubscription? _onAudioPositionChanged; StreamSubscription? _onAudioStateChanged; + + double playbackSpeed = 1.0; // Pangea# @override @@ -175,6 +177,9 @@ class AudioPlayerState extends State { : matrix.audioPlayer; if (currentPlayer != null) { + // #Pangea + currentPlayer.setSpeed(playbackSpeed); + // Pangea# if (currentPlayer.isAtEndPosition) { currentPlayer.seek(Duration.zero); } else if (currentPlayer.playing) { @@ -204,28 +209,37 @@ class AudioPlayerState extends State { // #Pangea // if (!kIsWeb) { - if (!kIsWeb && matrixFile != null) { - // Pangea# - final tempDir = await getTemporaryDirectory(); - final fileName = Uri.encodeComponent( - // #Pangea - // widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, - widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + if (!kIsWeb) { + if (matrixFile != null) { // Pangea# - ); - file = File('${tempDir.path}/${fileName}_${matrixFile.name}'); + final tempDir = await getTemporaryDirectory(); + final fileName = Uri.encodeComponent( + // #Pangea + // widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + widget.event!.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + // Pangea# + ); + file = File('${tempDir.path}/${fileName}_${matrixFile.name}'); - await file.writeAsBytes(matrixFile.bytes); + await file.writeAsBytes(matrixFile.bytes); - if (Platform.isIOS && - matrixFile.mimeType.toLowerCase() == 'audio/ogg') { - Logs().v('Convert ogg audio file for iOS...'); - final convertedFile = File('${file.path}.caf'); - if (await convertedFile.exists() == false) { - OpusCaf().convertOpusToCaf(file.path, convertedFile.path); + if (Platform.isIOS && + matrixFile.mimeType.toLowerCase() == 'audio/ogg') { + Logs().v('Convert ogg audio file for iOS...'); + final convertedFile = File('${file.path}.caf'); + if (await convertedFile.exists() == false) { + OpusCaf().convertOpusToCaf(file.path, convertedFile.path); + } + file = convertedFile; } - file = convertedFile; + // #Pangea + } else if (widget.matrixFile != null) { + final tempDir = await getTemporaryDirectory(); + + file = File('${tempDir.path}/${widget.matrixFile!.name}'); + await file.writeAsBytes(widget.matrixFile!.bytes); } + // Pangea# } setState(() { @@ -250,6 +264,7 @@ class AudioPlayerState extends State { final audioPlayer = matrix.audioPlayer = AudioPlayer(); // #Pangea + audioPlayer.setSpeed(playbackSpeed); _onAudioPositionChanged?.cancel(); _onAudioPositionChanged = matrix.audioPlayer!.positionStream.listen((state) { @@ -306,7 +321,22 @@ class AudioPlayerState extends State { void _toggleSpeed() async { final audioPlayer = matrix.audioPlayer; + // #Pangea + // if (audioPlayer == null) return; + switch (playbackSpeed) { + case 1.0: + setState(() => playbackSpeed = 0.75); + case 0.75: + setState(() => playbackSpeed = 0.5); + case 0.5: + setState(() => playbackSpeed = 1.25); + case 1.25: + setState(() => playbackSpeed = 1.5); + default: + setState(() => playbackSpeed = 1.0); + } if (audioPlayer == null) return; + // Pangea# switch (audioPlayer.speed) { // #Pangea // case 1.0: @@ -599,7 +629,7 @@ class AudioPlayerState extends State { height: 20, child: Center( child: Text( - '${audioPlayer?.speed.toString() ?? 1}x', + '${audioPlayer?.speed.toString() ?? playbackSpeed}x', style: TextStyle( color: widget.color, fontSize: 9, diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 9ddaa4fce..729ff7890 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -179,7 +179,9 @@ class HtmlMessage extends StatelessWidget { // #Pangea List? get tokens => - pangeaMessageEvent?.messageDisplayRepresentation?.tokens; + pangeaMessageEvent?.messageDisplayRepresentation?.tokens + ?.where((t) => t.pos != "PUNCT") + .toList(); PangeaToken? getToken( String text, @@ -385,6 +387,8 @@ class HtmlMessage extends StatelessWidget { overlayController: overlayController, isTransitionAnimation: isTransitionAnimation, ); + + final fontSize = renderer.fontSize(context) ?? this.fontSize; // Pangea# switch (node.localName) { @@ -500,10 +504,7 @@ class HtmlMessage extends StatelessWidget { avatar: user.avatarUrl, uri: href, outerContext: context, - // #Pangea - // fontSize: fontSize, - fontSize: renderer.fontSize(context) ?? fontSize, - // Pangea# + fontSize: fontSize, color: linkStyle.color, // #Pangea userId: user.id, @@ -524,10 +525,7 @@ class HtmlMessage extends StatelessWidget { avatar: room?.avatar, uri: href, outerContext: context, - // #Pangea - // fontSize: fontSize, - fontSize: renderer.fontSize(context) ?? fontSize, - // Pangea# + fontSize: fontSize, color: linkStyle.color, ), ); diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 30d787343..ada6cceb4 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -9,9 +9,7 @@ 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'; @@ -123,22 +121,6 @@ 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(); - } - - if (event.type == PangeaEventTypes.activityPlanEnd) { - return const ActivityFinishedEvent(); - } - // Pangea# return StateMessage(event); } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index eccc19960..519ff9218 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -587,7 +587,6 @@ class ChatListController extends State if (space != null) { chatListHandleSpaceTap( context, - this, space, ); } @@ -669,6 +668,10 @@ class ChatListController extends State _activeSpaceId = widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId; + + WidgetsBinding.instance.addPostFrameCallback((_) { + _joinInvitedSpaces(); + }); // Pangea# super.initState(); @@ -685,6 +688,16 @@ class ChatListController extends State : setActiveSpace(widget.activeSpaceId!); } } + + Future _joinInvitedSpaces() async { + final invitedSpaces = Matrix.of(context).client.rooms.where( + (r) => r.isSpace && r.membership == Membership.invite, + ); + + for (final space in invitedSpaces) { + await showInviteDialog(space, context); + } + } // Pangea# @override diff --git a/lib/pages/invitation_selection/invitation_selection.dart b/lib/pages/invitation_selection/invitation_selection.dart index 06509fae9..4929e27a9 100644 --- a/lib/pages/invitation_selection/invitation_selection.dart +++ b/lib/pages/invitation_selection/invitation_selection.dart @@ -35,35 +35,6 @@ class InvitationSelectionController extends State { String? get roomId => widget.roomId; // #Pangea - final viewportKey = GlobalKey(); - - final participantListItemHeight = 72.0; - final goToChatButtonHeight = 50.0; - final shareButtonsHeight = 150.0; - final padding = 16.0 * 2; - final fixedParticipantHeight = 72.0; - - double? viewportHeight; - double get availableHeight => - (viewportHeight ?? 0) - - goToChatButtonHeight - - shareButtonsHeight - - padding; - - bool showShareButtons(int numParticipants) => - (fixedParticipantHeight * numParticipants) < availableHeight; - - @override - initState() { - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = viewportKey.currentContext; - if (context == null) return; - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - setState(() => viewportHeight = size.height); - }); - super.initState(); - } List? get participants { final room = Matrix.of(context).client.getRoomById(roomId!); diff --git a/lib/pages/invitation_selection/invitation_selection_view.dart b/lib/pages/invitation_selection/invitation_selection_view.dart index 59c6f6ad0..2df3b6a56 100644 --- a/lib/pages/invitation_selection/invitation_selection_view.dart +++ b/lib/pages/invitation_selection/invitation_selection_view.dart @@ -8,12 +8,10 @@ import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart'; import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart'; -import 'package:fluffychat/pangea/chat_settings/widgets/space_invite_buttons.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; @@ -107,165 +105,153 @@ class InvitationSelectionView extends StatelessWidget { // #Pangea withScrolling: false, // Pangea# - child: Stack( - alignment: Alignment.bottomCenter, + child: Column( children: [ Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: 450, - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}", - errorWidget: (context, url, error) => const SizedBox(), - placeholder: (context, url) => const Center( - child: CircularProgressIndicator.adaptive(), + // #Pangea + // padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.only( + bottom: 16.0, + left: 16.0, + right: 16.0, + ), + // Pangea# + child: TextField( + textInputAction: TextInputAction.search, + decoration: InputDecoration( + filled: true, + fillColor: theme.colorScheme.secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), ), + hintStyle: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + // #Pangea + hintText: L10n.of(context).inviteStudentByUserName, + // hintText: L10n.of(context).inviteContactToGroup(groupName), + // Pangea# + prefixIcon: controller.loading + ? const Padding( + padding: EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 12, + ), + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), ), + onChanged: controller.searchUserWithCoolDown, ), ), - Column( - children: [ - Padding( - // #Pangea - // padding: const EdgeInsets.all(16.0), - padding: const EdgeInsets.only( - bottom: 16.0, - left: 16.0, - right: 16.0, - ), - // Pangea# - child: TextField( - textInputAction: TextInputAction.search, - decoration: InputDecoration( - filled: true, - fillColor: theme.colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - hintStyle: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - // #Pangea - hintText: L10n.of(context).inviteStudentByUserName, - // hintText: L10n.of(context).inviteContactToGroup(groupName), - // Pangea# - prefixIcon: controller.loading - ? const Padding( - padding: EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 12, - ), - child: SizedBox.square( - dimension: 24, + // #Pangea + // StreamBuilder( + Expanded( + child: StreamBuilder( + // stream: room.client.onRoomState.stream + // .where((update) => update.roomId == room.id), + stream: room.client.onRoomState.stream + .where((update) => update.roomId == room.id) + .rateLimit(const Duration(seconds: 1)), + // Pangea# + builder: (context, snapshot) { + final participants = + room.getParticipants().map((user) => user.id).toSet(); + return controller.foundProfiles.isNotEmpty + ? ListView.builder( + // #Pangea + // physics: const NeverScrollableScrollPhysics(), + // shrinkWrap: true, + // Pangea# + itemCount: controller.foundProfiles.length, + itemBuilder: (BuildContext context, int i) => + _InviteContactListTile( + profile: controller.foundProfiles[i], + isMember: participants.contains( + controller.foundProfiles[i].userId, + ), + onTap: () => controller.inviteAction( + context, + controller.foundProfiles[i].userId, + controller.foundProfiles[i].displayName ?? + controller + .foundProfiles[i].userId.localpart ?? + L10n.of(context).user, + ), + ), + ) + : FutureBuilder>( + future: controller.getContacts(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) { + return const Center( child: CircularProgressIndicator.adaptive( strokeWidth: 2, ), - ), - ) - : const Icon(Icons.search_outlined), - ), - onChanged: controller.searchUserWithCoolDown, - ), - ), - // #Pangea - // StreamBuilder( - Expanded( - key: controller.viewportKey, - child: StreamBuilder( - // stream: room.client.onRoomState.stream - // .where((update) => update.roomId == room.id), - stream: room.client.onRoomState.stream - .where((update) => update.roomId == room.id) - .rateLimit(const Duration(seconds: 1)), - // Pangea# - builder: (context, snapshot) { - final participants = - room.getParticipants().map((user) => user.id).toSet(); - return controller.foundProfiles.isNotEmpty - ? ListView.builder( + ); + } + final contacts = snapshot.data!; + return ListView.builder( // #Pangea // physics: const NeverScrollableScrollPhysics(), // shrinkWrap: true, - // Pangea# - itemCount: controller.foundProfiles.length, - itemBuilder: (BuildContext context, int i) => - _InviteContactListTile( - profile: controller.foundProfiles[i], - isMember: participants.contains( - controller.foundProfiles[i].userId, - ), - onTap: () => controller.inviteAction( - context, - controller.foundProfiles[i].userId, - controller.foundProfiles[i].displayName ?? - controller - .foundProfiles[i].userId.localpart ?? - L10n.of(context).user, - ), - ), - ) - : FutureBuilder>( - future: controller.getContacts(context), - builder: (BuildContext context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, + // itemCount: contacts.length, + // itemBuilder: (BuildContext context, int i) => + // _InviteContactListTile( + itemCount: contacts.length + 1, + itemBuilder: (BuildContext context, int i) { + if (i == contacts.length) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: 450, + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}", + errorWidget: (context, url, error) => + const SizedBox(), + placeholder: (context, url) => + const Center( + child: CircularProgressIndicator + .adaptive(), + ), + ), ), ); } - final contacts = snapshot.data!; - return ListView.builder( - // #Pangea - // physics: const NeverScrollableScrollPhysics(), - // shrinkWrap: true, - // itemCount: contacts.length, - // itemBuilder: (BuildContext context, int i) => - // _InviteContactListTile( - itemCount: contacts.length + 1, - itemBuilder: (BuildContext context, int i) { - if (i == contacts.length) { - final showButtons = controller - .showShareButtons(contacts.length); - return AnimatedOpacity( - duration: - FluffyThemes.animationDuration, - opacity: showButtons ? 1.0 : 0.0, - child: SpaceInviteButtons(room: room), - ); - } - - return _InviteContactListTile( - // Pangea# - user: contacts[i], - profile: Profile( - avatarUrl: contacts[i].avatarUrl, - displayName: contacts[i].displayName ?? - contacts[i].id.localpart ?? - L10n.of(context).user, - userId: contacts[i].id, - ), - isMember: - participants.contains(contacts[i].id), - onTap: () => controller.inviteAction( - context, - contacts[i].id, - contacts[i].displayName ?? - contacts[i].id.localpart ?? - L10n.of(context).user, - ), - ); - }, + return _InviteContactListTile( + // Pangea# + user: contacts[i], + profile: Profile( + avatarUrl: contacts[i].avatarUrl, + displayName: contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context).user, + userId: contacts[i].id, + ), + isMember: + participants.contains(contacts[i].id), + onTap: () => controller.inviteAction( + context, + contacts[i].id, + contacts[i].displayName ?? + contacts[i].id.localpart ?? + L10n.of(context).user, + ), ); }, ); - }, - ), - ), - ], + }, + ); + }, + ), ), Padding( padding: const EdgeInsets.all(16.0), @@ -355,6 +341,8 @@ class _InviteContactListTile extends StatelessWidget { style: const TextStyle( fontSize: 12.0, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), LevelDisplayName(userId: profile.userId), ], diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 81f488f2e..c273109c9 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -87,7 +87,14 @@ class SettingsView extends StatelessWidget { return Row( children: [ Padding( - padding: const EdgeInsets.all(32.0), + // #Pangea + // padding: const EdgeInsets.all(32.0), + padding: const EdgeInsets.only( + top: 32.0, + bottom: 32.0, + left: 12.0, + ), + // Pangea# child: Stack( children: [ Avatar( diff --git a/lib/pangea/activities/activity_aware_builder.dart b/lib/pangea/activities/activity_aware_builder.dart deleted file mode 100644 index 68eea54af..000000000 --- a/lib/pangea/activities/activity_aware_builder.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -class ActivityAwareBuilder extends StatefulWidget { - final DateTime? deadline; - final Widget Function(bool) builder; - - const ActivityAwareBuilder({ - super.key, - required this.builder, - this.deadline, - }); - - @override - State createState() => ActivityAwareBuilderState(); -} - -class ActivityAwareBuilderState extends State { - Timer? _timer; - - @override - void initState() { - super.initState(); - _setTimer(); - } - - @override - void didUpdateWidget(covariant ActivityAwareBuilder oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.deadline != widget.deadline) { - _setTimer(); - } - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - void _setTimer() { - final now = DateTime.now(); - final delay = widget.deadline?.difference(now); - - if (delay != null && delay > Duration.zero) { - _timer?.cancel(); - _timer = Timer(delay, () { - _timer?.cancel(); - _timer = null; - if (mounted) setState(() {}); - }); - } - } - - @override - Widget build(BuildContext context) { - return widget.builder( - widget.deadline != null && widget.deadline!.isAfter(DateTime.now()), - ); - } -} diff --git a/lib/pangea/activities/activity_constants.dart b/lib/pangea/activities/activity_constants.dart deleted file mode 100644 index 41858d53a..000000000 --- a/lib/pangea/activities/activity_constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class ActivityConstants { - static const String activityFinishedAsset = "EndActivityMsg.png"; -} diff --git a/lib/pangea/activities/activity_duration_popup.dart b/lib/pangea/activities/activity_duration_popup.dart deleted file mode 100644 index c6cbb56ab..000000000 --- a/lib/pangea/activities/activity_duration_popup.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; - -class ActivityDurationPopup extends StatefulWidget { - final Duration initialValue; - const ActivityDurationPopup({ - super.key, - required this.initialValue, - }); - - @override - State createState() => ActivityDurationPopupState(); -} - -class ActivityDurationPopupState extends State { - final TextEditingController _daysController = TextEditingController(); - final TextEditingController _hoursController = TextEditingController(); - final TextEditingController _minutesController = TextEditingController(); - - String? error; - - final List _durations = [ - const Duration(minutes: 15), - const Duration(minutes: 30), - const Duration(minutes: 45), - const Duration(minutes: 60), - const Duration(hours: 1, minutes: 30), - const Duration(hours: 2), - const Duration(hours: 24), - const Duration(days: 2), - const Duration(days: 7), - ]; - - @override - void initState() { - super.initState(); - _daysController.text = widget.initialValue.inDays.toString(); - _hoursController.text = - widget.initialValue.inHours.remainder(24).toString(); - _minutesController.text = - widget.initialValue.inMinutes.remainder(60).toString(); - - _daysController.addListener(() => setState(() => error = null)); - _hoursController.addListener(() => setState(() => error = null)); - _minutesController.addListener(() => setState(() => error = null)); - } - - @override - void dispose() { - _daysController.dispose(); - _hoursController.dispose(); - _minutesController.dispose(); - super.dispose(); - } - - void _setDuration({int? days, int? hours, int? minutes}) { - setState(() { - if (days != null) _daysController.text = days.toString(); - if (hours != null) _hoursController.text = hours.toString(); - if (minutes != null) _minutesController.text = minutes.toString(); - }); - } - - String _formatDuration(Duration duration) { - final days = duration.inDays; - final hours = duration.inHours.remainder(24); - final minutes = duration.inMinutes.remainder(60); - - final List parts = []; - if (days > 0) parts.add("${days}d"); - if (hours > 0) parts.add("${hours}h"); - if (minutes > 0) parts.add("${minutes}m"); - if (parts.isEmpty) return "0m"; - - return parts.join(" "); - } - - Duration get _duration { - final days = int.tryParse(_daysController.text) ?? 0; - final hours = int.tryParse(_hoursController.text) ?? 0; - final minutes = int.tryParse(_minutesController.text) ?? 0; - return Duration(days: days, hours: hours, minutes: minutes); - } - - void _submit() { - final days = int.tryParse(_daysController.text); - final hours = int.tryParse(_hoursController.text); - final minutes = int.tryParse(_minutesController.text); - - if (days == null || hours == null || minutes == null) { - setState(() { - error = "Invalid duration"; - }); - return; - } - - Navigator.of(context).pop(_duration); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Dialog( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 350.0, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - spacing: 12.0, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - L10n.of(context).setDuration, - style: const TextStyle(fontSize: 20.0, height: 1.2), - ), - Column( - children: [ - Container( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide( - width: 2, - color: theme.colorScheme.primary.withAlpha(100), - ), - borderRadius: BorderRadius.circular(20), - ), - ), - padding: const EdgeInsets.only( - top: 12.0, - bottom: 12.0, - right: 24.0, - left: 8.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SelectionArea( - child: Row( - spacing: 12.0, - children: [ - _DatePickerInput( - type: "d", - controller: _daysController, - ), - _DatePickerInput( - type: "h", - controller: _hoursController, - ), - _DatePickerInput( - type: "m", - controller: _minutesController, - ), - ], - ), - ), - const Icon( - Icons.alarm, - size: 24, - ), - ], - ), - ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: error != null - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - error!, - style: TextStyle( - color: theme.colorScheme.error, - fontSize: 14.0, - ), - ), - ) - : const SizedBox.shrink(), - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 24.0, - ), - child: Wrap( - spacing: 10.0, - runSpacing: 10.0, - children: _durations - .map( - (d) => InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () { - _setDuration( - days: d.inDays, - hours: d.inHours.remainder(24), - minutes: d.inMinutes.remainder(60), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 0.0, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer - .withAlpha(_duration == d ? 200 : 100), - borderRadius: BorderRadius.circular(12), - ), - child: Text(_formatDuration(d)), - ), - ), - ) - .toList(), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextButton( - onPressed: _submit, - child: Text(L10n.of(context).confirm), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } -} - -class _DatePickerInput extends StatelessWidget { - final String type; - final TextEditingController controller; - - const _DatePickerInput({ - required this.type, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox( - width: 35.0, - child: TextField( - controller: controller, - textAlign: TextAlign.end, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - contentPadding: const EdgeInsets.all(0.0), - hintText: "0", - hintStyle: TextStyle( - fontSize: 20.0, - color: theme.colorScheme.onSurfaceVariant.withAlpha(100), - ), - ), - style: const TextStyle( - fontSize: 20.0, - ), - keyboardType: TextInputType.number, - ), - ), - Text(type, style: const TextStyle(fontSize: 20.0)), - ], - ); - } -} diff --git a/lib/pangea/activities/activity_state_event.dart b/lib/pangea/activities/activity_state_event.dart deleted file mode 100644 index b59b8c233..000000000 --- a/lib/pangea/activities/activity_state_event.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/activities/activity_constants.dart'; -import 'package:fluffychat/pangea/activities/activity_duration_popup.dart'; -import 'package:fluffychat/pangea/activities/countdown.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; -import 'package:fluffychat/widgets/mxc_image.dart'; - -class ActivityStateEvent extends StatefulWidget { - final Event event; - - const ActivityStateEvent({required this.event, super.key}); - - @override - State createState() => ActivityStateEventState(); -} - -class ActivityStateEventState extends State { - Timer? _timer; - - @override - void initState() { - super.initState(); - final now = DateTime.now(); - final delay = activityPlan?.endAt != null - ? activityPlan!.endAt!.difference(now) - : null; - - if (delay != null && delay > Duration.zero) { - _timer = Timer(delay, () { - setState(() {}); - }); - } - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - ActivityPlanModel? get activityPlan { - try { - return ActivityPlanModel.fromJson(widget.event.content); - } catch (e) { - return null; - } - } - - @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: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(18), - ), - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: Text( - activityPlan!.markdown, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, - ), - ), - ), - ), - AnimatedSize( - duration: FluffyThemes.animationDuration, - 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: [ - 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, - ), - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} - -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/activities/countdown.dart b/lib/pangea/activities/countdown.dart deleted file mode 100644 index 17516cb23..000000000 --- a/lib/pangea/activities/countdown.dart +++ /dev/null @@ -1,98 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; - -class CountDown extends StatefulWidget { - final DateTime? deadline; - - final double? iconSize; - final double? textSize; - - const CountDown({ - super.key, - required this.deadline, - this.iconSize, - this.textSize, - }); - - @override - State createState() => CountDownState(); -} - -class CountDownState extends State { - Timer? _timer; - - @override - void initState() { - super.initState(); - _timer = Timer.periodic(const Duration(seconds: 1), (_) { - setState(() {}); - }); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - String? _formatDuration(Duration duration) { - final days = duration.inDays; - final hours = duration.inHours.remainder(24); - final minutes = duration.inMinutes.remainder(60); - final seconds = duration.inSeconds.remainder(60); - - final List parts = []; - if (days > 0) parts.add("${days}d"); - if (hours > 0) parts.add("${hours}h"); - if (minutes > 0) parts.add("${minutes}m"); - if (seconds > 0 && minutes <= 0) parts.add("${seconds}s"); - if (parts.isEmpty) return null; - - return parts.join(" "); - } - - Duration? get _remainingTime { - if (widget.deadline == null) { - return null; - } - - final now = DateTime.now(); - return widget.deadline!.difference(now); - } - - @override - Widget build(BuildContext context) { - final remainingTime = _remainingTime; - final durationString = _formatDuration(remainingTime ?? Duration.zero); - - return ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 250.0, - ), - child: Row( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.timer_outlined, - size: widget.iconSize ?? 28.0, - ), - Flexible( - child: Text( - remainingTime != null && - remainingTime >= Duration.zero && - durationString != null - ? durationString - : L10n.of(context).duration, - style: TextStyle(fontSize: widget.textSize ?? 20), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/activities/pinned_activity_message.dart b/lib/pangea/activities/pinned_activity_message.dart deleted file mode 100644 index 8be5a492f..000000000 --- a/lib/pangea/activities/pinned_activity_message.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; - -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart'; -import 'package:fluffychat/pangea/activities/activity_aware_builder.dart'; -import 'package:fluffychat/pangea/activities/activity_duration_popup.dart'; -import 'package:fluffychat/pangea/activities/countdown.dart'; -import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; -import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/widgets/future_loading_dialog.dart'; - -class PinnedActivityMessage extends StatelessWidget { - final ChatController controller; - - const PinnedActivityMessage(this.controller, {super.key}); - - Future _scrollToEvent() async { - final eventId = _activityPlanEvent?.eventId; - if (eventId != null) controller.scrollToEventId(eventId); - } - - Event? get _activityPlanEvent => controller.timeline?.events.firstWhereOrNull( - (event) => event.type == PangeaEventTypes.activityPlan, - ); - - ActivityPlanModel? get _activityPlan => controller.room.activityPlan; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return ActivityAwareBuilder( - deadline: _activityPlan?.endAt, - builder: (isActive) { - if (!isActive || _activityPlan == null) { - return const SizedBox.shrink(); - } - - return ChatAppBarListTile( - title: _activityPlan!.title, - leading: IconButton( - splashRadius: 18, - iconSize: 18, - color: theme.colorScheme.onSurfaceVariant, - icon: const Icon(Icons.push_pin), - onPressed: () {}, - ), - trailing: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () async { - final Duration? duration = await showDialog( - context: context, - builder: (context) { - return ActivityDurationPopup( - initialValue: - _activityPlan?.duration ?? const Duration(days: 1), - ); - }, - ); - - if (duration == null) return; - - showFutureLoadingDialog( - context: context, - future: () => controller.room.sendActivityPlan( - _activityPlan!.copyWith( - endAt: DateTime.now().add(duration), - duration: duration, - ), - ), - ); - }, - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(20), - ), - child: CountDown( - deadline: _activityPlan!.endAt, - iconSize: 16.0, - textSize: 14.0, - ), - ), - ), - ), - onTap: _scrollToEvent, - ); - }, - ); - } -} diff --git a/lib/pangea/activity_generator/activity_generator.dart b/lib/pangea/activity_generator/activity_generator.dart index 35c517b17..2ed3dcd9b 100644 --- a/lib/pangea/activity_generator/activity_generator.dart +++ b/lib/pangea/activity_generator/activity_generator.dart @@ -73,7 +73,7 @@ class ActivityGeneratorState extends State { ActivitySettingRequestSchema get req => ActivitySettingRequestSchema( langCode: - MatrixState.pangeaController.languageController.userL2?.langCode ?? + MatrixState.pangeaController.languageController.userL1?.langCode ?? LanguageKeys.defaultLanguage, ); diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index b4ce264a0..82d0ca74b 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -221,7 +221,7 @@ class ActivitySuggestionsAreaState extends State { ), AnimatedSize( duration: FluffyThemes.animationDuration, - child: _timeout + child: (_timeout || !_loading && cards.isEmpty) ? Padding( padding: const EdgeInsets.all(8.0), child: RichText( @@ -236,8 +236,10 @@ class ActivitySuggestionsAreaState extends State { ), const TextSpan(text: " "), TextSpan( - text: - L10n.of(context).activitySuggestionTimeoutMessage, + text: _timeout + ? L10n.of(context) + .activitySuggestionTimeoutMessage + : L10n.of(context).oopsSomethingWentWrong, ), ], ), diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 86a54e963..a7326e26f 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; @@ -35,7 +36,8 @@ class ConstructListModel { /// [D] is the "compression factor". It determines how quickly /// or slowly the level grows relative to XP - final double D = 1500; + + final double D = Environment.isStagingEnvironment ? 500 : 1500; List unlockedLemmas( ConstructTypeEnum type, { diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index dcdcafd65..5d861b089 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; +import 'package:fluffychat/widgets/matrix.dart'; /// A minimized version of AnalyticsController that get the logged in user's analytics class GetAnalyticsController extends BaseController { @@ -455,10 +456,13 @@ class GetAnalyticsController extends BaseController { // int diffXP = maxXP - minXP; // if (diffXP < 0) diffXP = 0; - Future getConstructSummaryFromStateEvent() async { + ConstructSummary? getConstructSummaryFromStateEvent() { try { final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!); - if (analyticsRoom == null) return null; + if (analyticsRoom == null) { + debugPrint("Analytics room is null"); + return null; + } final state = analyticsRoom.getState(PangeaEventTypes.constructSummary, ''); if (state == null) return null; @@ -492,23 +496,41 @@ class GetAnalyticsController extends BaseController { } // extract construct use message bodies for analytics - List? constructUseMessageContentBodies = []; + final Map> useEventIds = {}; for (final use in constructUseOfCurrentLevel) { - try { - final useMessage = await use.getEvent(_client); - final useMessageBody = useMessage?.content["body"]; - if (useMessageBody is String) { - constructUseMessageContentBodies.add(useMessageBody); - } else { - constructUseMessageContentBodies.add(null); - } - } catch (e) { - constructUseMessageContentBodies.add(null); - } + if (use.metadata.roomId == null) continue; + if (use.metadata.eventId == null) continue; + useEventIds[use.metadata.roomId!] ??= {}; + useEventIds[use.metadata.roomId!]!.add(use.metadata.eventId!); } - if (constructUseMessageContentBodies.length != - constructUseOfCurrentLevel.length) { - constructUseMessageContentBodies = null; + + final List constructUseMessageContentBodies = []; + for (final entry in useEventIds.entries) { + final String roomId = entry.key; + final room = _client.getRoomById(roomId); + if (room == null) continue; + final List messageBodies = []; + for (final eventId in entry.value) { + try { + final Event? event = await room.getEventById(eventId); + if (event?.content["body"] is! String) continue; + final String body = event?.content["body"] as String; + if (body.isEmpty) continue; + messageBodies.add(body); + } catch (e, s) { + debugPrint("Error getting event by ID: $e"); + ErrorHandler.logError( + e: e, + s: s, + data: { + 'roomId': roomId, + 'eventId': eventId, + }, + ); + continue; + } + } + constructUseMessageContentBodies.addAll(messageBodies); } final request = ConstructSummaryRequest( @@ -521,6 +543,10 @@ class GetAnalyticsController extends BaseController { final response = await ConstructRepo.generateConstructSummary(request); summary = response.summary; + summary.levelVocabConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + summary.levelGrammarConstructs = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; } catch (e) { debugPrint("Error generating level up analytics: $e"); ErrorHandler.logError(e: e, data: {'e': e}); diff --git a/lib/pangea/analytics_misc/level_up.dart b/lib/pangea/analytics_misc/level_up.dart deleted file mode 100644 index ab57cd03d..000000000 --- a/lib/pangea/analytics_misc/level_up.dart +++ /dev/null @@ -1,517 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:audioplayers/audioplayers.dart'; -import 'package:cached_network_image/cached_network_image.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; -import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/constructs/construct_repo.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class LevelUpConstants { - static const String starFileName = "star.png"; - static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; -} - -class LevelUpUtil { - static Future showLevelUpDialog( - int level, - int prevLevel, - BuildContext context, - ) async { - final player = AudioPlayer(); - - final snackbarRegex = RegExp(r'_snackbar$'); - - while (MatrixState.pAnyState.activeOverlays - .any((overlayId) => snackbarRegex.hasMatch(overlayId))) { - await Future.delayed(const Duration(milliseconds: 100)); - } - - player - .play( - UrlSource( - "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", - ), - ) - .then( - (_) => Future.delayed( - const Duration(seconds: 2), - () => player.dispose(), - ), - ); - - OverlayUtil.showOverlay( - overlayKey: "level_up_notification", - context: context, - child: LevelUpBanner( - level: level, - prevLevel: prevLevel, - ), - transformTargetId: '', - position: OverlayPositionEnum.top, - backDropToDismiss: false, - closePrevOverlay: false, - canPop: false, - ); - } -} - -class LevelUpBanner extends StatefulWidget { - final int level; - final int prevLevel; - - const LevelUpBanner({ - required this.level, - required this.prevLevel, - super.key, - }); - - @override - LevelUpBannerState createState() => LevelUpBannerState(); -} - -class LevelUpBannerState extends State - with TickerProviderStateMixin { - late AnimationController _slideController; - late Animation _slideAnimation; - - late AnimationController _sizeController; - late Animation _sizeAnimation; - - bool _showDetails = false; - bool _showedDetails = false; - - ConstructSummary? _constructSummary; - String? _error; - - @override - void initState() { - super.initState(); - _setConstructSummary(); - - _slideController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: _slideController, - curve: Curves.easeOut, - ), - ); - - _sizeController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _sizeAnimation = Tween( - begin: 0, - end: 1, - ).animate( - CurvedAnimation( - parent: _sizeController, - curve: Curves.easeOut, - ), - ); - - _slideController.forward(); - - Future.delayed(const Duration(seconds: 15), () async { - if (mounted && !_showedDetails) _close(); - }); - } - - @override - void dispose() { - _slideController.dispose(); - _sizeController.dispose(); - super.dispose(); - } - - Future _setConstructSummary() async { - try { - _constructSummary = await MatrixState.pangeaController.getAnalytics - .generateLevelUpAnalytics( - widget.level, - widget.prevLevel, - ); - } catch (e) { - _error = e.toString(); - } - } - - Future _close() async { - await _slideController.reverse(); - MatrixState.pAnyState.closeOverlay("level_up_notification"); - } - - int _skillsPoints(LearningSkillsEnum skill) { - switch (skill) { - case LearningSkillsEnum.writing: - return _constructSummary?.writingConstructScore ?? 0; - case LearningSkillsEnum.reading: - return _constructSummary?.readingConstructScore ?? 0; - case LearningSkillsEnum.speaking: - return _constructSummary?.speakingConstructScore ?? 0; - case LearningSkillsEnum.hearing: - return _constructSummary?.hearingConstructScore ?? 0; - default: - return 0; - } - } - - Future _toggleDetails() async { - if (!Environment.isStagingEnvironment) return; - - FocusScope.of(context).unfocus(); - - if (mounted) { - setState(() { - _showDetails = !_showDetails; - if (_showDetails && !_showedDetails) { - _showedDetails = true; - } - }); - - await (_showDetails - ? _sizeController.forward() - : _sizeController.reverse()); - - if (!_showDetails) { - await Future.delayed( - const Duration(milliseconds: 300), - () async { - if (!mounted) return; - _close(); - }, - ); - } - } - } - - @override - Widget build(BuildContext context) { - final style = FluffyThemes.isColumnMode(context) - ? Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ) - : Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ); - - return SafeArea( - child: Material( - color: Colors.transparent, - child: Stack( - children: [ - SlideTransition( - position: _slideAnimation, - child: Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 600.0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy < -10) _close(); - }, - onTap: _toggleDetails, - child: Container( - margin: const EdgeInsets.only( - top: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 24, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Flexible( - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: L10n.of(context) - .congratulationsOnReaching( - widget.level, - ), - style: style, - ), - TextSpan( - text: " ", - style: style, - ), - WidgetSpan( - child: CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", - height: 24, - width: 24, - ), - ), - ], - ), - ), - ), - Row( - children: [ - if (Environment.isStagingEnvironment) - AnimatedSize( - duration: FluffyThemes.animationDuration, - child: _error == null - ? FluffyThemes.isColumnMode(context) - ? ElevatedButton( - style: IconButton.styleFrom( - padding: const EdgeInsets - .symmetric( - vertical: 4.0, - horizontal: 16.0, - ), - ), - onPressed: _toggleDetails, - child: Text( - L10n.of(context).details, - ), - ) - : SizedBox( - width: 32.0, - height: 32.0, - child: Center( - child: IconButton( - icon: const Icon( - Icons.info_outline, - ), - style: - IconButton.styleFrom( - padding: - const EdgeInsets - .all( - 4.0, - ), - ), - onPressed: _toggleDetails, - constraints: - const BoxConstraints(), - ), - ), - ) - : Row( - children: [ - Tooltip( - message: L10n.of(context) - .oopsSomethingWentWrong, - child: Icon( - Icons.error, - color: Theme.of(context) - .colorScheme - .error, - ), - ), - ], - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: _close, - ), - ], - ), - ], - ), - ), - ), - SizeTransition( - sizeFactor: _sizeAnimation, - child: Container( - constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * 0.75, - ), - margin: const EdgeInsets.only( - top: 4.0, - ), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(16), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 24.0, - children: [ - Table( - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - 2: IntrinsicColumnWidth(), - }, - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - children: [ - ...LearningSkillsEnum.values - .where( - (v) => - v.isVisible && _skillsPoints(v) > -1, - ) - .map((skill) { - return TableRow( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Icon( - skill.icon, - size: 25, - color: Colors.white, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - skill.tooltip(context), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 18.0, - ), - child: Text( - "+ ${_skillsPoints(skill)} XP", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - textAlign: TextAlign.center, - ), - ), - ], - ); - }), - ], - ), - CachedNetworkImage( - imageUrl: - "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", - width: 400, - fit: BoxFit.cover, - ), - if (_constructSummary?.textSummary != null) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - _constructSummary!.textSummary, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 24, - ), - // Share button, currently no functionality - // ElevatedButton( - // onPressed: () { - // // Add share functionality - // }, - // style: ElevatedButton.styleFrom( - // backgroundColor: Colors.white, - // foregroundColor: Colors.black, - // padding: const EdgeInsets.symmetric( - // vertical: 12, - // horizontal: 24, - // ), - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(8), - // ), - // ), - // child: const Row( - // mainAxisSize: MainAxisSize - // .min, - // children: [ - // Text( - // "Share with Friends", - // style: TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.bold, - // ), - // ), - // SizedBox( - // width: 8, - // ), - // Icon( - // Icons.ios_share, - // size: 20, - // ), - // ), - // ), - // ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pangea/analytics_misc/level_up/level_up_banner.dart b/lib/pangea/analytics_misc/level_up/level_up_banner.dart new file mode 100644 index 000000000..a94916231 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_banner.dart @@ -0,0 +1,270 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:audioplayers/audioplayers.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_popup.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelUpConstants { + static const String starFileName = "star.png"; + static const String dinoLevelUPFileName = "DinoBot-Congratulate.png"; +} + +class LevelUpUtil { + static Future showLevelUpDialog( + int level, + int prevLevel, + BuildContext context, + ) async { + // Remove delay since GetAnalyticsController._onLevelUp is already async + final player = AudioPlayer(); + + // Wait for any existing snackbars to dismiss + await _waitForSnackbars(context); + + await player.play( + UrlSource( + "${AppConfig.assetsBaseURL}/${AnalyticsConstants.levelUpAudioFileName}", + ), + ); + + if (!context.mounted) return; + + await OverlayUtil.showOverlay( + overlayKey: "level_up_notification", + context: context, + child: LevelUpBanner( + level: level, + prevLevel: prevLevel, + backButtonOverride: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + MatrixState.pAnyState.closeOverlay("level_up_notification"); + }, + ), + ), + transformTargetId: '', + position: OverlayPositionEnum.top, + backDropToDismiss: false, + closePrevOverlay: false, + canPop: false, + ); + + await Future.delayed(const Duration(seconds: 2)); + player.dispose(); + } + + static Future _waitForSnackbars(BuildContext context) async { + final snackbarRegex = RegExp(r'_snackbar$'); + while (MatrixState.pAnyState.activeOverlays + .any((id) => snackbarRegex.hasMatch(id))) { + await Future.delayed(const Duration(milliseconds: 100)); + } + } +} + +class LevelUpBanner extends StatefulWidget { + final int level; + final int prevLevel; + final Widget? backButtonOverride; + + const LevelUpBanner({ + required this.level, + required this.prevLevel, + required this.backButtonOverride, + super.key, + }); + + @override + LevelUpBannerState createState() => LevelUpBannerState(); +} + +class LevelUpBannerState extends State + with TickerProviderStateMixin { + late AnimationController _slideController; + late Animation _slideAnimation; + + bool _showedDetails = false; + + @override + void initState() { + super.initState(); + + LevelUpManager.instance.preloadAnalytics( + context, + widget.level, + widget.prevLevel, + ); + + _slideController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideController, + curve: Curves.easeOut, + ), + ); + + _slideController.forward(); + + Future.delayed(const Duration(seconds: 10), () async { + if (mounted && !_showedDetails) { + _close(); + } + }); + } + + Future _close() async { + await _slideController.reverse(); + MatrixState.pAnyState.closeOverlay("level_up_notification"); + } + + @override + void dispose() { + _slideController.dispose(); + super.dispose(); + } + + Future _toggleDetails() async { + await _close(); + LevelUpManager.instance.markPopupSeen(); + _showedDetails = true; + + FocusScope.of(context).unfocus(); + + await showDialog( + context: context, + builder: (context) => const LevelUpPopup(), + ); + } + + @override + Widget build(BuildContext context) { + final isColumnMode = FluffyThemes.isColumnMode(context); + + final style = isColumnMode + ? Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ) + : Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppConfig.gold, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ); + + return SafeArea( + child: Material( + type: MaterialType.transparency, + child: SlideTransition( + position: _slideAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + return GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy < -10) _close(); + }, + onTap: _toggleDetails, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 4.0, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: AppConfig.gold.withAlpha(200), + width: 2.0, + ), + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(AppConfig.borderRadius), + bottomRight: Radius.circular(AppConfig.borderRadius), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Spacer for symmetry + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + ), + // Centered content + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 8.0, + ), + child: Wrap( + spacing: 16.0, + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + "Level up", + style: style, + overflow: TextOverflow.ellipsis, + ), + CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.starFileName}", + height: 24, + width: 24, + ), + ], + ), + ), + ), + SizedBox( + width: constraints.maxWidth >= 600 ? 120.0 : 65.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (Environment.isStagingEnvironment) + SizedBox( + width: 32.0, + height: 32.0, + child: Center( + child: IconButton( + icon: const Icon(Icons.arrow_drop_down), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4.0), + ), + onPressed: _toggleDetails, + constraints: const BoxConstraints(), + color: + Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_manager.dart b/lib/pangea/analytics_misc/level_up/level_up_manager.dart new file mode 100644 index 000000000..7716d4165 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_manager.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LevelUpManager { + // Singleton instance so analytics can be generated when level up is initiated, and be ready by the time user clicks on banner + static final LevelUpManager instance = LevelUpManager._internal(); + + LevelUpManager._internal(); + + int prevLevel = 0; + int level = 0; + + int prevGrammar = 0; + int nextGrammar = 0; + int prevVocab = 0; + int nextVocab = 0; + + String? userL2Code; + + ConstructSummary? constructSummary; + + bool hasSeenPopup = false; + bool shouldAutoPopup = false; + String? error; + + Future preloadAnalytics( + BuildContext context, + int level, + int prevLevel, + ) async { + this.level = level; + this.prevLevel = prevLevel; + + //For on route change behavior, if added in the future + shouldAutoPopup = true; + + nextGrammar = MatrixState + .pangeaController.getAnalytics.constructListModel.grammarLemmas; + nextVocab = MatrixState + .pangeaController.getAnalytics.constructListModel.vocabLemmas; + + userL2Code = MatrixState.pangeaController.languageController + .activeL2Code() + ?.toUpperCase(); + + getConstructFromLevelUp(); + + final LanguageModel? l2 = + MatrixState.pangeaController.languageController.userL2; + final Room? analyticsRoom = + MatrixState.pangeaController.matrixState.client.analyticsRoomLocal(l2!); + + if (analyticsRoom != null) { + // How to get all summary events in the timeline + final timeline = await analyticsRoom.getTimeline(); + final summaryEvents = timeline.events + .where( + (e) => e.type == PangeaEventTypes.constructSummary, + ) + .map( + (e) => ConstructSummary.fromJson(e.content), + ) + .toList(); + + //Find previous summary to get grammar constructs and vocab numbers from + final lastSummary = summaryEvents + .where((summary) => summary.upperLevel == prevLevel) + .toList() + .isNotEmpty + ? summaryEvents + .firstWhere((summary) => summary.upperLevel == prevLevel) + : null; + + //Set grammar and vocab from last level summary, if there is one. Otherwise set to placeholder data + if (lastSummary != null && + lastSummary.levelVocabConstructs != null && + lastSummary.levelGrammarConstructs != null) { + prevVocab = lastSummary.levelVocabConstructs!; + prevGrammar = lastSummary.levelGrammarConstructs!; + } else { + prevGrammar = (nextGrammar / prevLevel) as int; + prevVocab = (nextVocab / prevLevel) as int; + } + } + } + + //for testing, just fetch last level up from saved analytics + void getConstructFromButton() { + constructSummary = MatrixState.pangeaController.getAnalytics + .getConstructSummaryFromStateEvent(); + debugPrint( + "Last saved construct summary from analytics controller function: ${constructSummary?.toJson()}", + ); + } + + //for getting real level up data when leveled up + void getConstructFromLevelUp() async { + try { + constructSummary = await MatrixState.pangeaController.getAnalytics + .generateLevelUpAnalytics( + prevLevel, + level, + ); + } catch (e) { + error = e.toString(); + } + } + + void markPopupSeen() { + hasSeenPopup = true; + shouldAutoPopup = false; + } + + void reset() { + hasSeenPopup = false; + shouldAutoPopup = false; + prevLevel = 0; + level = 0; + prevGrammar = 0; + nextGrammar = 0; + prevVocab = 0; + nextVocab = 0; + constructSummary = null; + error = null; + } +} diff --git a/lib/pangea/analytics_misc/level_up/level_up_popup.dart b/lib/pangea/analytics_misc/level_up/level_up_popup.dart new file mode 100644 index 000000000..c7aa2fb3b --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/level_up_popup.dart @@ -0,0 +1,521 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:animated_flip_counter/animated_flip_counter.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:confetti/confetti.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix_api_lite/generated/model.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_banner.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/level_up_manager.dart'; +import 'package:fluffychat/pangea/analytics_misc/level_up/rain_confetti.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/level_bar.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_bar/progress_bar_details.dart'; +import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart'; +import 'package:fluffychat/pangea/constructs/construct_repo.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; + +class LevelUpPopup extends StatelessWidget { + const LevelUpPopup({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return FullWidthDialog( + maxWidth: 400, + maxHeight: 800, + dialogContent: Scaffold( + appBar: AppBar( + centerTitle: true, + title: kIsWeb + ? Text( + L10n.of(context).youHaveLeveledUp, + style: const TextStyle( + color: AppConfig.gold, + fontWeight: FontWeight.w600, + ), + ) + : null, + ), + body: LevelUpPopupContent( + prevLevel: LevelUpManager.instance.prevLevel, + level: LevelUpManager.instance.level, + ), + ), + ); + } +} + +class LevelUpPopupContent extends StatefulWidget { + final int prevLevel; + final int level; + + const LevelUpPopupContent({ + super.key, + required this.prevLevel, + required this.level, + }); + + @override + State createState() => _LevelUpPopupContentState(); +} + +class _LevelUpPopupContentState extends State + with SingleTickerProviderStateMixin { + late int _endGrammar; + late int _endVocab; + final int _startGrammar = LevelUpManager.instance.prevGrammar; + final int _startVocab = LevelUpManager.instance.prevVocab; + Timer? _summaryPollTimer; + final String? _error = LevelUpManager.instance.error; + String language = LevelUpManager.instance.userL2Code ?? "N/A"; + + late final AnimationController _controller; + late final ConfettiController _confettiController; + bool _hasBlastedConfetti = false; + final Duration _animationDuration = const Duration(seconds: 5); + + Uri? avatarUrl; + late final Future profile; + int displayedLevel = -1; + late ConstructSummary? _constructSummary; + + @override + void initState() { + super.initState(); + LevelUpManager.instance.markPopupSeen(); + displayedLevel = widget.prevLevel; + _confettiController = + ConfettiController(duration: const Duration(seconds: 1)); + _endGrammar = LevelUpManager.instance.nextGrammar; + _endVocab = LevelUpManager.instance.nextVocab; + _constructSummary = LevelUpManager.instance.constructSummary; + // Poll for constructSummary if not available + if (_constructSummary == null) { + _summaryPollTimer = + Timer.periodic(const Duration(milliseconds: 300), (timer) { + final summary = LevelUpManager.instance.constructSummary; + if (summary != null) { + setState(() { + _constructSummary = summary; + }); + timer.cancel(); + } + }); + } + final client = Matrix.of(context).client; + client.fetchOwnProfile().then((profile) { + setState(() { + avatarUrl = profile.avatarUrl; + }); + }); + _controller = AnimationController( + duration: _animationDuration, + vsync: this, + ); + + // halfway through the animation, switch to the new level + _controller.addListener(() { + if (_controller.value >= 0.5 && displayedLevel == widget.prevLevel) { + setState(() { + displayedLevel = widget.level; + }); + } + }); + + _controller.addListener(() { + if (_controller.value >= 0.5 && !_hasBlastedConfetti) { + //_confettiController.play(); + _hasBlastedConfetti = true; + rainConfetti(context); + } + }); + + _controller.forward(); + } + + @override + void dispose() { + _summaryPollTimer?.cancel(); + _controller.dispose(); + _confettiController.dispose(); + LevelUpManager.instance.reset(); + stopConfetti(); + super.dispose(); + } + + int _getSkillXP(LearningSkillsEnum skill) { + if (_constructSummary == null) return 0; + return switch (skill) { + LearningSkillsEnum.writing => + _constructSummary?.writingConstructScore ?? 0, + LearningSkillsEnum.reading => + _constructSummary?.readingConstructScore ?? 0, + LearningSkillsEnum.speaking => + _constructSummary?.speakingConstructScore ?? 0, + LearningSkillsEnum.hearing => + _constructSummary?.hearingConstructScore ?? 0, + _ => 0, + }; + } + + @override + @override + Widget build(BuildContext context) { + final Animation progressAnimation = + Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.5)), + ); + + final Animation vocabAnimation = + IntTween(begin: _startVocab, end: _endVocab).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation grammarAnimation = + IntTween(begin: _startGrammar, end: _endGrammar).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeInOutQuad), + ), + ); + + final Animation skillsOpacity = + Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeIn), + ), + ); + + final Animation shrinkMultiplier = + Tween(begin: 1.0, end: 0.3).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeInOut), + ), + ); + + final colorScheme = Theme.of(context).colorScheme; + final grammarVocabStyle = Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ); + final username = + Matrix.of(context).client.userID?.split(':').first.substring(1) ?? ''; + + return Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(24.0), + child: avatarUrl == null + ? Avatar( + name: username, + showPresence: false, + size: 150 * shrinkMultiplier.value, + ) + : ClipOval( + child: MxcImage( + uri: avatarUrl, + width: 150 * shrinkMultiplier.value, + height: 150 * shrinkMultiplier.value, + ), + ), + ), + Text( + language, + style: TextStyle( + fontSize: 24 * skillsOpacity.value, + color: AppConfig.goldLight, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + // Progress bar + Level + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return LevelBar( + details: const LevelBarDetails( + fillColor: AppConfig.goldLight, + currentPoints: 0, + widthMultiplier: 1, + ), + progressBarDetails: ProgressBarDetails( + totalWidth: constraints.maxWidth * + progressAnimation.value, + height: 20, + borderColor: colorScheme.primary, + ), + ); + }, + ), + ), + const SizedBox(width: 8), + Text( + "⭐", + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedFlipCounter( + value: displayedLevel, + textStyle: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: AppConfig.goldLight, + ), + duration: const Duration(milliseconds: 1000), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + const SizedBox(height: 25), + + // Vocab and grammar row + AnimatedBuilder( + animation: _controller, + builder: (_, __) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endVocab - _startVocab}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.dictionary, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${vocabAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], + ), + const SizedBox(width: 40), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "+ ${_endGrammar - _startGrammar}", + style: const TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + Row( + children: [ + Icon( + Symbols.toys_and_games, + color: colorScheme.primary, + size: 35, + ), + const SizedBox(width: 8), + Text( + '${grammarAnimation.value}', + style: grammarVocabStyle, + ), + ], + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + + // Skills section + AnimatedBuilder( + animation: skillsOpacity, + builder: (_, __) => Opacity( + opacity: skillsOpacity.value, + child: _error == null + ? Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildSkillsTable(context), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Text( + _constructSummary?.textSummary ?? + L10n.of(context).loadingPleaseWait, + textAlign: TextAlign.left, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: CachedNetworkImage( + imageUrl: + "${AppConfig.assetsBaseURL}/${LevelUpConstants.dinoLevelUPFileName}", + width: 400, + fit: BoxFit.cover, + ), + ), + ], + ) + // if error getting construct summary + : Row( + children: [ + Tooltip( + message: L10n.of(context).oopsSomethingWentWrong, + child: Icon( + Icons.error, + color: Theme.of(context).colorScheme.error, + ), + ), + ], + ), + ), + ), + // Share button, currently no functionality + // ElevatedButton( + // onPressed: () { + // // Add share functionality + // }, + // style: ElevatedButton.styleFrom( + // backgroundColor: Colors.white, + // foregroundColor: Colors.black, + // padding: const EdgeInsets.symmetric( + // vertical: 12, + // horizontal: 24, + // ), + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(8), + // ), + // ), + // child: const Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Text( + // "Share with Friends", + // style: TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // ), + // ), + // SizedBox( + // width: 8, + // ), + // Icon( + // Icons.ios_share, + // size: 20, + // ), + // ], + // ), + // ), + ], + ), + ), + ], + ); + } + + Widget _buildSkillsTable(BuildContext context) { + final visibleSkills = LearningSkillsEnum.values + .where((skill) => (_getSkillXP(skill) > -1) && skill.isVisible) + .toList(); + + const itemsPerRow = 4; + // chunk into rows of up to 4 + final rows = >[ + for (var i = 0; i < visibleSkills.length; i += itemsPerRow) + visibleSkills.sublist( + i, + min(i + itemsPerRow, visibleSkills.length), + ), + ]; + + return Column( + children: rows.map((row) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: row.map((skill) { + return Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + skill.tooltip(context), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Icon( + skill.icon, + size: 25, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(height: 4), + Text( + '+ ${_getSkillXP(skill)} XP', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConfig.gold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }).toList(), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/pangea/analytics_misc/level_up/rain_confetti.dart b/lib/pangea/analytics_misc/level_up/rain_confetti.dart new file mode 100644 index 000000000..13e43aca3 --- /dev/null +++ b/lib/pangea/analytics_misc/level_up/rain_confetti.dart @@ -0,0 +1,121 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:confetti/confetti.dart'; + +import 'package:fluffychat/config/app_config.dart'; + +OverlayEntry? _confettiEntry; +ConfettiController? _blastController; +ConfettiController? _rainController; + +void rainConfetti(BuildContext context) { + if (_confettiEntry != null) return; // Prevent duplicates + + _blastController = ConfettiController(duration: const Duration(seconds: 1)); + _rainController = ConfettiController(duration: const Duration(seconds: 3)); + + _blastController!.play(); + _rainController!.play(); + + final screenWidth = MediaQuery.of(context).size.width; + final isSmallScreen = screenWidth < 600; + final count = isSmallScreen ? 2 : 5; + final spacing = screenWidth / (count + 1); + + _confettiEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + // Initial center blast + Positioned( + top: 0, + left: screenWidth / 2, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _blastController!, + blastDirectionality: BlastDirectionality.explosive, + shouldLoop: false, + emissionFrequency: .02, + numberOfParticles: 40, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + minBlastForce: 10, + maxBlastForce: 40, + gravity: 0.07, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ), + + // Rain confetti from the top + ...List.generate(count, (index) { + final left = spacing * (index + 1) - 10; + + return Positioned( + top: -30, // Small buffer above top edge + left: left, + child: IgnorePointer( + child: ConfettiWidget( + confettiController: _rainController!, + blastDirectionality: BlastDirectionality.directional, + blastDirection: 3 * pi / 2, + shouldLoop: true, + maxBlastForce: 5, + minBlastForce: 2, + minimumSize: const Size(20, 20), + maximumSize: const Size(25, 25), + gravity: 0.07, + emissionFrequency: 0.1, + numberOfParticles: 2, + colors: const [AppConfig.goldLight, AppConfig.gold], + createParticlePath: drawStar, + ), + ), + ); + }), + ], + ), + ); + + Overlay.of(context, rootOverlay: true).insert(_confettiEntry!); +} + +void stopConfetti() { + _confettiEntry?.remove(); + _confettiEntry = null; + + _blastController?.dispose(); + _blastController = null; + + _rainController?.dispose(); + _rainController = null; +} + +Path drawStar(Size size) { + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo( + halfWidth + externalRadius * cos(step), + halfWidth + externalRadius * sin(step), + ); + path.lineTo( + halfWidth + internalRadius * cos(step + halfDegreesPerStep), + halfWidth + internalRadius * sin(step + halfDegreesPerStep), + ); + } + path.close(); + return path; +} diff --git a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart index 4950c40c3..72839b716 100644 --- a/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart +++ b/lib/pangea/chat_list/utils/chat_list_handle_space_tap.dart @@ -3,16 +3,15 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../common/utils/error_handler.dart'; -Future _showInviteDialog(Room room, BuildContext context) async { +Future showInviteDialog(Room room, BuildContext context) async { + if (room.membership != Membership.invite) return; final acceptInvite = await showOkCancelAlertDialog( context: context, title: L10n.of(context).youreInvited, @@ -23,7 +22,7 @@ Future _showInviteDialog(Room room, BuildContext context) async { cancelLabel: L10n.of(context).decline, ); - await showFutureLoadingDialog( + final resp = await showFutureLoadingDialog( context: context, future: () async { if (acceptInvite == OkCancelResult.ok) { @@ -31,28 +30,25 @@ Future _showInviteDialog(Room room, BuildContext context) async { context.go( room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}", ); - return; + return room.id; + } else if (acceptInvite == OkCancelResult.cancel) { + await room.leave(); } - await room.leave(); }, ); + + if (!resp.isError && resp.result is String) { + context.go("/rooms?spaceId=${resp.result}"); + } } // ignore: curly_braces_in_flow_control_structures void chatListHandleSpaceTap( BuildContext context, - ChatListController controller, Room space, ) { void setActiveSpaceAndCloseChat() { - controller.setActiveSpace(space.id); - - if (FluffyThemes.isColumnMode(context)) { - context.go('/rooms/${space.id}'); - } else if (controller.activeChat != null && - !space.isFirstOrSecondChild(controller.activeChat!)) { - context.go("/rooms"); - } + context.go("/rooms?spaceId=${space.id}"); } void autoJoin(Room space) { @@ -85,7 +81,7 @@ void chatListHandleSpaceTap( justInputtedCode == space.classCode) { // do nothing } else { - _showInviteDialog(space, context); + showInviteDialog(space, context); } break; case Membership.leave: diff --git a/lib/pangea/chat_settings/widgets/delete_space_dialog.dart b/lib/pangea/chat_settings/widgets/delete_space_dialog.dart index 6afb8ab26..4adce8e62 100644 --- a/lib/pangea/chat_settings/widgets/delete_space_dialog.dart +++ b/lib/pangea/chat_settings/widgets/delete_space_dialog.dart @@ -146,7 +146,7 @@ class DeleteSpaceDialogState extends State { style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), - Expanded( + Flexible( child: SingleChildScrollView( child: Builder( builder: (context) { diff --git a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart b/lib/pangea/chat_settings/widgets/space_invite_buttons.dart deleted file mode 100644 index 5fd3c7347..000000000 --- a/lib/pangea/chat_settings/widgets/space_invite_buttons.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:matrix/matrix.dart'; -import 'package:universal_html/html.dart' as html; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; - -class SpaceInviteButtons extends StatefulWidget { - final Room room; - // final ScrollController scrollController; - const SpaceInviteButtons({ - super.key, - required this.room, - // required this.scrollController, - }); - - @override - SpaceInviteButtonsController createState() => SpaceInviteButtonsController(); -} - -class SpaceInviteButtonsController extends State { - // bool get isVisible { - // final context = (widget.key as GlobalKey).currentContext; - // if (context == null) return false; - - // final renderBox = context.findRenderObject() as RenderBox; - // final position = renderBox.localToGlobal(Offset.zero); - - // final size = renderBox.size; - // final screenHeight = MediaQuery.of(context).size.height; - - // debugPrint("position: $position, size: $size, screenHeight: $screenHeight"); - - // // Check if any part of the widget is within the visible range - // return position.dy + size.height > 0 && position.dy < screenHeight; - // } - - @override - void initState() { - // WidgetsBinding.instance.addPostFrameCallback( - // (_) => debugPrint("isVisible: $isVisible"), - // ); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final spaceCode = widget.room.classCode; - if (!widget.room.isSpace || spaceCode == null) { - return const SizedBox.shrink(); - } - - return SizedBox( - height: 150.0, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - ), - child: ElevatedButton( - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.share_outlined, - ), - Text(L10n.of(context).shareSpaceLink), - ], - ), - onPressed: () async { - final String initialUrl = - kIsWeb ? html.window.origin! : Environment.frontendURL; - final link = - "$initialUrl/#/join_with_link?${SpaceConstants.classCode}=$spaceCode"; - await Clipboard.setData( - ClipboardData( - text: link, - ), - ); - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).copiedToClipboard, - ), - ), - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - right: 16.0, - left: 16.0, - ), - child: ElevatedButton( - child: Row( - spacing: 8.0, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.share_outlined, - ), - Text(L10n.of(context).shareInviteCode(spaceCode)), - ], - ), - onPressed: () async { - await Clipboard.setData(ClipboardData(text: spaceCode)); - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).copiedToClipboard, - ), - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/constructs/construct_repo.dart b/lib/pangea/constructs/construct_repo.dart index 6d97ce0af..d847f8168 100644 --- a/lib/pangea/constructs/construct_repo.dart +++ b/lib/pangea/constructs/construct_repo.dart @@ -11,6 +11,8 @@ import 'package:fluffychat/widgets/matrix.dart'; class ConstructSummary { final int upperLevel; final int lowerLevel; + int? levelVocabConstructs; + int? levelGrammarConstructs; final String language; final String textSummary; final int writingConstructScore; @@ -21,6 +23,8 @@ class ConstructSummary { ConstructSummary({ required this.upperLevel, required this.lowerLevel, + this.levelVocabConstructs, + this.levelGrammarConstructs, required this.language, required this.textSummary, required this.writingConstructScore, @@ -33,6 +37,8 @@ class ConstructSummary { return { 'upper_level': upperLevel, 'lower_level': lowerLevel, + 'level_grammar_constructs': levelGrammarConstructs, + 'level_vocab_constructs': levelVocabConstructs, 'language': language, 'text_summary': textSummary, 'writing_construct_score': writingConstructScore, @@ -46,6 +52,8 @@ class ConstructSummary { return ConstructSummary( upperLevel: json['upper_level'], lowerLevel: json['lower_level'], + levelGrammarConstructs: json['level_grammar_constructs'], + levelVocabConstructs: json['level_vocab_constructs'], language: json['language'], textSummary: json['text_summary'], writingConstructScore: json['writing_construct_score'], diff --git a/lib/pangea/events/constants/pangea_event_types.dart b/lib/pangea/events/constants/pangea_event_types.dart index a385fe97d..fe2a47e18 100644 --- a/lib/pangea/events/constants/pangea_event_types.dart +++ b/lib/pangea/events/constants/pangea_event_types.dart @@ -26,7 +26,6 @@ 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/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index f6f7e6b91..9df0b2e0d 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -268,6 +268,21 @@ class PangeaMessageEvent { final botTranscription = SpeechToTextModel.fromJson( Map.from(rawBotTranscription), ); + + _representations?.add( + RepresentationEvent( + timeline: timeline, + parentMessageEvent: _event, + content: PangeaRepresentation( + langCode: botTranscription.langCode, + text: botTranscription.transcript.text, + originalSent: false, + originalWritten: false, + speechToText: botTranscription, + ), + ), + ); + return botTranscription; } @@ -398,7 +413,15 @@ class PangeaMessageEvent { ), ); } - _representations!.add(sent); + + // If originalSent has no tokens, there is not way to generate a tokens event + // and send it as a related event, since original sent has not eventID to set + // as parentEventId. In this case, it's better to generate a new representation + // with an eventID and send the related tokens event to that representation. + // This is a rare situation, and has only been seen with some bot messages. + if (sent.tokens != null) { + _representations!.add(sent); + } } catch (err, s) { ErrorHandler.logError( m: "error parsing originalSent", diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 58616de36..316ef7199 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:html_unescape/html_unescape.dart'; +import 'package:http/http.dart' as http; import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/pangea/extensions/pangea_rooms_chunk_extension.dart b/lib/pangea/extensions/pangea_rooms_chunk_extension.dart new file mode 100644 index 000000000..78785948f --- /dev/null +++ b/lib/pangea/extensions/pangea_rooms_chunk_extension.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +import 'package:matrix/matrix_api_lite/generated/model.dart'; + +import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; + +extension PangeaRoomsChunk on PublicRoomsChunk { + /// Use Random with a seed to get the default + /// avatar associated with this space + String defaultAvatar() { + final int seed = roomId.hashCode; + return SpaceConstants.publicSpaceIcons[Random(seed).nextInt( + SpaceConstants.publicSpaceIcons.length, + )]; + } +} diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index e24427ef3..799ebdb66 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -277,6 +277,57 @@ extension EventsRoomExtension on Room { }) async { BookmarkedActivitiesRepo.save(activity); + String? imageURL = activity.imageURL; + final eventId = await pangeaSendTextEvent( + activity.markdown, + messageTag: ModelKey.messageTagActivityPlan, + ); + + Uint8List? bytes = avatar; + if (imageURL != null && bytes == null) { + try { + final resp = await http + .get(Uri.parse(imageURL)) + .timeout(const Duration(seconds: 5)); + bytes = resp.bodyBytes; + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "avatarURL": imageURL, + }, + ); + } + } + + if (bytes != null && imageURL == null) { + final url = await client.uploadContent( + bytes, + filename: filename, + ); + imageURL = url.toString(); + } + + MatrixFile? file; + if (filename != null && bytes != null) { + file = MatrixFile( + bytes: bytes, + name: filename, + ); + } + + if (file != null) { + final content = { + 'msgtype': file.msgType, + 'body': file.name, + 'filename': file.name, + 'url': imageURL, + ModelKey.messageTags: ModelKey.messageTagActivityPlan, + }; + await sendEvent(content); + } + if (canChangeStateEvent(PangeaEventTypes.activityPlan)) { await client.setRoomStateWithKey( id, @@ -284,6 +335,10 @@ extension EventsRoomExtension on Room { "", activity.toJson(), ); + + if (eventId != null && canChangeStateEvent(EventTypes.RoomPinnedEvents)) { + await setPinnedEvents([eventId]); + } } } diff --git a/lib/pangea/find_your_people/public_space_tile.dart b/lib/pangea/find_your_people/public_space_tile.dart index 7880937a0..5dd19f68f 100644 --- a/lib/pangea/find_your_people/public_space_tile.dart +++ b/lib/pangea/find_your_people/public_space_tile.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicator_button.dart'; +import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -35,14 +37,26 @@ class PublicSpaceTile extends StatelessWidget { height: isColumnMode ? 80.0 : 58.0, child: Row( children: [ - Avatar( - mxContent: space.avatarUrl, - name: space.name, - size: isColumnMode ? 80.0 : 58.0, - borderRadius: BorderRadius.circular( - 10, - ), - ), + (space.avatarUrl != null) + ? Avatar( + mxContent: space.avatarUrl, + name: space.name, + size: isColumnMode ? 80.0 : 58.0, + borderRadius: BorderRadius.circular( + 10, + ), + ) + : ClipRRect( + borderRadius: BorderRadius.circular( + 10, + ), + child: CachedNetworkImage( + imageUrl: space.defaultAvatar(), + width: isColumnMode ? 80.0 : 58.0, + height: isColumnMode ? 80.0 : 58.0, + fit: BoxFit.cover, + ), + ), Flexible( child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/pangea/learning_settings/models/language_model.dart b/lib/pangea/learning_settings/models/language_model.dart index 5e62e312e..285577bfc 100644 --- a/lib/pangea/learning_settings/models/language_model.dart +++ b/lib/pangea/learning_settings/models/language_model.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/l2_support_enum.dart'; import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; @@ -71,13 +70,6 @@ class LanguageModel { displayName: "Unknown", ); - static LanguageModel multiLingual([BuildContext? context]) => LanguageModel( - displayName: context != null - ? L10n.of(context).multiLingualSpace - : "Multilingual Space", - langCode: LanguageKeys.multiLanguage, - ); - String? getDisplayName(BuildContext context) { return displayName; } diff --git a/lib/pangea/learning_settings/pages/settings_learning.dart b/lib/pangea/learning_settings/pages/settings_learning.dart index 76e4fd764..304beebe6 100644 --- a/lib/pangea/learning_settings/pages/settings_learning.dart +++ b/lib/pangea/learning_settings/pages/settings_learning.dart @@ -125,10 +125,12 @@ class SettingsLearningController extends State { if (formKey.currentState!.validate()) { await showFutureLoadingDialog( context: context, - future: () async => pangeaController.userController.updateProfile( - (_) => _profile, - waitForDataInSync: true, - ), + future: () async => pangeaController.userController + .updateProfile( + (_) => _profile, + waitForDataInSync: true, + ) + .timeout(const Duration(seconds: 15)), ); Navigator.of(context).pop(); } diff --git a/lib/pangea/learning_settings/utils/p_language_store.dart b/lib/pangea/learning_settings/utils/p_language_store.dart index 075912e75..60310f18b 100644 --- a/lib/pangea/learning_settings/utils/p_language_store.dart +++ b/lib/pangea/learning_settings/utils/p_language_store.dart @@ -42,7 +42,6 @@ class PLanguageStore { _langList = _langList.toSet().toList(); _langList.sort((a, b) => a.displayName.compareTo(b.displayName)); - _langList.insert(0, LanguageModel.multiLingual()); } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError( diff --git a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart index c71fea714..d47b48cf0 100644 --- a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart +++ b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart @@ -14,7 +14,6 @@ class PLanguageDropdown extends StatefulWidget { final List languages; final LanguageModel? initialLanguage; final Function(LanguageModel) onChange; - final bool showMultilingual; final bool isL2List; final String? decorationText; final String? error; @@ -28,7 +27,6 @@ class PLanguageDropdown extends StatefulWidget { required this.languages, required this.onChange, required this.initialLanguage, - this.showMultilingual = false, this.decorationText, this.isL2List = false, this.error, @@ -132,15 +130,6 @@ class PLanguageDropdownState extends State { ), ), items: [ - if (widget.showMultilingual) - DropdownMenuItem( - value: LanguageModel.multiLingual(context), - enabled: widget.enabled, - child: LanguageDropDownEntry( - languageModel: LanguageModel.multiLingual(context), - isL2List: widget.isL2List, - ), - ), ...sortedLanguages.map( (languageModel) => DropdownMenuItem( value: languageModel, diff --git a/lib/pangea/onboarding/onboarding.dart b/lib/pangea/onboarding/onboarding.dart index bf2f0979c..53d45bd87 100644 --- a/lib/pangea/onboarding/onboarding.dart +++ b/lib/pangea/onboarding/onboarding.dart @@ -80,17 +80,16 @@ class OnboardingController extends State { Future startChatWithBot() async { final resp = await showFutureLoadingDialog( context: context, - future: () => Matrix.of(context).client.createRoom( - invite: [BotName.byEnvironment], - isDirect: true, - preset: CreateRoomPreset.trustedPrivateChat, - initialState: [ - BotOptionsModel(mode: BotMode.directChat).toStateEvent, - RoomDefaults.defaultPowerLevels( - Matrix.of(context).client.userID!, - ), - ], + future: () => Matrix.of(context).client.startDirectChat( + BotName.byEnvironment, + preset: CreateRoomPreset.trustedPrivateChat, + initialState: [ + BotOptionsModel(mode: BotMode.directChat).toStateEvent, + RoomDefaults.defaultPowerLevels( + Matrix.of(context).client.userID!, ), + ], + ), ); if (resp.isError) return; context.go("/rooms/${resp.result}"); diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index 8486a9991..ee9f64546 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -22,6 +22,8 @@ class PhoneticTranscriptionWidget extends StatefulWidget { final bool enabled; + final VoidCallback? onTranscriptionFetched; + const PhoneticTranscriptionWidget({ super.key, required this.text, @@ -30,6 +32,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget { this.iconSize, this.iconColor, this.enabled = true, + this.onTranscriptionFetched, }); @override @@ -51,6 +54,17 @@ class _PhoneticTranscriptionWidgetState _fetchTranscription(); } + @override + void didUpdateWidget( + covariant PhoneticTranscriptionWidget oldWidget, + ) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text || + oldWidget.textLanguage != widget.textLanguage) { + _fetchTranscription(); + } + } + Future _fetchTranscription() async { try { setState(() { @@ -92,7 +106,12 @@ class _PhoneticTranscriptionWidgetState }, ); } finally { - if (mounted) setState(() => _isLoading = false); + if (mounted) { + setState(() { + _isLoading = false; + widget.onTranscriptionFetched?.call(); + }); + } } } diff --git a/lib/pangea/public_spaces/public_room_bottom_sheet.dart b/lib/pangea/public_spaces/public_room_bottom_sheet.dart index 89ed09700..a634dc39b 100644 --- a/lib/pangea/public_spaces/public_room_bottom_sheet.dart +++ b/lib/pangea/public_spaces/public_room_bottom_sheet.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -9,7 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; +import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -212,10 +210,7 @@ class PublicRoomBottomSheetState extends State { : ClipRRect( borderRadius: BorderRadius.circular(24.0), child: CachedNetworkImage( - imageUrl: SpaceConstants - .publicSpaceIcons[Random().nextInt( - SpaceConstants.publicSpaceIcons.length, - )], + imageUrl: chunk!.defaultAvatar(), width: 160.0, height: 160.0, fit: BoxFit.cover, diff --git a/lib/pangea/public_spaces/public_space_card.dart b/lib/pangea/public_spaces/public_space_card.dart index 5b8063225..29521f67b 100644 --- a/lib/pangea/public_spaces/public_space_card.dart +++ b/lib/pangea/public_spaces/public_space_card.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -7,8 +5,8 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; +import 'package:fluffychat/pangea/extensions/pangea_rooms_chunk_extension.dart'; import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart'; -import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; class PublicSpaceCard extends StatelessWidget { @@ -71,10 +69,7 @@ class PublicSpaceCard extends StatelessWidget { fit: BoxFit.cover, ) : CachedNetworkImage( - imageUrl: SpaceConstants - .publicSpaceIcons[Random().nextInt( - SpaceConstants.publicSpaceIcons.length, - )], + imageUrl: space.defaultAvatar(), width: width, height: width, fit: BoxFit.cover, diff --git a/lib/pangea/subscription/controllers/subscription_controller.dart b/lib/pangea/subscription/controllers/subscription_controller.dart index b3fb0e991..91027f88c 100644 --- a/lib/pangea/subscription/controllers/subscription_controller.dart +++ b/lib/pangea/subscription/controllers/subscription_controller.dart @@ -58,7 +58,7 @@ class SubscriptionController extends BaseController { final bool hasSubscription = currentSubscriptionInfo?.currentSubscriptionId != null; - return hasSubscription; + return hasSubscription || _userController.inTrialWindow(); } bool _isInitializing = false; diff --git a/lib/pangea/toolbar/widgets/message_audio_card.dart b/lib/pangea/toolbar/widgets/message_audio_card.dart index 2cc529422..3b854b5bf 100644 --- a/lib/pangea/toolbar/widgets/message_audio_card.dart +++ b/lib/pangea/toolbar/widgets/message_audio_card.dart @@ -8,7 +8,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; @@ -96,9 +95,7 @@ class MessageAudioCardState extends State { ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onPrimary, ) - : const CardErrorWidget( - error: "Null audio file in message_audio_card", - ); + : const SizedBox(); } } diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index daaecd2ae..d95fee56a 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -18,8 +18,6 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; -import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; @@ -101,6 +99,8 @@ class MessageOverlayController extends State bool showSpeechTranslation = false; String? speechTranslation; + final StreamController contentChangedStream = StreamController.broadcast(); + double maxWidth = AppConfig.toolbarMinWidth; ///////////////////////////////////// @@ -121,6 +121,7 @@ class MessageOverlayController extends State WidgetsBinding.instance.addPostFrameCallback( (_) => widget.chatController.clearSelectedEvents(), ); + contentChangedStream.close(); super.dispose(); } @@ -132,7 +133,11 @@ class MessageOverlayController extends State RepresentationEvent? repEvent = pangeaMessageEvent?.messageDisplayRepresentation; - repEvent ??= await _fetchNewRepEvent(); + + if (repEvent == null || + (repEvent.event == null && repEvent.tokens == null)) { + repEvent = await _fetchNewRepEvent(); + } if (repEvent?.event != null) { await repEvent!.sendTokensEvent( @@ -142,24 +147,6 @@ class MessageOverlayController extends State MatrixState.pangeaController.languageController.userL2!.langCode, ); } - // If repEvent is originalSent but it's missing tokens, then fetch tokens. - // An edge case, but has happened with some bot message. - else if (repEvent != null && - repEvent.tokens == null && - repEvent.content.originalSent) { - final tokens = await repEvent.tokensGlobal( - pangeaMessageEvent!.senderId, - pangeaMessageEvent!.event.originServerTs, - ); - await pangeaMessageEvent!.room.pangeaSendTextEvent( - pangeaMessageEvent!.messageDisplayText, - editEventId: pangeaMessageEvent!.eventId, - originalSent: pangeaMessageEvent!.originalSent?.content, - originalWritten: pangeaMessageEvent!.originalWritten?.content, - tokensSent: PangeaMessageTokens(tokens: tokens), - choreo: pangeaMessageEvent!.originalSent?.choreo, - ); - } // Get all the lemma infos final messageVocabConstructIds = pangeaMessageEvent! @@ -587,7 +574,10 @@ class MessageOverlayController extends State void setTranslation(String value) { if (mounted) { - setState(() => translation = value); + setState(() { + translation = value; + contentChangedStream.add(true); + }); } } @@ -598,12 +588,18 @@ class MessageOverlayController extends State } if (showTranslation == show) return; - setState(() => showTranslation = show); + setState(() { + showTranslation = show; + contentChangedStream.add(true); + }); } void setSpeechTranslation(String value) { if (mounted) { - setState(() => speechTranslation = value); + setState(() { + speechTranslation = value; + contentChangedStream.add(true); + }); } } @@ -614,7 +610,10 @@ class MessageOverlayController extends State } if (showSpeechTranslation == show) return; - setState(() => showSpeechTranslation = show); + setState(() { + showSpeechTranslation = show; + contentChangedStream.add(true); + }); } void setTranscription(SpeechToTextModel value) { @@ -622,13 +621,17 @@ class MessageOverlayController extends State setState(() { transcriptionError = null; transcription = value; + contentChangedStream.add(true); }); } } void setTranscriptionError(String value) { if (mounted) { - setState(() => transcriptionError = value); + setState(() { + transcriptionError = value; + contentChangedStream.add(true); + }); } } diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index f32f4d3ba..bbfad506d 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -71,6 +71,7 @@ class MessageSelectionPositionerState extends State Offset? _currentOffset; StreamSubscription? _reactionSubscription; + StreamSubscription? _contentChangedSubscription; final _animationDuration = const Duration( milliseconds: AppConfig.overlayAnimationDuration, @@ -106,6 +107,10 @@ class MessageSelectionPositionerState extends State }, ).listen((_) => setState(() {})); + _contentChangedSubscription = widget + .overlayController.contentChangedStream.stream + .listen(_onContentSizeChanged); + WidgetsBinding.instance.addPostFrameCallback((_) async { await _centeredMessageCompleter.future; if (!mounted) return; @@ -138,6 +143,7 @@ class MessageSelectionPositionerState extends State void dispose() { _animationController.dispose(); _reactionSubscription?.cancel(); + _contentChangedSubscription?.cancel(); MatrixState.pangeaController.matrixState.audioPlayer ?..stop() ..dispose(); @@ -196,34 +202,9 @@ class MessageSelectionPositionerState extends State } if (mode == ReadingAssistanceMode.selectMode) { - _overlayOffsetAnimation = Tween( - begin: _currentOffset, - end: _adjustedOriginalMessageOffset, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - )..addListener(() { - if (mounted) { - setState(() => _currentOffset = _overlayOffsetAnimation?.value); - } - }); + _resetOffsetAnimation(_adjustedOriginalMessageOffset); } else if (mode == ReadingAssistanceMode.practiceMode) { - _overlayOffsetAnimation = Tween( - begin: _currentOffset, - end: _centeredMessageOffset!, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: FluffyThemes.animationCurve, - ), - )..addListener(() { - if (mounted) { - setState(() => _currentOffset = _overlayOffsetAnimation?.value); - } - }); - + _resetOffsetAnimation(_centeredMessageOffset!); _messageSizeAnimation = Tween( begin: Size( _originalMessageSize.width, @@ -244,6 +225,40 @@ class MessageSelectionPositionerState extends State } } + void _onContentSizeChanged(_) { + Future.delayed(FluffyThemes.animationDuration, () { + final offset = _overlayMessageRenderBox?.localToGlobal(Offset.zero); + if (offset == null || !_overlayMessageRenderBox!.hasSize) { + return null; + } + + final newOffset = _adjustedMessageOffset( + _overlayMessageRenderBox!.size, + offset, + ); + + if (newOffset == _currentOffset) return; + _resetOffsetAnimation(newOffset); + _animationController.forward(from: 0); + }); + } + + void _resetOffsetAnimation(Offset offset) { + _overlayOffsetAnimation = Tween( + begin: _currentOffset, + end: offset, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: FluffyThemes.animationCurve, + ), + )..addListener(() { + if (mounted) { + setState(() => _currentOffset = _overlayOffsetAnimation?.value); + } + }); + } + T _runWithLogging( Function runner, String errorMessage, @@ -326,6 +341,14 @@ class MessageSelectionPositionerState extends State null, ); + RenderBox? get _overlayMessageRenderBox => _runWithLogging( + () => MatrixState.pAnyState.getRenderBox( + 'overlay_message_${widget.event.eventId}', + ), + "Error getting overlay message render box", + null, + ); + Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100); /// The size of the message in the chat list (as opposed to the expanded size in the center overlay) @@ -394,17 +417,28 @@ class MessageSelectionPositionerState extends State } Offset get _adjustedOriginalMessageOffset { + return _adjustedMessageOffset( + _originalMessageSize, + _originalMessageOffset, + ); + } + + Offset _adjustedMessageOffset( + Size messageSize, + Offset messageOffset, + ) { if (_messageRenderBox == null || !_messageRenderBox!.hasSize) { return _defaultMessageOffset; } - final topOffset = _originalMessageOffset.dy; - final bottomOffset = _originalMessageBottomOffset - - _reactionsHeight - - _selectionButtonsHeight; + final topOffset = messageOffset.dy; + final bottomOffset = + (_mediaQuery!.size.height - topOffset - messageSize.height) - + _reactionsHeight - + _selectionButtonsHeight; - final hasHeaderOverflow = topOffset < - (_headerHeight + AppConfig.toolbarSpacing + _audioTranscriptionHeight); + final hasHeaderOverflow = + topOffset < (_headerHeight + AppConfig.toolbarSpacing); final hasFooterOverflow = bottomOffset < (_footerHeight + AppConfig.toolbarSpacing); @@ -416,15 +450,12 @@ class MessageSelectionPositionerState extends State } if (hasHeaderOverflow) { - final difference = topOffset - - (_headerHeight + - AppConfig.toolbarSpacing + - _audioTranscriptionHeight); + final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing); double newBottomOffset = _mediaQuery!.size.height - - _originalMessageOffset.dy + + topOffset + difference - - _originalMessageSize.height - + messageSize.height - _selectionButtonsHeight; if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) { @@ -524,12 +555,6 @@ class MessageSelectionPositionerState extends State return showSelectionButtons ? AppConfig.toolbarButtonsHeight : 0; } - double get _audioTranscriptionHeight { - return widget.pangeaMessageEvent?.isAudioMessage ?? false - ? AppConfig.audioTranscriptionMaxHeight - : 0; - } - bool get _hasReactions { final reactionsEvents = widget.event.aggregatedEvents( widget.chatController.timeline!, diff --git a/lib/pangea/toolbar/widgets/overlay_center_content.dart b/lib/pangea/toolbar/widgets/overlay_center_content.dart index 835177fc5..6b5396b65 100644 --- a/lib/pangea/toolbar/widgets/overlay_center_content.dart +++ b/lib/pangea/toolbar/widgets/overlay_center_content.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dar import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart'; +import 'package:fluffychat/widgets/matrix.dart'; class OverlayCenterContent extends StatelessWidget { final Event event; @@ -69,6 +70,11 @@ class OverlayCenterContent extends StatelessWidget { MeasureRenderBox( onChange: onChangeMessageSize, child: OverlayMessage( + key: isTransitionAnimation + ? MatrixState.pAnyState + .layerLinkAndKey('overlay_message_${event.eventId}') + .key + : null, event, pangeaMessageEvent: pangeaMessageEvent, immersionMode: chatController.choreographer.immersionMode, diff --git a/lib/pangea/toolbar/widgets/overlay_header.dart b/lib/pangea/toolbar/widgets/overlay_header.dart index a13e1c2a4..81bb4aba4 100644 --- a/lib/pangea/toolbar/widgets/overlay_header.dart +++ b/lib/pangea/toolbar/widgets/overlay_header.dart @@ -109,10 +109,15 @@ class OverlayHeaderState extends State { icon: pinned ? const Icon(Icons.push_pin) : const Icon(Icons.push_pin_outlined), - onPressed: controller.pinEvent, + onPressed: () { + controller + .pinEvent() + .then((_) => setState(() {})); + }, tooltip: pinned ? l10n.unpin : l10n.pinMessage, color: theme.colorScheme.primary, ), + if (controller.canEditSelectedEvents && !controller.selectedEvents.first.isActivityMessage) IconButton( diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index e83e856d3..e678ac1ad 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -1,9 +1,12 @@ +import 'dart:math'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.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/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_content.dart'; @@ -19,6 +22,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart import 'package:fluffychat/pangea/toolbar/widgets/stt_transcript_tokens.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/file_description.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; // @ggurdin be great to explain the need/function of a widget like this @@ -149,9 +153,13 @@ class OverlayMessage extends StatelessWidget { final transcription = showTranscription ? Container( - width: messageWidth, - constraints: const BoxConstraints( - maxHeight: AppConfig.audioTranscriptionMaxHeight, + constraints: BoxConstraints( + maxWidth: min( + FluffyThemes.columnWidth * 1.5, + MediaQuery.of(context).size.width - + (ownMessage ? 0 : Avatar.defaultSize) - + 24.0, + ), ), child: Padding( padding: const EdgeInsets.all(12.0), @@ -178,6 +186,7 @@ class OverlayMessage extends StatelessWidget { child: Column( spacing: 8.0, crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ SttTranscriptTokens( model: overlayController.transcription!, @@ -197,8 +206,8 @@ class OverlayMessage extends StatelessWidget { text: overlayController .transcription!.transcript.text, textLanguage: PLanguageStore.byLangCode( - pangeaMessageEvent! - .messageDisplayLangCode, + overlayController + .transcription!.langCode, ) ?? LanguageModel.unknown, style: AppConfig.messageTextStyle( @@ -208,6 +217,9 @@ class OverlayMessage extends StatelessWidget { iconColor: textColor, enabled: event.senderId != BotName.byEnvironment, + onTranscriptionFetched: () => + overlayController.contentChangedStream + .add(true), ), ], ), @@ -226,9 +238,13 @@ class OverlayMessage extends StatelessWidget { final translation = showTranslation || showSpeechTranslation ? Container( - width: messageWidth, - constraints: const BoxConstraints( - maxHeight: AppConfig.audioTranscriptionMaxHeight, + constraints: BoxConstraints( + maxWidth: min( + FluffyThemes.columnWidth * 1.5, + MediaQuery.of(context).size.width - + (ownMessage ? 0 : Avatar.defaultSize) - + 24.0, + ), ), child: Padding( padding: const EdgeInsets.fromLTRB( @@ -271,8 +287,6 @@ class OverlayMessage extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (readingAssistanceMode == ReadingAssistanceMode.transitionMode) - transcription, if (event.relationshipType == RelationshipTypes.reply) FutureBuilder( future: event.getReplyEvent( @@ -371,8 +385,6 @@ class OverlayMessage extends StatelessWidget { ], ), ), - if (readingAssistanceMode == ReadingAssistanceMode.transitionMode) - translation, ], ), ), @@ -386,26 +398,31 @@ class OverlayMessage extends StatelessWidget { color: noBubble ? Colors.transparent : color, borderRadius: borderRadius, ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (readingAssistanceMode != ReadingAssistanceMode.transitionMode) + constraints: BoxConstraints( + maxWidth: FluffyThemes.columnWidth * 1.5, + maxHeight: maxHeight, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ transcription, - sizeAnimation != null - ? AnimatedBuilder( - animation: sizeAnimation!, - builder: (context, child) { - return SizedBox( - height: sizeAnimation!.value.height, - width: sizeAnimation!.value.width, - child: content, - ); - }, - ) - : content, - if (readingAssistanceMode != ReadingAssistanceMode.transitionMode) + sizeAnimation != null + ? AnimatedBuilder( + animation: sizeAnimation!, + builder: (context, child) { + return SizedBox( + height: sizeAnimation!.value.height, + width: sizeAnimation!.value.width, + child: content, + ); + }, + ) + : content, translation, - ], + ], + ), ), ), ); diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart index 43759a1a1..1a5ff426b 100644 --- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -126,7 +125,8 @@ class SelectModeButtonsState extends State { void _clear() { setState(() { - _audioError = null; + // Audio errors do not go away when I switch modes and back + // Is there any reason to wipe error records on clear? _translationError = null; _speechTranslationError = null; }); @@ -149,8 +149,10 @@ class SelectModeButtonsState extends State { } setState( - () => _selectedMode = - _selectedMode == mode && mode != SelectMode.audio ? null : mode, + () => _selectedMode = _selectedMode == mode && + (mode != SelectMode.audio || _audioError != null) + ? null + : mode, ); if (_selectedMode == SelectMode.audio) { @@ -202,12 +204,10 @@ class SelectModeButtonsState extends State { File? file; file = File('${tempDir.path}/${_audioBytes!.name}'); await file.writeAsBytes(_audioBytes!.bytes); - setState(() => _audioFile = file); + _audioFile = file; } - - if (mounted) setState(() => _isLoadingAudio = false); } catch (e, s) { - debugger(when: kDebugMode); + _audioError = e.toString(); ErrorHandler.logError( e: e, s: s, @@ -217,6 +217,7 @@ class SelectModeButtonsState extends State { messageEvent?.messageDisplayLangCode, }, ); + } finally { if (mounted) setState(() => _isLoadingAudio = false); } } @@ -240,7 +241,7 @@ class SelectModeButtonsState extends State { matrix?.audioPlayer?.dispose(); matrix?.audioPlayer = AudioPlayer(); matrix?.voiceMessageEventId.value = - widget.overlayController.pangeaMessageEvent?.eventId; + "${widget.overlayController.pangeaMessageEvent?.eventId}_button"; _onPlayerStateChanged = matrix?.audioPlayer?.playerStateStream.listen((state) { @@ -289,7 +290,7 @@ class SelectModeButtonsState extends State { } TtsController.stop(); - matrix?.audioPlayer?.play(); + await matrix?.audioPlayer?.play(); } catch (e, s) { setState(() => _audioError = e.toString()); ErrorHandler.logError( @@ -487,25 +488,28 @@ class SelectModeButtonsState extends State { spacing: 4.0, children: [ for (final mode in modes) - Tooltip( - message: mode.tooltip(context), - child: PressableButton( - depressed: mode == _selectedMode, - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.primaryContainer, - onPressed: () => _updateMode(mode), - playSound: true, - colorFactor: Theme.of(context).brightness == Brightness.light - ? 0.55 - : 0.3, - child: Container( - height: buttonSize, - width: buttonSize, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - shape: BoxShape.circle, + TooltipVisibility( + visible: (!_isError || mode != _selectedMode), + child: Tooltip( + message: mode.tooltip(context), + child: PressableButton( + depressed: mode == _selectedMode, + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + onPressed: () => _updateMode(mode), + playSound: mode != SelectMode.audio, + colorFactor: Theme.of(context).brightness == Brightness.light + ? 0.55 + : 0.3, + child: Container( + height: buttonSize, + width: buttonSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: icon(mode), ), - child: icon(mode), ), ), ), diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index a3e9481cb..dcb86b6eb 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -73,15 +73,18 @@ class WordZoomWidget extends StatelessWidget { ), ), ), - Text( - token.text.content, - style: TextStyle( - fontSize: 32.0, - fontWeight: FontWeight.w600, - height: 1.2, - color: Theme.of(context).brightness == Brightness.light - ? AppConfig.yellowDark - : AppConfig.yellowLight, + Flexible( + child: Text( + token.text.content, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 32.0, + fontWeight: FontWeight.w600, + height: 1.2, + color: Theme.of(context).brightness == Brightness.light + ? AppConfig.yellowDark + : AppConfig.yellowLight, + ), ), ), ConstructXpWidget( diff --git a/lib/utils/client_manager.dart b/lib/utils/client_manager.dart index bde4dd1c5..974ef75d3 100644 --- a/lib/utils/client_manager.dart +++ b/lib/utils/client_manager.dart @@ -129,6 +129,7 @@ abstract class ClientManager { PangeaEventTypes.userSetLemmaInfo, EventTypes.RoomJoinRules, PangeaEventTypes.activityPlan, + PangeaEventTypes.constructSummary, // Pangea# }, logLevel: kReleaseMode ? Level.warning : Level.verbose, diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index d762a4dcc..fee1b3c62 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -1,13 +1,36 @@ 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 { - List filterByVisibleInGui({String? exceptionEventId}) => where( - (event) => event.isVisibleInGui || event.eventId == exceptionEventId, - ).toList(); + 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; + } } extension IsStateExtension on Event { @@ -23,12 +46,7 @@ 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 - // #Pangea - // (!AppConfig.hideUnknownEvents || isEventTypeKnown) && - (!AppConfig.hideUnknownEvents || - isEventTypeKnown || - importantStateEvents.contains(type)) && - // Pangea# + (!AppConfig.hideUnknownEvents || isEventTypeKnown) && // remove state events that we don't want to render (isState || !AppConfig.hideAllStateEvents) && // #Pangea @@ -64,8 +82,6 @@ extension IsStateExtension on Event { EventTypes.RoomMember, EventTypes.RoomTombstone, EventTypes.CallInvite, - PangeaEventTypes.activityPlan, - PangeaEventTypes.activityPlanEnd, }; // Pangea# } diff --git a/lib/widgets/navigation_rail.dart b/lib/widgets/navigation_rail.dart index ab0983345..ac14abdb9 100644 --- a/lib/widgets/navigation_rail.dart +++ b/lib/widgets/navigation_rail.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/navi_rail_item.dart'; +import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; @@ -175,7 +176,20 @@ class SpacesNavigationRail extends StatelessWidget { return NaviRailItem( toolTip: displayname, isSelected: activeSpaceId == space.id, - onTap: () => onGoToSpaceId(rootSpaces[i].id), + // #Pangea + // onTap: () => onGoToSpaceId(rootSpaces[i].id), + onTap: () { + final room = client.getRoomById(rootSpaces[i].id); + if (room != null) { + chatListHandleSpaceTap( + context, + room, + ); + } else { + onGoToSpaceId(rootSpaces[i].id); + } + }, + // Pangea# unreadBadgeFilter: (room) => spaceChildrenIds.contains(room.id), icon: Avatar( diff --git a/pubspec.lock b/pubspec.lock index 101510960..17a90ab35 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -38,6 +38,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.3.0" + animated_flip_counter: + dependency: "direct main" + description: + name: animated_flip_counter + sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1" + url: "https://pub.dev" + source: hosted + version: "0.3.4" animations: dependency: "direct main" description: @@ -334,6 +342,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + confetti: + dependency: "direct main" + description: + name: confetti + sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751" + url: "https://pub.dev" + source: hosted + version: "0.8.0" console: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6045a6386..a17bd1ee8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ description: Learn a language while texting your friends. # Pangea# publish_to: none # On version bump also increase the build number for F-Droid -version: 4.1.12+2 +version: 4.1.12+1 environment: sdk: ">=3.0.0 <4.0.0" @@ -21,6 +21,7 @@ dependencies: chewie: ^1.11.3 collection: ^1.18.0 cross_file: ^0.3.4+2 + confetti: ^0.8.0 cupertino_icons: any # #Pangea # desktop_drop: ^0.4.4 @@ -135,6 +136,7 @@ dependencies: text_to_speech: git: https://github.com/pangeachat/text_to_speech.git flutter_tts: ^4.2.0 + animated_flip_counter: ^0.3.4 # Pangea# dev_dependencies: diff --git a/web/index.html b/web/index.html index 1a3730e5e..cdb551c4d 100644 --- a/web/index.html +++ b/web/index.html @@ -70,6 +70,33 @@ }); }); + + + + +