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
This commit is contained in:
ggurdin 2025-09-25 08:46:46 -04:00 committed by GitHub
parent d88d1303c6
commit 89bb560347
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 797 additions and 781 deletions

View file

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

View file

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

View file

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

View file

@ -104,14 +104,13 @@ class RoomCreationStateEventState extends State<RoomCreationStateEvent> {
),
// #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)

View file

@ -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<ChatList>
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<ChatContextAction>(
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<ChatContextAction>(
// 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<bool?>(
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#
}

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ class ActivityStatsButton extends StatefulWidget {
class _ActivityStatsButtonState extends State<ActivityStatsButton> {
StreamSubscription? _analyticsSubscription;
ActivitySummaryAnalyticsModel analytics = ActivitySummaryAnalyticsModel();
ActivitySummaryAnalyticsModel? analytics;
@override
void initState() {
@ -47,7 +47,8 @@ class _ActivityStatsButtonState extends State<ActivityStatsButton> {
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<ActivityStatsButton> {
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<ActivityStatsButton> {
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<ActivityStatsButton> {
!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",
),
],
),
),
);
}

View file

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

View file

@ -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<OneConstructUse> uses) {
final user = constructs[userId] ??= UserConstructAnalytics(userId);
for (final use in uses) {
user.addUsage(use.identifier);
}
constructs[userId] = user;
}
Map<String, List> generateSuperlatives() {

View file

@ -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<ChatContextAction>(
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<bool?>(
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;
}
}

View file

@ -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<CourseChats>
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<ChatContextAction>(
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<bool?>(
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,

View file

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

View file

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

View file

@ -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<InstructionsInlineTooltip>
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<InlineTooltip>
with TickerProviderStateMixin {
bool _isToggledOff = true;
AnimationController? _controller;
Animation<double>? _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<void> setToggled() async {
_isToggledOff = widget.instructionsEnum.isToggledOff;
Future<void> _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<InstructionsInlineTooltip>
);
// Start in correct state
if (!_isToggledOff) {
if (!_isClosed) {
await _controller!.forward();
}
}
@ -70,37 +101,18 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
if (mounted) setState(() {});
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> _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<InstructionsInlineTooltip>
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<InstructionsInlineTooltip>
),
),
);
return widget.animate
? SizeTransition(
sizeFactor: _animation!,
axisAlignment: -1.0,
child: content,
)
: (_isClosed ? const SizedBox.shrink() : content);
}
}

View file

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

View file

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