From 89bb560347d78a0500e0b26f7ddf20d8a2585bd8 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:46:46 -0400 Subject: [PATCH] 4110 playtest 92325 (#4121) * style activity role tooltip like instruction inline tooltips * style updates to activity details * don't show token underlines in practice mode * show loading activity analytics * use all construct types to calculate activity analytics, include audio messages in activity summary request * update chat context menus for activities * fix positioning on menu in main chat list --- lib/l10n/intl_en.arb | 3 +- lib/pages/chat/events/html_message.dart | 6 + lib/pages/chat/events/message_content.dart | 6 +- .../events/room_creation_state_event.dart | 5 +- lib/pages/chat_list/chat_list.dart | 609 ++++++++---------- .../activity_room_extension.dart | 82 ++- .../activity_session_analytics_repo.dart | 13 +- .../activity_role_tooltip.dart | 38 +- .../activity_stats_button.dart | 43 +- .../activity_summary_widget.dart | 66 +- .../activity_summary_analytics_model.dart | 12 +- .../widgets/chat_context_menu_action.dart | 301 +++++++++ .../course_chats/course_chats_page.dart | 270 -------- .../course_chats/course_chats_view.dart | 7 +- .../room_information_extension.dart | 5 + .../instructions_inline_tooltip.dart | 106 +-- .../toolbar/utils/token_rendering_util.dart | 2 + lib/pangea/user/models/user_model.dart | 4 +- 18 files changed, 797 insertions(+), 781 deletions(-) create mode 100644 lib/pangea/chat_settings/widgets/chat_context_menu_action.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ee2a10bb3..e9f1fdb79 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5256,5 +5256,6 @@ "activityStatsButtonInstruction": "Click here to view your activity stats and to close the activity when finished", "readingAnalyticsDesc": "Click practice on each message for reading activities.", "speakingAnalyticsDesc": "Record voice messages for speaking practice.", - "audioAnalyticsDesc": "Click practice on each message for listening activities." + "audioAnalyticsDesc": "Click practice on each message for listening activities.", + "endActivity": "End activity" } diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index c192e57f2..1e273b3d5 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -460,6 +460,8 @@ class HtmlMessage extends StatelessWidget { selected, highlighted, isNew, + readingAssistanceMode == + ReadingAssistanceMode.practiceMode, ), ), width: tokenWidth, @@ -486,6 +488,8 @@ class HtmlMessage extends StatelessWidget { selected, highlighted, isNew, + readingAssistanceMode == + ReadingAssistanceMode.practiceMode, ), ), linkStyle: linkStyle, @@ -627,6 +631,7 @@ class HtmlMessage extends StatelessWidget { false, false, false, + false, ), ), ), @@ -644,6 +649,7 @@ class HtmlMessage extends StatelessWidget { false, false, false, + false, ), ), // Pangea# diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index f00896646..213ff19f8 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -360,7 +360,11 @@ class MessageContent extends StatelessWidget { pangeaMessageEvent: pangeaMessageEvent, nextEvent: nextEvent, prevEvent: prevEvent, - onClick: event.isActivityMessage ? null : onClick, + onClick: event.isActivityMessage || + readingAssistanceMode == + ReadingAssistanceMode.practiceMode + ? null + : onClick, isTransitionAnimation: isTransitionAnimation, readingAssistanceMode: readingAssistanceMode, // Pangea# diff --git a/lib/pages/chat/events/room_creation_state_event.dart b/lib/pages/chat/events/room_creation_state_event.dart index 8bfdb7324..8c4c3519e 100644 --- a/lib/pages/chat/events/room_creation_state_event.dart +++ b/lib/pages/chat/events/room_creation_state_event.dart @@ -104,14 +104,13 @@ class RoomCreationStateEventState extends State { ), // #Pangea const SizedBox(height: 16.0), - InstructionsInlineTooltip( + const InstructionsInlineTooltip( instructionsEnum: InstructionsEnum.clickMessage, - padding: const EdgeInsets.only( + padding: EdgeInsets.only( left: 16.0, right: 16.0, top: 16.0, ), - onClose: () => setState(() {}), animate: false, ), if (_members <= 1 && InstructionsEnum.clickMessage.isToggledOff) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index ad528285a..599e1b6f2 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -18,8 +18,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart'; import 'package:fluffychat/pangea/chat_list/utils/chat_list_handle_space_tap.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart'; -import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; +import 'package:fluffychat/pangea/chat_settings/widgets/chat_context_menu_action.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/subscription/widgets/subscription_snackbar.dart'; @@ -32,7 +31,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart' import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; import '../../../utils/account_bundles.dart'; @@ -43,6 +41,7 @@ import '../../widgets/matrix.dart'; import 'package:fluffychat/utils/tor_stub.dart' if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; + enum PopupMenuAction { settings, invite, @@ -700,351 +699,290 @@ class ChatListController extends State super.dispose(); } + // #Pangea void chatContextAction( Room room, BuildContext posContext, [ Room? space, - ]) async { - final overlay = - Overlay.of(posContext).context.findRenderObject() as RenderBox; + ]) => + chatContextMenuAction( + room, + posContext, + () => onChatTap(room), + space, + ); + // void chatContextAction( + // Room room, + // BuildContext posContext, [ + // Room? space, + // ]) async { + // final overlay = + // Overlay.of(posContext).context.findRenderObject() as RenderBox; - final button = posContext.findRenderObject() as RenderBox; + // final button = posContext.findRenderObject() as RenderBox; - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(const Offset(0, -65), ancestor: overlay), - button.localToGlobal( - button.size.bottomRight(Offset.zero) + const Offset(-50, 0), - ancestor: overlay, - ), - ), - Offset.zero & overlay.size, - ); + // final position = RelativeRect.fromRect( + // Rect.fromPoints( + // button.localToGlobal(const Offset(0, -65), ancestor: overlay), + // button.localToGlobal( + // button.size.bottomRight(Offset.zero) + const Offset(-50, 0), + // ancestor: overlay, + // ), + // ), + // Offset.zero & overlay.size, + // ); - final displayname = - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); + // final displayname = + // room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); - // #Pangea - // final spacesWithPowerLevels = room.client.rooms - // .where( - // (space) => - // space.isSpace && - // space.canChangeStateEvent(EventTypes.SpaceChild) && - // !space.spaceChildren.any((c) => c.roomId == room.id), - // ) - // .toList(); - // Pangea# + // final spacesWithPowerLevels = room.client.rooms + // .where( + // (space) => + // space.isSpace && + // space.canChangeStateEvent(EventTypes.SpaceChild) && + // !space.spaceChildren.any((c) => c.roomId == room.id), + // ) + // .toList(); - final action = await showMenu( - context: posContext, - position: position, - items: [ - PopupMenuItem( - value: ChatContextAction.open, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 12.0, - children: [ - Avatar( - mxContent: room.avatar, - name: displayname, - // #Pangea - userId: room.directChatMatrixID, - // Pangea# - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 128), - child: Text( - displayname, - style: - TextStyle(color: Theme.of(context).colorScheme.onSurface), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - const PopupMenuDivider(), - if (space != null) - PopupMenuItem( - value: ChatContextAction.goToSpace, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Avatar( - mxContent: space.avatar, - size: Avatar.defaultSize / 2, - name: space.getLocalizedDisplayname(), - // #Pangea - userId: space.directChatMatrixID, - // Pangea# - ), - const SizedBox(width: 12), - Expanded( - child: Text( - // Pangea# - // L10n.of(context).goToSpace(space.getLocalizedDisplayname()), - L10n.of(context) - .goToCourse(space.getLocalizedDisplayname()), - // Pangea# - ), - ), - ], - ), - ), - if (room.membership == Membership.join) ...[ - PopupMenuItem( - value: ChatContextAction.mute, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - // #Pangea - // room.pushRuleState == PushRuleState.notify - // ? Icons.notifications_off_outlined - // : Icons.notifications_off, - room.pushRuleState == PushRuleState.notify - ? Icons.notifications_on_outlined - : Icons.notifications_off_outlined, - // Pangea# - ), - const SizedBox(width: 12), - Text( - // #Pangea - // room.pushRuleState == PushRuleState.notify - // ? L10n.of(context).muteChat - // : L10n.of(context).unmuteChat, - room.pushRuleState == PushRuleState.notify - ? L10n.of(context).notificationsOn - : L10n.of(context).notificationsOff, - // Pangea# - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.markUnread, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.markedUnread - ? Icons.mark_as_unread - : Icons.mark_as_unread_outlined, - ), - const SizedBox(width: 12), - Text( - room.markedUnread - ? L10n.of(context).markAsRead - : L10n.of(context).markAsUnread, - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.favorite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, - ), - const SizedBox(width: 12), - Text( - room.isFavourite - ? L10n.of(context).unpin - : L10n.of(context).pin, - ), - ], - ), - ), - // #Pangea - // if (spacesWithPowerLevels.isNotEmpty) - // PopupMenuItem( - // value: ChatContextAction.addToSpace, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.group_work_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context).addToSpace), - // ], - // ), - // ), - // Pangea# - ], - PopupMenuItem( - value: ChatContextAction.leave, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - // #Pangea - // Icons.delete_outlined, - Icons.logout_outlined, - // Pangea# - color: Theme.of(context).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Text( - room.membership == Membership.invite - ? L10n.of(context).delete - : L10n.of(context).leave, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ), - // #Pangea - if (room.isRoomAdmin && !room.isDirectChat) - PopupMenuItem( - value: ChatContextAction.delete, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete_outlined, - color: Theme.of(context).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Text( - L10n.of(context).delete, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ), - // Pangea# - ], - ); + // final action = await showMenu( + // context: posContext, + // position: position, + // items: [ + // PopupMenuItem( + // value: ChatContextAction.open, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // spacing: 12.0, + // children: [ + // Avatar( + // mxContent: room.avatar, + // name: displayname, + // ), + // ConstrainedBox( + // constraints: const BoxConstraints(maxWidth: 128), + // child: Text( + // displayname, + // style: + // TextStyle(color: Theme.of(context).colorScheme.onSurface), + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ], + // ), + // ), + // const PopupMenuDivider(), + // if (space != null) + // PopupMenuItem( + // value: ChatContextAction.goToSpace, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Avatar( + // mxContent: space.avatar, + // size: Avatar.defaultSize / 2, + // name: space.getLocalizedDisplayname(), + // ), + // const SizedBox(width: 12), + // Expanded( + // child: Text( + // L10n.of(context).goToSpace(space.getLocalizedDisplayname()), + // ), + // ), + // ], + // ), + // ), + // if (room.membership == Membership.join) ...[ + // PopupMenuItem( + // value: ChatContextAction.mute, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // room.pushRuleState == PushRuleState.notify + // ? Icons.notifications_off_outlined + // : Icons.notifications_off, + // ), + // const SizedBox(width: 12), + // Text( + // room.pushRuleState == PushRuleState.notify + // ? L10n.of(context).muteChat + // : L10n.of(context).unmuteChat, + // ), + // ], + // ), + // ), + // PopupMenuItem( + // value: ChatContextAction.markUnread, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // room.markedUnread + // ? Icons.mark_as_unread + // : Icons.mark_as_unread_outlined, + // ), + // const SizedBox(width: 12), + // Text( + // room.markedUnread + // ? L10n.of(context).markAsRead + // : L10n.of(context).markAsUnread, + // ), + // ], + // ), + // ), + // PopupMenuItem( + // value: ChatContextAction.favorite, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, + // ), + // const SizedBox(width: 12), + // Text( + // room.isFavourite + // ? L10n.of(context).unpin + // : L10n.of(context).pin, + // ), + // ], + // ), + // ), + // if (spacesWithPowerLevels.isNotEmpty) + // PopupMenuItem( + // value: ChatContextAction.addToSpace, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const Icon(Icons.group_work_outlined), + // const SizedBox(width: 12), + // Text(L10n.of(context).addToSpace), + // ], + // ), + // ), + // ], + // PopupMenuItem( + // value: ChatContextAction.leave, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // Icons.delete_outlined, + // color: Theme.of(context).colorScheme.onErrorContainer, + // ), + // const SizedBox(width: 12), + // Text( + // room.membership == Membership.invite + // ? L10n.of(context).delete + // : L10n.of(context).leave, + // style: TextStyle( + // color: Theme.of(context).colorScheme.onErrorContainer, + // ), + // ), + // ], + // ), + // ), + // if (room.membership == Membership.invite) + // PopupMenuItem( + // value: ChatContextAction.block, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon( + // Icons.block_outlined, + // color: Theme.of(context).colorScheme.onErrorContainer, + // ), + // const SizedBox(width: 12), + // Text( + // L10n.of(context).block, + // style: TextStyle( + // color: Theme.of(context).colorScheme.onErrorContainer, + // ), + // ), + // ], + // ), + // ), + // ], + // ); - if (action == null) return; - if (!mounted) return; + // if (action == null) return; + // if (!mounted) return; - switch (action) { - case ChatContextAction.open: - onChatTap(room); - return; - case ChatContextAction.goToSpace: - setActiveSpace(space!.id); - return; - case ChatContextAction.favorite: - await showFutureLoadingDialog( - context: context, - future: () => room.setFavourite(!room.isFavourite), - ); - return; - case ChatContextAction.markUnread: - await showFutureLoadingDialog( - context: context, - future: () => room.markUnread(!room.markedUnread), - ); - return; - case ChatContextAction.mute: - await showFutureLoadingDialog( - context: context, - future: () => room.setPushRuleState( - room.pushRuleState == PushRuleState.notify - ? PushRuleState.mentionsOnly - : PushRuleState.notify, - ), - ); - return; - case ChatContextAction.leave: - final confirmed = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - // #Pangea - // message: L10n.of(context).archiveRoomDescription, - message: room.isSpace - ? L10n.of(context).leaveSpaceDescription - : L10n.of(context).leaveRoomDescription, - // Pangea# - okLabel: L10n.of(context).leave, - cancelLabel: L10n.of(context).cancel, - isDestructive: true, - ); - // #Pangea - // if (confirmed == OkCancelResult.cancel) return; - if (confirmed != OkCancelResult.ok) return; - // Pangea# - if (!mounted) return; + // switch (action) { + // case ChatContextAction.open: + // onChatTap(room); + // return; + // case ChatContextAction.goToSpace: + // setActiveSpace(space!.id); + // return; + // case ChatContextAction.favorite: + // await showFutureLoadingDialog( + // context: context, + // future: () => room.setFavourite(!room.isFavourite), + // ); + // return; + // case ChatContextAction.markUnread: + // await showFutureLoadingDialog( + // context: context, + // future: () => room.markUnread(!room.markedUnread), + // ); + // return; + // case ChatContextAction.mute: + // await showFutureLoadingDialog( + // context: context, + // future: () => room.setPushRuleState( + // room.pushRuleState == PushRuleState.notify + // ? PushRuleState.mentionsOnly + // : PushRuleState.notify, + // ), + // ); + // return; + // case ChatContextAction.block: + // final inviteEvent = room.getState( + // EventTypes.RoomMember, + // room.client.userID!, + // ); + // context.go( + // '/rooms/settings/security/ignorelist', + // extra: inviteEvent?.senderId, + // ); + // case ChatContextAction.leave: + // final confirmed = await showOkCancelAlertDialog( + // context: context, + // title: L10n.of(context).areYouSure, + // message: L10n.of(context).archiveRoomDescription, + // okLabel: L10n.of(context).leave, + // cancelLabel: L10n.of(context).cancel, + // isDestructive: true, + // ); + // if (confirmed == OkCancelResult.cancel) return; + // if (!mounted) return; - // #Pangea - // await showFutureLoadingDialog(context: context, future: room.leave); - final resp = await showFutureLoadingDialog( - context: context, - future: room.isSpace ? room.leaveSpace : room.leave, - ); - if (mounted && !resp.isError) { - context.go("/rooms"); - } - // Pangea# + // await showFutureLoadingDialog(context: context, future: room.leave); - return; - // #Pangea - // case ChatContextAction.addToSpace: - // final space = await showModalActionPopup( - // context: context, - // title: L10n.of(context).space, - // actions: spacesWithPowerLevels - // .map( - // (space) => AdaptiveModalAction( - // value: space, - // label: space - // .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), - // ), - // ) - // .toList(), - // ); - // if (space == null) return; - // await showFutureLoadingDialog( - // context: context, - // future: () => space.setSpaceChild(room.id), - // ); - // Pangea# - case ChatContextAction.delete: - if (room.isSpace) { - final resp = await showDialog( - context: context, - builder: (_) => DeleteSpaceDialog(space: room), - ); - if (resp == true && mounted) { - context.go("/rooms"); - } - } else { - final confirmed = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).delete, - cancelLabel: L10n.of(context).cancel, - isDestructive: true, - message: room.isSpace - ? L10n.of(context).deleteSpaceDesc - : L10n.of(context).deleteChatDesc, - ); - if (confirmed != OkCancelResult.ok) return; - if (!mounted) return; - - final resp = await showFutureLoadingDialog( - context: context, - future: room.delete, - ); - if (mounted && !resp.isError) { - activeSpaceId != null - ? context.go('/rooms/spaces/$activeSpaceId/details') - : context.go("/rooms"); - } - } - return; - // Pangea# - } - } + // return; + // case ChatContextAction.addToSpace: + // final space = await showModalActionPopup( + // context: context, + // title: L10n.of(context).space, + // actions: spacesWithPowerLevels + // .map( + // (space) => AdaptiveModalAction( + // value: space, + // label: space + // .getLocalizedDisplayname(MatrixLocals(L10n.of(context))), + // ), + // ) + // .toList(), + // ); + // if (space == null) return; + // await showFutureLoadingDialog( + // context: context, + // future: () => space.setSpaceChild(room.id), + // ); + // } + // } + // Pangea# void dismissStatusList() async { final result = await showOkCancelAlertDialog( @@ -1302,6 +1240,7 @@ enum ChatContextAction { // #Pangea // addToSpace, delete, + endActivity, // Pangea# } diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index 974cc5b1c..75f6f81a7 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_analytics_r import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; @@ -160,7 +161,10 @@ extension ActivityRoomExtension on Room { final timeline = this.timeline ?? await getTimeline(); for (final event in events) { if (event.type != EventTypes.Message || - event.messageType != MessageTypes.Text) { + ![ + MessageTypes.Text, + MessageTypes.Audio, + ].contains(event.messageType)) { continue; } @@ -170,21 +174,38 @@ extension ActivityRoomExtension on Room { ownMessage: client.userID == event.senderId, ); - final activityMessage = ActivitySummaryResultsMessage( - userId: event.senderId, - sent: pangeaMessage.originalSent?.text ?? event.body, - written: pangeaMessage.originalWrittenContent, - time: event.originServerTs, - tool: [ - if (pangeaMessage.originalSent?.choreo?.includedIT == true) "it", - if (pangeaMessage.originalSent?.choreo?.includedIGC == true) "igc", - ], - ); + if (event.messageType == MessageTypes.Audio && + pangeaMessage.getSpeechToTextLocal() == null) { + continue; + } + + final activityMessage = event.messageType == MessageTypes.Text + ? ActivitySummaryResultsMessage( + userId: event.senderId, + sent: pangeaMessage.originalSent?.text ?? event.body, + written: pangeaMessage.originalWrittenContent, + time: event.originServerTs, + tool: [ + if (pangeaMessage.originalSent?.choreo?.includedIT == true) + "it", + if (pangeaMessage.originalSent?.choreo?.includedIGC == true) + "igc", + ], + ) + : ActivitySummaryResultsMessage( + userId: event.senderId, + sent: + pangeaMessage.getSpeechToTextLocal()!.transcript.text.trim(), + written: + pangeaMessage.getSpeechToTextLocal()!.transcript.text.trim(), + time: event.originServerTs, + tool: [], + ); messages.add(activityMessage); if (activitySummary?.analytics == null) { - analytics.addConstructs(pangeaMessage); + analytics.addMessageConstructs(pangeaMessage); } } @@ -396,28 +417,33 @@ extension ActivityRoomExtension on Room { final cached = ActivitySessionAnalyticsRepo.get(id); final analytics = cached?.analytics ?? ActivitySummaryAnalyticsModel(); - final eventsSince = await getAllEvents(since: cached?.lastEventId); - final timeline = this.timeline ?? await getTimeline(); - final messageEvents = getPangeaMessageEvents( - eventsSince, - timeline, - msgtypes: [ - MessageTypes.Text, - MessageTypes.Audio, - ], - ); + DateTime? timestamp = creationTimestamp; + if (cached != null) { + timestamp = cached.lastUseTimestamp; + } - if (messageEvents.isEmpty) { + final List uses = []; + + for (final use + in MatrixState.pangeaController.getAnalytics.constructListModel.uses) { + final useTimestamp = use.metadata.timeStamp; + if (timestamp != null && + (useTimestamp == timestamp || useTimestamp.isBefore(timestamp))) { + break; + } + + if (use.metadata.roomId != id) continue; + uses.add(use); + } + + if (uses.isEmpty) { return analytics; } - for (final pangeaMessage in messageEvents) { - analytics.addConstructs(pangeaMessage); - } - + analytics.addConstructs(client.userID!, uses); await ActivitySessionAnalyticsRepo.set( id, - messageEvents.last.eventId, + uses.first.metadata.timeStamp, analytics, ); diff --git a/lib/pangea/activity_sessions/activity_session_analytics_repo.dart b/lib/pangea/activity_sessions/activity_session_analytics_repo.dart index fbb61b8df..9de51dbba 100644 --- a/lib/pangea/activity_sessions/activity_session_analytics_repo.dart +++ b/lib/pangea/activity_sessions/activity_session_analytics_repo.dart @@ -4,12 +4,12 @@ import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_mo class CachedActivityAnalytics { final DateTime timestamp; - final String lastEventId; + final DateTime lastUseTimestamp; final ActivitySummaryAnalyticsModel analytics; CachedActivityAnalytics( this.timestamp, - this.lastEventId, + this.lastUseTimestamp, this.analytics, ); } @@ -31,10 +31,11 @@ class ActivitySessionAnalyticsRepo { return null; } - final lastEventId = json['last_event_id'] as String; + final lastUseTimestamp = + DateTime.parse(json['last_use_timestamp'] as String); final analyticsJson = json['analytics'] as Map; final analytics = ActivitySummaryAnalyticsModel.fromJson(analyticsJson); - return CachedActivityAnalytics(timestamp, lastEventId, analytics); + return CachedActivityAnalytics(timestamp, lastUseTimestamp, analytics); } catch (e) { _activityAnalyticsStorage.remove(roomId); return null; @@ -43,12 +44,12 @@ class ActivitySessionAnalyticsRepo { static Future set( String roomId, - String lastEventId, + DateTime lastUseTimestamp, ActivitySummaryAnalyticsModel analytics, ) async { final json = { 'timestamp': DateTime.now().toIso8601String(), - 'last_event_id': lastEventId, + 'last_use_timestamp': lastUseTimestamp.toIso8601String(), 'analytics': analytics.toJson(), }; await _activityAnalyticsStorage.write(roomId, json); diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart index 607da25bb..b6d24f7de 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; class ActivityRoleTooltip extends StatefulWidget { final Choreographer choreographer; @@ -54,34 +54,14 @@ class ActivityRoleTooltipState extends State { ), ), ), - child: AnimatedSize( - duration: FluffyThemes.animationDuration, - child: room.hasDismissedGoalTooltip - ? const SizedBox() - : Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - spacing: 10.0, - children: [ - Expanded( - child: Text( - room.ownRole!.goal!, - style: const TextStyle( - fontSize: 12.0, - ), - textAlign: TextAlign.center, - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - await room.dismissGoalTooltip(); - if (mounted) setState(() {}); - }, - ), - ], - ), - ), + child: InlineTooltip( + message: room.ownRole!.goal!, + isClosed: room.hasDismissedGoalTooltip, + onClose: () async { + await room.dismissGoalTooltip(); + if (mounted) setState(() {}); + }, + padding: const EdgeInsets.all(16.0), ), ); } diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart index f844d386c..2d79a9e5c 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_stats_button.dart @@ -30,7 +30,7 @@ class ActivityStatsButton extends StatefulWidget { class _ActivityStatsButtonState extends State { StreamSubscription? _analyticsSubscription; - ActivitySummaryAnalyticsModel analytics = ActivitySummaryAnalyticsModel(); + ActivitySummaryAnalyticsModel? analytics; @override void initState() { @@ -47,7 +47,8 @@ class _ActivityStatsButtonState extends State { Client get client => widget.controller.room.client; void _showInstructionPopup() { - if (InstructionsEnum.activityStatsMenu.isToggledOff || xpCount <= 0) { + if (InstructionsEnum.activityStatsMenu.isToggledOff || + (xpCount ?? 0) <= 0) { return; } @@ -112,16 +113,16 @@ class _ActivityStatsButtonState extends State { super.dispose(); } - int get xpCount => analytics.totalXPForUser( + int? get xpCount => analytics?.totalXPForUser( client.userID!, ); - int get vocabCount => analytics.uniqueConstructCountForUser( + int? get vocabCount => analytics?.uniqueConstructCountForUser( client.userID!, ConstructTypeEnum.vocab, ); - int get grammarCount => analytics.uniqueConstructCountForUser( + int? get grammarCount => analytics?.uniqueConstructCountForUser( client.userID!, ConstructTypeEnum.morph, ); @@ -131,7 +132,7 @@ class _ActivityStatsButtonState extends State { final analytics = await widget.controller.room.getActivityAnalytics(); if (mounted) { setState(() => this.analytics = analytics); - if (prevXP == 0 && xpCount > 0) _showInstructionPopup(); + if (prevXP == 0 && (xpCount ?? 0) > 0) _showInstructionPopup(); } } @@ -143,32 +144,34 @@ class _ActivityStatsButtonState extends State { !widget.controller.showActivityDropdown, ), borderRadius: BorderRadius.circular(12), - color: xpCount > 0 + color: (xpCount ?? 0) > 0 ? AppConfig.gold.withAlpha(180) : theme.colorScheme.surface, - depressed: xpCount <= 0 || widget.controller.showActivityDropdown, + depressed: (xpCount ?? 0) <= 0 || widget.controller.showActivityDropdown, child: AnimatedContainer( duration: FluffyThemes.animationDuration, width: 300, height: 55, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - color: xpCount > 0 + color: (xpCount ?? 0) > 0 ? AppConfig.gold.withAlpha(180) : theme.colorScheme.surface, borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _StatsBadge(icon: Icons.radar, value: "$xpCount XP"), - _StatsBadge(icon: Symbols.dictionary, value: "$vocabCount"), - _StatsBadge( - icon: Symbols.toys_and_games, - value: "$grammarCount", - ), - ], - ), + child: analytics == null + ? const CircularProgressIndicator.adaptive() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _StatsBadge(icon: Icons.radar, value: "$xpCount XP"), + _StatsBadge(icon: Symbols.dictionary, value: "$vocabCount"), + _StatsBadge( + icon: Symbols.toys_and_games, + value: "$grammarCount", + ), + ], + ), ), ); } diff --git a/lib/pangea/activity_sessions/activity_summary_widget.dart b/lib/pangea/activity_sessions/activity_summary_widget.dart index ca0786b48..c351fa897 100644 --- a/lib/pangea/activity_sessions/activity_summary_widget.dart +++ b/lib/pangea/activity_sessions/activity_summary_widget.dart @@ -89,42 +89,32 @@ class ActivitySummary extends StatelessWidget { spacing: 4.0, mainAxisSize: MainAxisSize.min, children: [ - InlineEllipsisText( - text: activity.description, - maxLines: showInstructions ? null : 2, - trailingWidth: 50.0, - style: DefaultTextStyle.of(context) - .style - .copyWith(fontSize: 12.0), - trailing: WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + Text( + activity.description, + style: theme.textTheme.bodyMedium, + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + ), + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context).details, + style: theme.textTheme.bodyMedium, ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - ), - child: TextButton( - onPressed: toggleInstructions, - style: TextButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - backgroundColor: - Theme.of(context).colorScheme.surface, - ), - child: Text( + InkWell( + onTap: toggleInstructions, + child: Icon( showInstructions - ? L10n.of(context).less - : L10n.of(context).moreLabel, - style: TextStyle( - fontSize: 12.0, - color: Theme.of(context).colorScheme.primary, - ), + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: 20.0, ), ), - ), + ], ), ), if (showInstructions) ...[ @@ -134,7 +124,7 @@ class ActivitySummary extends StatelessWidget { children: [ Text( activity.req.mode, - style: const TextStyle(fontSize: 12.0), + style: theme.textTheme.bodyMedium, ), Row( spacing: 4.0, @@ -143,7 +133,7 @@ class ActivitySummary extends StatelessWidget { const Icon(Icons.school, size: 12.0), Text( activity.req.cefrLevel.string, - style: const TextStyle(fontSize: 12.0), + style: theme.textTheme.bodyMedium, ), ], ), @@ -154,7 +144,7 @@ class ActivitySummary extends StatelessWidget { iconSize: 16.0, child: Text( activity.learningObjective, - style: const TextStyle(fontSize: 12.0), + style: theme.textTheme.bodyMedium, ), ), ActivitySessionDetailsRow( @@ -166,7 +156,9 @@ class ActivitySummary extends StatelessWidget { "body": Style( margin: Margins.all(0), padding: HtmlPaddings.all(0), - fontSize: FontSize(12.0), + fontSize: FontSize( + theme.textTheme.bodyMedium!.fontSize!, + ), ), }, ), @@ -194,7 +186,7 @@ class ActivitySummary extends StatelessWidget { ), child: Text( vocab.lemma, - style: const TextStyle(fontSize: 12), + style: theme.textTheme.bodyMedium, ), ), ) diff --git a/lib/pangea/activity_summary/activity_summary_analytics_model.dart b/lib/pangea/activity_summary/activity_summary_analytics_model.dart index 43e62f12a..dc6b80581 100644 --- a/lib/pangea/activity_summary/activity_summary_analytics_model.dart +++ b/lib/pangea/activity_summary/activity_summary_analytics_model.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -43,16 +44,19 @@ class ActivitySummaryAnalyticsModel { return totalXP; } - void addConstructs(PangeaMessageEvent event) { + void addMessageConstructs(PangeaMessageEvent event) { final uses = event.originalSent?.vocabAndMorphUses(); if (uses == null || uses.isEmpty) return; + addConstructs(event.senderId, uses); + } - final user = - constructs[event.senderId] ??= UserConstructAnalytics(event.senderId); - + void addConstructs(String userId, List uses) { + final user = constructs[userId] ??= UserConstructAnalytics(userId); for (final use in uses) { user.addUsage(use.identifier); } + + constructs[userId] = user; } Map generateSuperlatives() { diff --git a/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart b/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart new file mode 100644 index 000000000..e4d1b576a --- /dev/null +++ b/lib/pangea/chat_settings/widgets/chat_context_menu_action.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart'; +import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; + +void chatContextMenuAction( + Room room, + BuildContext context, + VoidCallback onChatTap, [ + Room? space, +]) async { + final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + + final button = context.findRenderObject() as RenderBox; + + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(const Offset(0, -65), ancestor: overlay), + button.localToGlobal( + button.size.bottomRight(Offset.zero) + const Offset(-50, 0), + ancestor: overlay, + ), + ), + Offset.zero & overlay.size, + ); + + final displayname = + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); + + final action = await showMenu( + context: context, + position: position, + items: [ + PopupMenuItem( + value: ChatContextAction.open, + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 12.0, + children: [ + Avatar( + mxContent: room.avatar, + name: displayname, + userId: room.directChatMatrixID, + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 128), + child: Text( + displayname, + style: + TextStyle(color: Theme.of(context).colorScheme.onSurface), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const PopupMenuDivider(), + if (space != null) + PopupMenuItem( + value: ChatContextAction.goToSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + mxContent: space.avatar, + size: Avatar.defaultSize / 2, + name: space.getLocalizedDisplayname(), + userId: space.directChatMatrixID, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + L10n.of(context).goToCourse(space.getLocalizedDisplayname()), + ), + ), + ], + ), + ), + if (room.membership == Membership.join) ...[ + PopupMenuItem( + value: ChatContextAction.mute, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.pushRuleState == PushRuleState.notify + ? Icons.notifications_on_outlined + : Icons.notifications_off_outlined, + ), + const SizedBox(width: 12), + Text( + room.pushRuleState == PushRuleState.notify + ? L10n.of(context).notificationsOn + : L10n.of(context).notificationsOff, + ), + ], + ), + ), + if (!room.isActivitySession) + PopupMenuItem( + value: ChatContextAction.markUnread, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.markedUnread + ? Icons.mark_as_unread + : Icons.mark_as_unread_outlined, + ), + const SizedBox(width: 12), + Text( + room.markedUnread + ? L10n.of(context).markAsRead + : L10n.of(context).markAsUnread, + ), + ], + ), + ), + if (!room.isActivitySession) + PopupMenuItem( + value: ChatContextAction.favorite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, + ), + const SizedBox(width: 12), + Text( + room.isFavourite + ? L10n.of(context).unpin + : L10n.of(context).pin, + ), + ], + ), + ), + ], + if (room.isActiveInActivity) + PopupMenuItem( + value: ChatContextAction.endActivity, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.stop_circle_outlined, + ), + const SizedBox(width: 12), + Text( + L10n.of(context).endActivity, + ), + ], + ), + ), + if (!room.isActivitySession || room.ownRole == null) + PopupMenuItem( + value: ChatContextAction.leave, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.logout_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 12), + Text( + room.membership == Membership.invite + ? L10n.of(context).delete + : L10n.of(context).leave, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ], + ), + ), + if (room.isRoomAdmin && !room.isDirectChat) + PopupMenuItem( + value: ChatContextAction.delete, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.delete_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + const SizedBox(width: 12), + Text( + L10n.of(context).delete, + style: TextStyle( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ], + ), + ), + ], + ); + + if (action == null) return; + + switch (action) { + case ChatContextAction.open: + onChatTap.call(); + return; + case ChatContextAction.goToSpace: + context.go("/rooms/spaces/${space!.id}/details"); + return; + case ChatContextAction.favorite: + await showFutureLoadingDialog( + context: context, + future: () => room.setFavourite(!room.isFavourite), + ); + return; + case ChatContextAction.markUnread: + await showFutureLoadingDialog( + context: context, + future: () => room.markUnread(!room.markedUnread), + ); + return; + case ChatContextAction.mute: + await showFutureLoadingDialog( + context: context, + future: () => room.setPushRuleState( + room.pushRuleState == PushRuleState.notify + ? PushRuleState.mentionsOnly + : PushRuleState.notify, + ), + ); + return; + case ChatContextAction.leave: + final confirmed = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSure, + message: room.isSpace + ? L10n.of(context).leaveSpaceDescription + : L10n.of(context).leaveRoomDescription, + okLabel: L10n.of(context).leave, + cancelLabel: L10n.of(context).cancel, + isDestructive: true, + ); + if (confirmed != OkCancelResult.ok) return; + + final resp = await showFutureLoadingDialog( + context: context, + future: room.isSpace ? room.leaveSpace : room.leave, + ); + if (!resp.isError) { + context.go("/rooms/spaces/${room.id}/details"); + } + + return; + case ChatContextAction.delete: + if (room.isSpace) { + final resp = await showDialog( + context: context, + builder: (_) => DeleteSpaceDialog(space: room), + ); + if (resp == true) { + context.go("/rooms"); + } + } else { + final confirmed = await showOkCancelAlertDialog( + context: context, + title: L10n.of(context).areYouSure, + okLabel: L10n.of(context).delete, + cancelLabel: L10n.of(context).cancel, + isDestructive: true, + message: room.isSpace + ? L10n.of(context).deleteSpaceDesc + : L10n.of(context).deleteChatDesc, + ); + if (confirmed != OkCancelResult.ok) return; + final resp = await showFutureLoadingDialog( + context: context, + future: room.delete, + ); + if (!resp.isError) { + context.go("/rooms/spaces/${room.id}/details"); + } + } + return; + case ChatContextAction.endActivity: + await showFutureLoadingDialog( + context: context, + future: room.finishActivity, + ); + return; + } +} diff --git a/lib/pangea/course_chats/course_chats_page.dart b/lib/pangea/course_chats/course_chats_page.dart index ffae51b03..f89792e5a 100644 --- a/lib/pangea/course_chats/course_chats_page.dart +++ b/lib/pangea/course_chats/course_chats_page.dart @@ -12,8 +12,6 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart'; -import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_view.dart'; import 'package:fluffychat/pangea/course_chats/extended_space_rooms_chunk.dart'; @@ -25,8 +23,6 @@ import 'package:fluffychat/pangea/spaces/constants/space_constants.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart'; -import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; -import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -472,272 +468,6 @@ class CourseChatsController extends State context.go("/rooms/spaces/${widget.roomId}/$roomId"); } - void chatContextAction( - Room room, - BuildContext posContext, [ - Room? space, - ]) async { - final overlay = - Overlay.of(posContext).context.findRenderObject() as RenderBox; - - final button = posContext.findRenderObject() as RenderBox; - - final position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(const Offset(0, -65), ancestor: overlay), - button.localToGlobal( - button.size.bottomRight(Offset.zero) + const Offset(-50, 0), - ancestor: overlay, - ), - ), - Offset.zero & overlay.size, - ); - - final displayname = - room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); - - final action = await showMenu( - context: posContext, - position: position, - items: [ - PopupMenuItem( - value: ChatContextAction.open, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 12.0, - children: [ - Avatar( - mxContent: room.avatar, - name: displayname, - userId: room.directChatMatrixID, - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 128), - child: Text( - displayname, - style: - TextStyle(color: Theme.of(context).colorScheme.onSurface), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - const PopupMenuDivider(), - if (space != null) - PopupMenuItem( - value: ChatContextAction.goToSpace, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Avatar( - mxContent: space.avatar, - size: Avatar.defaultSize / 2, - name: space.getLocalizedDisplayname(), - userId: space.directChatMatrixID, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - L10n.of(context) - .goToCourse(space.getLocalizedDisplayname()), - ), - ), - ], - ), - ), - if (room.membership == Membership.join) ...[ - PopupMenuItem( - value: ChatContextAction.mute, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.pushRuleState == PushRuleState.notify - ? Icons.notifications_on_outlined - : Icons.notifications_off_outlined, - ), - const SizedBox(width: 12), - Text( - room.pushRuleState == PushRuleState.notify - ? L10n.of(context).notificationsOn - : L10n.of(context).notificationsOff, - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.markUnread, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.markedUnread - ? Icons.mark_as_unread - : Icons.mark_as_unread_outlined, - ), - const SizedBox(width: 12), - Text( - room.markedUnread - ? L10n.of(context).markAsRead - : L10n.of(context).markAsUnread, - ), - ], - ), - ), - PopupMenuItem( - value: ChatContextAction.favorite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, - ), - const SizedBox(width: 12), - Text( - room.isFavourite - ? L10n.of(context).unpin - : L10n.of(context).pin, - ), - ], - ), - ), - ], - PopupMenuItem( - value: ChatContextAction.leave, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.logout_outlined, - color: Theme.of(context).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Text( - room.membership == Membership.invite - ? L10n.of(context).delete - : L10n.of(context).leave, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ), - if (room.isRoomAdmin && !room.isDirectChat) - PopupMenuItem( - value: ChatContextAction.delete, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete_outlined, - color: Theme.of(context).colorScheme.onErrorContainer, - ), - const SizedBox(width: 12), - Text( - L10n.of(context).delete, - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ], - ), - ), - ], - ); - - if (action == null) return; - if (!mounted) return; - - switch (action) { - case ChatContextAction.open: - onChatTap(room); - return; - case ChatContextAction.goToSpace: - context.go("/rooms/spaces/${space!.id}/details"); - return; - case ChatContextAction.favorite: - await showFutureLoadingDialog( - context: context, - future: () => room.setFavourite(!room.isFavourite), - ); - return; - case ChatContextAction.markUnread: - await showFutureLoadingDialog( - context: context, - future: () => room.markUnread(!room.markedUnread), - ); - return; - case ChatContextAction.mute: - await showFutureLoadingDialog( - context: context, - future: () => room.setPushRuleState( - room.pushRuleState == PushRuleState.notify - ? PushRuleState.mentionsOnly - : PushRuleState.notify, - ), - ); - return; - case ChatContextAction.leave: - final confirmed = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - message: room.isSpace - ? L10n.of(context).leaveSpaceDescription - : L10n.of(context).leaveRoomDescription, - okLabel: L10n.of(context).leave, - cancelLabel: L10n.of(context).cancel, - isDestructive: true, - ); - if (confirmed != OkCancelResult.ok) return; - if (!mounted) return; - - final resp = await showFutureLoadingDialog( - context: context, - future: room.isSpace ? room.leaveSpace : room.leave, - ); - if (mounted && !resp.isError) { - context.go("/rooms/spaces/${widget.roomId}/details"); - } - - return; - case ChatContextAction.delete: - if (room.isSpace) { - final resp = await showDialog( - context: context, - builder: (_) => DeleteSpaceDialog(space: room), - ); - if (resp == true && mounted) { - context.go("/rooms"); - } - } else { - final confirmed = await showOkCancelAlertDialog( - context: context, - title: L10n.of(context).areYouSure, - okLabel: L10n.of(context).delete, - cancelLabel: L10n.of(context).cancel, - isDestructive: true, - message: room.isSpace - ? L10n.of(context).deleteSpaceDesc - : L10n.of(context).deleteChatDesc, - ); - if (confirmed != OkCancelResult.ok) return; - if (!mounted) return; - - final resp = await showFutureLoadingDialog( - context: context, - future: room.delete, - ); - if (mounted && !resp.isError) { - context.go("/rooms/spaces/${widget.roomId}/details"); - } - } - return; - } - } - bool _includeSpaceChild( Room space, SpaceRoomsChunk hierarchyMember, diff --git a/lib/pangea/course_chats/course_chats_view.dart b/lib/pangea/course_chats/course_chats_view.dart index 1104ba49a..3978f206b 100644 --- a/lib/pangea/course_chats/course_chats_view.dart +++ b/lib/pangea/course_chats/course_chats_view.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; +import 'package:fluffychat/pangea/chat_settings/widgets/chat_context_menu_action.dart'; import 'package:fluffychat/pangea/course_chats/activity_template_chat_list_item.dart'; import 'package:fluffychat/pangea/course_chats/course_chats_page.dart'; import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart'; @@ -112,9 +113,10 @@ class CourseChatsView extends StatelessWidget { return ChatListItem( joinedRoom, onTap: () => controller.onChatTap(joinedRoom), - onLongPress: (context) => controller.chatContextAction( + onLongPress: (context) => chatContextMenuAction( joinedRoom, context, + () => controller.onChatTap(joinedRoom), ), activeChat: controller.widget.activeChat == joinedRoom.id, ); @@ -169,9 +171,10 @@ class CourseChatsView extends StatelessWidget { return ChatListItem( joinedRoom, onTap: () => controller.onChatTap(joinedRoom), - onLongPress: (context) => controller.chatContextAction( + onLongPress: (context) => chatContextMenuAction( joinedRoom, context, + () => controller.onChatTap(joinedRoom), ), activeChat: controller.widget.activeChat == joinedRoom.id, borderRadius: BorderRadius.circular( diff --git a/lib/pangea/extensions/room_information_extension.dart b/lib/pangea/extensions/room_information_extension.dart index 204e056a7..d80ace5b8 100644 --- a/lib/pangea/extensions/room_information_extension.dart +++ b/lib/pangea/extensions/room_information_extension.dart @@ -3,6 +3,11 @@ part of "pangea_room_extension.dart"; extension RoomInformationRoomExtension on Room { String? get creatorId => getState(EventTypes.RoomCreate)?.senderId; + DateTime? get creationTimestamp { + final creationEvent = getState(EventTypes.RoomCreate) as Event?; + return creationEvent?.originServerTs; + } + bool isFirstOrSecondChild(String roomId) { return isSpace && (spaceChildren.any((room) => room.roomId == roomId) || diff --git a/lib/pangea/instructions/instructions_inline_tooltip.dart b/lib/pangea/instructions/instructions_inline_tooltip.dart index e8d8aab17..a82aa8ce9 100644 --- a/lib/pangea/instructions/instructions_inline_tooltip.dart +++ b/lib/pangea/instructions/instructions_inline_tooltip.dart @@ -5,52 +5,83 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -class InstructionsInlineTooltip extends StatefulWidget { +class InstructionsInlineTooltip extends StatelessWidget { final InstructionsEnum instructionsEnum; - final bool bold; final bool animate; final EdgeInsets? padding; - final VoidCallback? onClose; const InstructionsInlineTooltip({ super.key, required this.instructionsEnum, - this.bold = false, this.animate = true, this.padding, - this.onClose, }); @override - InstructionsInlineTooltipState createState() => - InstructionsInlineTooltipState(); + Widget build(BuildContext context) { + return InlineTooltip( + message: instructionsEnum.body(L10n.of(context)), + isClosed: instructionsEnum.isToggledOff, + onClose: () => instructionsEnum.setToggledOff(true), + animate: animate, + padding: padding, + ); + } } -class InstructionsInlineTooltipState extends State +class InlineTooltip extends StatefulWidget { + final String message; + final bool isClosed; + + final EdgeInsets? padding; + final VoidCallback? onClose; + final bool animate; + + const InlineTooltip({ + super.key, + required this.message, + required this.isClosed, + this.onClose, + this.animate = true, + this.padding, + }); + + @override + InlineTooltipState createState() => InlineTooltipState(); +} + +class InlineTooltipState extends State with TickerProviderStateMixin { - bool _isToggledOff = true; AnimationController? _controller; Animation? _animation; + bool _isClosed = true; + @override - void didUpdateWidget(covariant InstructionsInlineTooltip oldWidget) { - if (oldWidget.instructionsEnum != widget.instructionsEnum) { - setToggled(); + void initState() { + super.initState(); + _isClosed = widget.isClosed; + _openTooltip(); + } + + @override + void didUpdateWidget(covariant InlineTooltip oldWidget) { + if (oldWidget.message != widget.message) { + _isClosed = widget.isClosed; + _openTooltip(); } super.didUpdateWidget(oldWidget); } @override - void initState() { - super.initState(); - setToggled(); + void dispose() { + _controller?.dispose(); + super.dispose(); } - Future setToggled() async { - _isToggledOff = widget.instructionsEnum.isToggledOff; - + Future _openTooltip() async { if (widget.animate) { - // Initialize AnimationController and Animation only if animate is true + _controller?.dispose(); _controller = AnimationController( duration: FluffyThemes.animationDuration, vsync: this, @@ -62,7 +93,7 @@ class InstructionsInlineTooltipState extends State ); // Start in correct state - if (!_isToggledOff) { + if (!_isClosed) { await _controller!.forward(); } } @@ -70,37 +101,18 @@ class InstructionsInlineTooltipState extends State if (mounted) setState(() {}); } - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - Future _closeTooltip() async { - widget.instructionsEnum.setToggledOff(true); - setState(() => _isToggledOff = true); + widget.onClose?.call(); + setState(() => _isClosed = true); if (widget.animate) { await _controller?.reverse(); } - widget.onClose?.call(); } @override Widget build(BuildContext context) { - return widget.animate - ? SizeTransition( - sizeFactor: _animation!, - axisAlignment: -1.0, - child: _buildTooltipContent(context), - ) - : (_isToggledOff - ? const SizedBox.shrink() - : _buildTooltipContent(context)); - } - - Widget _buildTooltipContent(BuildContext context) { - return Padding( + final content = Padding( padding: widget.padding ?? const EdgeInsets.all(0), child: DecoratedBox( decoration: BoxDecoration( @@ -125,7 +137,7 @@ class InstructionsInlineTooltipState extends State Flexible( child: Center( child: Text( - widget.instructionsEnum.body(L10n.of(context)), + widget.message, style: FluffyThemes.isColumnMode(context) ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.bodyLarge, @@ -147,5 +159,13 @@ class InstructionsInlineTooltipState extends State ), ), ); + + return widget.animate + ? SizeTransition( + sizeFactor: _animation!, + axisAlignment: -1.0, + child: content, + ) + : (_isClosed ? const SizedBox.shrink() : content); } } diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index 20fd7225d..e2785b285 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -91,7 +91,9 @@ class TokenRenderingUtil { bool selected, bool highlighted, bool isNew, + bool practiceMode, ) { + if (practiceMode) return Colors.white.withAlpha(0); if (highlighted) return Theme.of(context).colorScheme.primary; if (isNew) return AppConfig.success; return selected diff --git a/lib/pangea/user/models/user_model.dart b/lib/pangea/user/models/user_model.dart index ec5897359..6ffe2a844 100644 --- a/lib/pangea/user/models/user_model.dart +++ b/lib/pangea/user/models/user_model.dart @@ -1,11 +1,11 @@ +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/instructions/instruction_settings.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/pangea/spaces/models/space_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:matrix/matrix.dart'; - import '../../learning_settings/models/language_model.dart'; /// The user's settings learning settings.