feat: activity sessions
This commit is contained in:
parent
f4bef7f3e4
commit
62140c56da
66 changed files with 3738 additions and 2454 deletions
3
.env
3
.env
|
|
@ -11,4 +11,5 @@ RC_OFFERING_NAME = 'test'
|
|||
|
||||
STRIPE_MANAGEMENT_LINK = 'https://billing.stripe.com/p/login/test_9AQaI8d3O9lmaXe5kk'
|
||||
|
||||
SUPPORT_SPACE_ID = '!gqSNSkvwTpgumyjLsV:staging.pangea.chat'
|
||||
SUPPORT_SPACE_ID = '!gqSNSkvwTpgumyjLsV:staging.pangea.chat'
|
||||
GOOGLE_API_KEY = 'AIzaSyD1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q'
|
||||
|
|
@ -352,6 +352,25 @@ abstract class AppRoutes {
|
|||
: null,
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: ':roomid',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
ChatPage(
|
||||
roomId: state.pathParameters['roomid']!,
|
||||
eventId: state.uri.queryParameters['event'],
|
||||
backButton: BackButton(
|
||||
onPressed: () => context.go(
|
||||
"/rooms/analytics?mode=activities",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4896,7 +4896,6 @@
|
|||
"newChatActivityDesc": "Make every group chat an adventure with Activity Planner! Set captivating topics and objectives for the group, and bring conversations to life with stunning images. Spark imaginative discussions and keep the fun flowing effortlessly!",
|
||||
"exploreMore": "Explore more",
|
||||
"wordFocusListeningMultipleChoice": "Which audio matches the word?",
|
||||
"createActivity": "Create activity",
|
||||
"startChat": "Start a chat",
|
||||
"translationProblem": "Translation problem",
|
||||
"perfectTranslation": "Perfect translation!",
|
||||
|
|
@ -5080,6 +5079,7 @@
|
|||
"reset": "Reset",
|
||||
"errorGenerateActivityMessage": "Failed to generate activity",
|
||||
"errorRegenerateActivityMessage": "Failed to regenerate activity",
|
||||
"errorLaunchActivityMessage": "Failed to launch activity",
|
||||
"errorFetchingActivitiesMessage": "Failed to fetch activities",
|
||||
"errorFetchingDefinition": "Failed to fetch definition",
|
||||
"errorProcessAnalytics": "Failed to process analytics",
|
||||
|
|
@ -5092,5 +5092,46 @@
|
|||
"errorFetchingTranslation": "Failed to fetch translation",
|
||||
"errorFetchingActivity": "Failed to fetch activity",
|
||||
"check": "Check",
|
||||
"unableToFindRoom": "No chat or space found with that code. Please try again."
|
||||
"unableToFindRoom": "No chat or space found with that code. Please try again.",
|
||||
"createActivityPlan": "Create a new activity plan",
|
||||
"saveAndLaunch": "Save and Launch",
|
||||
"launchToSpace": "Launch to Space",
|
||||
"numberOfActivities": "Number of Activity Sessions",
|
||||
"minimumActivityParticipants": "Each Activity needs a minimum {count} of participant(s).",
|
||||
"@minimumActivityParticipants": {
|
||||
"type": "String",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unjoinedActivityMessage": "Do you want to participate? Choose an open role!\nOr hang out and watch the show!",
|
||||
"fullActivityMessage": "Feel free to watch the show! While there aren't any open roles to participate, you can view the chat!",
|
||||
"confirmRole": "Confirm role",
|
||||
"openRoleLabel": "OPEN",
|
||||
"joinedTheActivity": "👋 {username} joined as {role}",
|
||||
"@joinedTheActivity": {
|
||||
"type": "String",
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
},
|
||||
"role": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"finishedTheActivity": "🎯 {username} wrapped up this activity",
|
||||
"@finishedTheActivity": {
|
||||
"type": "String",
|
||||
"placeholders": {
|
||||
"username": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"endActivityTitle": "Wrapped up my part",
|
||||
"endActivityDesc": "Did you complete the objectives?\nThis is your confirmation that you're stepping back from texting. But don’t worry, the fun continues in the chat! Feel free to hang out and enjoy the show until everyone clicks 'Done'.",
|
||||
"archiveToAnalytics": "Archive to Analytics"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5532,7 +5532,6 @@
|
|||
"save": "Guardar",
|
||||
"selectActivity": "Seleccionar actividad",
|
||||
"wordFocusListeningMultipleChoice": "¿Qué audio coincide con la palabra?",
|
||||
"createActivity": "Crear actividad",
|
||||
"startChat": "Iniciar un chat",
|
||||
"translationProblem": "Problema de traducción",
|
||||
"perfectTranslation": "¡Traducción perfecta!",
|
||||
|
|
|
|||
|
|
@ -3832,7 +3832,6 @@
|
|||
"save": "Lưu",
|
||||
"selectActivity": "Chọn hoạt động",
|
||||
"wordFocusListeningMultipleChoice": "Âm thanh nào phù hợp với từ?",
|
||||
"createActivity": "Tạo hoạt động",
|
||||
"startChat": "Bắt đầu trò chuyện",
|
||||
"translationProblem": "Vấn đề dịch thuật",
|
||||
"perfectTranslation": "Dịch thuật hoàn hảo!",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ArchiveController extends State<Archive> {
|
|||
// #Pangea
|
||||
//return archive = await Matrix.of(context).client.loadArchive();
|
||||
return archive = (await Matrix.of(context).client.loadArchive())
|
||||
.where((e) => (!e.isSpace && !e.isAnalyticsRoom))
|
||||
.where((e) => (!e.isSpace && !e.isHiddenRoom))
|
||||
.toList();
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,11 +81,18 @@ class ChatPage extends StatelessWidget {
|
|||
final List<ShareItem>? shareItems;
|
||||
final String? eventId;
|
||||
|
||||
// #Pangea
|
||||
final Widget? backButton;
|
||||
// Pangea#
|
||||
|
||||
const ChatPage({
|
||||
super.key,
|
||||
required this.roomId,
|
||||
this.eventId,
|
||||
this.shareItems,
|
||||
// #Pangea
|
||||
this.backButton,
|
||||
// Pangea#
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -122,6 +129,9 @@ class ChatPage extends StatelessWidget {
|
|||
room: room,
|
||||
shareItems: shareItems,
|
||||
eventId: eventId,
|
||||
// #Pangea
|
||||
backButton: backButton,
|
||||
// Pangea#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,11 +141,18 @@ class ChatPageWithRoom extends StatefulWidget {
|
|||
final List<ShareItem>? shareItems;
|
||||
final String? eventId;
|
||||
|
||||
// #Pangea
|
||||
final Widget? backButton;
|
||||
// Pangea#
|
||||
|
||||
const ChatPageWithRoom({
|
||||
super.key,
|
||||
required this.room,
|
||||
this.shareItems,
|
||||
this.eventId,
|
||||
// #Pangea
|
||||
this.backButton,
|
||||
// Pangea#
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
|||
import 'package:fluffychat/pages/chat/chat_app_bar_title.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_event_list.dart';
|
||||
import 'package:fluffychat/pages/chat/pinned_events.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_pinned_message.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_status_message.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
|
||||
|
|
@ -24,7 +27,9 @@ import 'package:fluffychat/widgets/mxc_image.dart';
|
|||
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
|
||||
import '../../utils/stream_extension.dart';
|
||||
|
||||
enum _EventContextAction { info, report }
|
||||
// #Pangea
|
||||
// enum _EventContextAction { info, report }
|
||||
// Pangea#
|
||||
|
||||
class ChatView extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
|
@ -210,31 +215,34 @@ class ChatView extends StatelessWidget {
|
|||
// : theme.colorScheme.tertiaryContainer,
|
||||
// Pangea#
|
||||
automaticallyImplyLeading: false,
|
||||
leading:
|
||||
leading: controller.selectMode
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: controller.clearSelectedEvents,
|
||||
tooltip: L10n.of(context).close,
|
||||
color: theme.colorScheme.onTertiaryContainer,
|
||||
)
|
||||
// #Pangea
|
||||
// controller.selectMode
|
||||
// ? IconButton(
|
||||
// icon: const Icon(Icons.close),
|
||||
// onPressed: controller.clearSelectedEvents,
|
||||
// tooltip: L10n.of(context).close,
|
||||
// color: theme.colorScheme.onTertiaryContainer,
|
||||
// )
|
||||
// :
|
||||
// Pangea#
|
||||
FluffyThemes.isColumnMode(context)
|
||||
? null
|
||||
: StreamBuilder<Object>(
|
||||
stream:
|
||||
Matrix.of(context).client.onSync.stream.where(
|
||||
: controller.widget.backButton != null
|
||||
? controller.widget.backButton!
|
||||
// Pangea#
|
||||
: FluffyThemes.isColumnMode(context)
|
||||
? null
|
||||
: StreamBuilder<Object>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(syncUpdate) => syncUpdate.hasRoomUpdate,
|
||||
),
|
||||
builder: (context, _) => UnreadRoomsBadge(
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition:
|
||||
BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
),
|
||||
builder: (context, _) => UnreadRoomsBadge(
|
||||
filter: (r) => r.id != controller.roomId,
|
||||
badgePosition:
|
||||
BadgePosition.topEnd(end: 8, top: 4),
|
||||
child: const Center(child: BackButton()),
|
||||
),
|
||||
),
|
||||
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
|
||||
title: ChatAppBarTitle(controller),
|
||||
actions: _appBarActions(context),
|
||||
|
|
@ -413,13 +421,18 @@ class ChatView extends StatelessWidget {
|
|||
),
|
||||
// #Pangea
|
||||
// Keep messages above minimum input bar height
|
||||
if (!controller.room.isAbandonedDMRoom)
|
||||
if (!controller.room.isAbandonedDMRoom &&
|
||||
controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join &&
|
||||
controller.room.hasJoinedActivity &&
|
||||
!controller.room.hasFinishedActivity)
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
height: controller.inputBarHeight,
|
||||
),
|
||||
),
|
||||
ActivityStatusMessage(room: controller.room),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
|
|
@ -427,7 +440,9 @@ class ChatView extends StatelessWidget {
|
|||
ChatViewBackground(controller.choreographer),
|
||||
if (!controller.room.isAbandonedDMRoom &&
|
||||
controller.room.canSendDefaultMessages &&
|
||||
controller.room.membership == Membership.join)
|
||||
controller.room.membership == Membership.join &&
|
||||
controller.room.hasJoinedActivity &&
|
||||
!controller.room.hasFinishedActivity)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
|
@ -447,6 +462,7 @@ class ChatView extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
ActivityPinnedMessage(controller),
|
||||
// Pangea#
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/pangea_message_reactions.dart';
|
||||
import 'package:fluffychat/pages/chat/events/room_creation_state_event.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/activity_role_state_message.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/activity_state_event.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
|
|
@ -125,6 +128,17 @@ class Message extends StatelessWidget {
|
|||
if (event.type == EventTypes.RoomCreate) {
|
||||
return RoomCreationStateEvent(event: event);
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
if (event.type == PangeaEventTypes.activityPlan) {
|
||||
return ActivityStateEvent(event: event);
|
||||
}
|
||||
|
||||
if (event.type == PangeaEventTypes.activityRole) {
|
||||
return ActivityRoleStateMessage(event);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
return StateMessage(event);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
|
|
@ -315,50 +313,9 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
}
|
||||
|
||||
Future<void> addSubspace() async {
|
||||
final names = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).createNewSpace,
|
||||
hintText: L10n.of(context).spaceName,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
maxLength: 64,
|
||||
validator: (text) {
|
||||
if (text.isEmpty) {
|
||||
return L10n.of(context).pleaseChoose;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
okLabel: L10n.of(context).create,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
);
|
||||
if (names == null) return;
|
||||
final client = Matrix.of(context).client;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final activeSpace = client.getRoomById(roomId!)!;
|
||||
await activeSpace.postLoad();
|
||||
|
||||
final resp = await client.createRoom(
|
||||
name: names,
|
||||
visibility: RoomDefaults.spaceChildVisibility,
|
||||
creationContent: {'type': 'm.space'},
|
||||
initialState: [
|
||||
RoomDefaults.defaultSpacePowerLevels(client.userID!),
|
||||
await client.pangeaJoinRules(
|
||||
'knock_restricted',
|
||||
allow: [
|
||||
{
|
||||
"type": "m.room_membership",
|
||||
"room_id": roomId!,
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
await activeSpace.addToSpace(resp);
|
||||
},
|
||||
);
|
||||
final activeSpace = Matrix.of(context).client.getRoomById(roomId!);
|
||||
if (activeSpace == null || !activeSpace.isSpace) return;
|
||||
await activeSpace.addSubspace(context);
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,24 +265,24 @@ class ChatListController extends State<ChatList>
|
|||
case ActiveFilter.allChats:
|
||||
// #Pangea
|
||||
// return (room) => true;
|
||||
return (room) => !room.isAnalyticsRoom && !room.isSpace;
|
||||
return (room) => !room.isHiddenRoom && !room.isSpace;
|
||||
// Pangea#
|
||||
case ActiveFilter.messages:
|
||||
// #Pangea
|
||||
// return (room) => !room.isSpace && room.isDirectChat;
|
||||
return (room) =>
|
||||
!room.isSpace && room.isDirectChat && !room.isAnalyticsRoom;
|
||||
!room.isSpace && room.isDirectChat && !room.isHiddenRoom;
|
||||
// Pangea#
|
||||
case ActiveFilter.groups:
|
||||
// #Pangea
|
||||
// return (room) => !room.isSpace && !room.isDirectChat;
|
||||
return (room) =>
|
||||
!room.isSpace && !room.isDirectChat && !room.isAnalyticsRoom;
|
||||
!room.isSpace && !room.isDirectChat && !room.isHiddenRoom;
|
||||
// Pangea#
|
||||
case ActiveFilter.unread:
|
||||
// #Pangea
|
||||
// return (room) => room.isUnreadOrInvited;
|
||||
return (room) => room.isUnreadOrInvited && !room.isAnalyticsRoom;
|
||||
return (room) => room.isUnreadOrInvited && !room.isHiddenRoom;
|
||||
// Pangea#
|
||||
case ActiveFilter.spaces:
|
||||
return (room) => room.isSpace;
|
||||
|
|
@ -650,7 +650,7 @@ class ChatListController extends State<ChatList>
|
|||
final room = client.getRoomById(roomID);
|
||||
if (room == null ||
|
||||
room.isSpace ||
|
||||
room.isAnalyticsRoom ||
|
||||
room.isHiddenRoom ||
|
||||
room.capacity == null ||
|
||||
(room.summary.mJoinedMemberCount ?? 1) <= room.capacity!) {
|
||||
continue;
|
||||
|
|
@ -1113,12 +1113,10 @@ class ChatListController extends State<ChatList>
|
|||
if (confirmed != OkCancelResult.ok) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (resp.isError) return;
|
||||
if (mounted) context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
return;
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -22,6 +21,7 @@ import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
|||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
|
||||
import 'package:fluffychat/pangea/spaces/widgets/leaderboard_participant_list.dart';
|
||||
import 'package:fluffychat/pangea/spaces/widgets/space_view_appbar.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
|
|
@ -391,6 +391,13 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
if (resp == true) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
break;
|
||||
case SpaceActions.groupChat:
|
||||
context.go("/rooms/newgroup?space=${widget.spaceId}");
|
||||
break;
|
||||
case SpaceActions.subspace:
|
||||
space?.addSubspace(context);
|
||||
break;
|
||||
// Pangea#
|
||||
}
|
||||
}
|
||||
|
|
@ -552,8 +559,10 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final theme = Theme.of(context);
|
||||
|
||||
final room = Matrix.of(context).client.getRoomById(widget.spaceId);
|
||||
final displayname =
|
||||
room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound;
|
||||
// #Pangea
|
||||
// final displayname =
|
||||
// room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound;
|
||||
// Pangea#
|
||||
|
||||
// #Pangea
|
||||
final joinedParents = room?.spaceParents
|
||||
|
|
@ -567,167 +576,92 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// Pangea#
|
||||
|
||||
return Scaffold(
|
||||
// #Pangea
|
||||
// appBar: AppBar(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(
|
||||
kIsWeb ? 72.0 : (kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_onSpaceAction(SpaceActions.settings);
|
||||
},
|
||||
child: AppBar(
|
||||
// leading: FluffyThemes.isColumnMode(context)
|
||||
// ? null
|
||||
// : Center(
|
||||
// child: CloseButton(
|
||||
// onPressed: widget.onBack,
|
||||
// ),
|
||||
// ),
|
||||
leading: joinedParents?.isEmpty ?? true
|
||||
? FluffyThemes.isColumnMode(context)
|
||||
? null
|
||||
: Center(
|
||||
child: CloseButton(
|
||||
onPressed: widget.onBack,
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: joinedParents!.length == 1
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back_outlined),
|
||||
onPressed: () =>
|
||||
widget.toParentSpace(joinedParents.first.id),
|
||||
)
|
||||
: PopupMenuButton(
|
||||
tooltip: null,
|
||||
useRootNavigator: true,
|
||||
icon: const Icon(Icons.arrow_back_outlined),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
...joinedParents.mapIndexed((i, room) {
|
||||
return PopupMenuItem(
|
||||
value: i,
|
||||
child: Text(room.getLocalizedDisplayname()),
|
||||
);
|
||||
}),
|
||||
];
|
||||
},
|
||||
onSelected: (i) {
|
||||
widget.toParentSpace(joinedParents[i].id);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0,
|
||||
title: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
mxContent: room?.avatar,
|
||||
name: displayname,
|
||||
// #Pangea
|
||||
userId: room?.directChatMatrixID,
|
||||
// Pangea#
|
||||
border: BorderSide(width: 1, color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
),
|
||||
title: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: room == null
|
||||
? null
|
||||
: Text(
|
||||
L10n.of(context).countChatsAndCountParticipants(
|
||||
// #Pangea
|
||||
// room.spaceChildren.length,
|
||||
room.spaceChildCount,
|
||||
// Pangea#
|
||||
room.summary.mJoinedMemberCount ?? 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<SpaceActions>(
|
||||
useRootNavigator: true,
|
||||
onSelected: _onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).invite),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// #Pangea
|
||||
// const Icon(Icons.delete_outlined),
|
||||
const Icon(Icons.logout_outlined),
|
||||
// Pangea#
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
// #Pangea
|
||||
if (Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(widget.spaceId)
|
||||
?.isRoomAdmin ??
|
||||
false)
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.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#
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
preferredSize: const Size.fromHeight(60.0),
|
||||
child: SpaceViewAppbar(
|
||||
onSpaceAction: _onSpaceAction,
|
||||
onBack: widget.onBack,
|
||||
room: room,
|
||||
toParentSpace: widget.toParentSpace,
|
||||
joinedParents: joinedParents,
|
||||
),
|
||||
),
|
||||
// appBar: AppBar(
|
||||
// leading: FluffyThemes.isColumnMode(context)
|
||||
// ? null
|
||||
// : Center(
|
||||
// child: CloseButton(
|
||||
// onPressed: widget.onBack,
|
||||
// ),
|
||||
// ),
|
||||
// automaticallyImplyLeading: false,
|
||||
// titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0,
|
||||
// title: ListTile(
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// leading: Avatar(
|
||||
// mxContent: room?.avatar,
|
||||
// name: displayname,
|
||||
// border: BorderSide(width: 1, color: theme.dividerColor),
|
||||
// borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
|
||||
// ),
|
||||
// title: Text(
|
||||
// displayname,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// subtitle: room == null
|
||||
// ? null
|
||||
// : Text(
|
||||
// L10n.of(context).countChatsAndCountParticipants(
|
||||
// room.spaceChildren.length,
|
||||
// room.summary.mJoinedMemberCount ?? 1,
|
||||
// ),
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// ),
|
||||
// actions: [
|
||||
// PopupMenuButton<SpaceActions>(
|
||||
// useRootNavigator: true,
|
||||
// onSelected: _onSpaceAction,
|
||||
// itemBuilder: (context) => [
|
||||
// PopupMenuItem(
|
||||
// value: SpaceActions.settings,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.settings_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context).settings),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SpaceActions.invite,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.person_add_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context).invite),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SpaceActions.leave,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.delete_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context).leave),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
floatingActionButton: room?.canChangeStateEvent(
|
||||
EventTypes.SpaceChild,
|
||||
) ==
|
||||
|
|
@ -736,11 +670,12 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
// #Pangea
|
||||
// onPressed: _addChatOrSubspace,
|
||||
// label: Text(L10n.of(context).group),
|
||||
// icon: const Icon(Icons.group_add_outlined),
|
||||
onPressed: () =>
|
||||
context.go("/rooms/newgroup?space=${widget.spaceId}"),
|
||||
label: Text(L10n.of(context).groupChat),
|
||||
context.go("/rooms/${widget.spaceId}/details/planner"),
|
||||
label: Text(L10n.of(context).activities),
|
||||
icon: const Icon(Icons.event_note_outlined),
|
||||
// Pangea#
|
||||
icon: const Icon(Icons.group_add_outlined),
|
||||
)
|
||||
: null,
|
||||
body: room == null
|
||||
|
|
@ -763,7 +698,7 @@ class _SpaceViewState extends State<SpaceView> {
|
|||
final joinedRooms = room.client.rooms
|
||||
.where((room) => childrenIds.remove(room.id))
|
||||
// #Pangea
|
||||
.where((room) => !room.isAnalyticsRoom)
|
||||
.where((room) => !room.isHiddenRoom)
|
||||
// Pangea#
|
||||
.toList();
|
||||
|
||||
|
|
@ -1025,5 +960,7 @@ enum SpaceActions {
|
|||
leave,
|
||||
// #Pangea
|
||||
delete,
|
||||
groupChat,
|
||||
subspace,
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/pages/chat_permissions_settings/permission_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -49,6 +50,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
controller.defaultPowerLevels,
|
||||
);
|
||||
|
||||
final excludedEvents = [PangeaEventTypes.activityRole];
|
||||
|
||||
Map<String, dynamic> missingPowerLevels = Map<String, dynamic>.from(
|
||||
defaults,
|
||||
)..removeWhere((k, v) => v is! int || powerLevels.containsKey(k));
|
||||
|
|
@ -72,6 +75,9 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
|
||||
powerLevels.addAll(missingPowerLevels);
|
||||
eventsPowerLevels.addAll(missingEventsPowerLevels);
|
||||
eventsPowerLevels.removeWhere(
|
||||
(key, value) => excludedEvents.contains(key),
|
||||
);
|
||||
// Pangea#
|
||||
return Column(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group_view.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_carousel.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
|
|
@ -175,17 +174,6 @@ class NewGroupView extends StatelessWidget {
|
|||
// onChanged: null,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.createGroupType == CreateGroupType.group)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: ActivitySuggestionCarousel(
|
||||
onActivitySelected: controller.setSelectedActivity,
|
||||
enabled: controller.nameController.text.isNotEmpty &&
|
||||
!controller.loading,
|
||||
selectedActivity: controller.selectedActivity,
|
||||
selectedActivityImage: controller.selectedActivityImage,
|
||||
),
|
||||
),
|
||||
// Pangea#
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_en
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityGenerator extends StatefulWidget {
|
||||
final String? roomID;
|
||||
final String roomID;
|
||||
const ActivityGenerator({
|
||||
this.roomID,
|
||||
required this.roomID,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -53,6 +53,7 @@ class ActivityGeneratorState extends State<ActivityGenerator> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
selectedLanguageOfInstructions =
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode;
|
||||
selectedTargetLanguage =
|
||||
|
|
@ -96,9 +97,7 @@ class ActivityGeneratorState extends State<ActivityGenerator> {
|
|||
Future<List<ActivitySettingResponseSchema>> get objectiveItems =>
|
||||
LearningObjectiveListRepo.get(req);
|
||||
|
||||
Room? get room => widget.roomID != null
|
||||
? Matrix.of(context).client.getRoomById(widget.roomID!)
|
||||
: null;
|
||||
Room? get room => Matrix.of(context).client.getRoomById(widget.roomID);
|
||||
|
||||
String? validateNotNull(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ActivityGeneratorView extends StatelessWidget {
|
|||
padding: EdgeInsets.all(32.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
} else if (controller.error != null) {
|
||||
} else if (controller.error != null || controller.room == null) {
|
||||
body = Center(
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
|
|
@ -61,7 +61,7 @@ class ActivityGeneratorView extends StatelessWidget {
|
|||
return ActivityPlannerBuilder(
|
||||
initialActivity: controller.activities![index],
|
||||
initialFilename: controller.filename,
|
||||
room: controller.room,
|
||||
room: controller.room!,
|
||||
builder: (c) {
|
||||
return ActivityPlanCard(
|
||||
regenerate: () => controller.generate(force: true),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,259 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_participant_indicator.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_results_carousel.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityFinishedStatusMessage extends StatefulWidget {
|
||||
final Room room;
|
||||
|
||||
const ActivityFinishedStatusMessage({
|
||||
super.key,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivityFinishedStatusMessageState createState() =>
|
||||
ActivityFinishedStatusMessageState();
|
||||
}
|
||||
|
||||
class ActivityFinishedStatusMessageState
|
||||
extends State<ActivityFinishedStatusMessage> {
|
||||
ActivityRoleModel? _highlightedRole;
|
||||
bool _expanded = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setDefaultHighlightedRole();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ActivityFinishedStatusMessage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_setDefaultHighlightedRole();
|
||||
}
|
||||
|
||||
void _setExpanded(bool expanded) {
|
||||
if (mounted) setState(() => _expanded = expanded);
|
||||
}
|
||||
|
||||
int get _hightlightedRoleIndex {
|
||||
if (_highlightedRole == null) {
|
||||
return -1; // No highlighted role
|
||||
}
|
||||
return widget.room.activityRoles.indexOf(_highlightedRole!);
|
||||
}
|
||||
|
||||
void _setDefaultHighlightedRole() {
|
||||
if (_hightlightedRoleIndex >= 0) return;
|
||||
|
||||
final roles = widget.room.activityRoles;
|
||||
_highlightedRole = roles.firstWhereOrNull(
|
||||
(r) => r.userId == widget.room.client.userID,
|
||||
);
|
||||
|
||||
if (_highlightedRole == null && roles.isNotEmpty) {
|
||||
_highlightedRole = roles.first;
|
||||
}
|
||||
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _highlightRole(ActivityRoleModel role) {
|
||||
if (mounted) setState(() => _highlightedRole = role);
|
||||
}
|
||||
|
||||
bool get _canMoveLeft =>
|
||||
_hightlightedRoleIndex > 0 && _highlightedRole != null;
|
||||
|
||||
bool get _canMoveRight =>
|
||||
_hightlightedRoleIndex < widget.room.activityRoles.length - 1 &&
|
||||
_highlightedRole != null;
|
||||
|
||||
void _moveLeft() {
|
||||
if (_hightlightedRoleIndex > 0) {
|
||||
_highlightRole(widget.room.activityRoles[_hightlightedRoleIndex - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
void _moveRight() {
|
||||
if (_hightlightedRoleIndex < widget.room.activityRoles.length - 1) {
|
||||
_highlightRole(widget.room.activityRoles[_hightlightedRoleIndex + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _archiveToAnalytics() async {
|
||||
final role = widget.room.activityRole(widget.room.client.userID!);
|
||||
if (role == null) {
|
||||
throw Exception(
|
||||
"Cannot archive activity without a role for user ${widget.room.client.userID!}",
|
||||
);
|
||||
}
|
||||
|
||||
role.archivedAt = DateTime.now();
|
||||
await widget.room.archiveActivity();
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.sendActivityAnalytics(widget.room.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final summary = widget.room.activitySummary;
|
||||
final imageURL = widget.room.activityPlan!.imageURL;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = MediaQuery.of(context).size.width < 600;
|
||||
|
||||
final user = widget.room.getParticipants().firstWhereOrNull(
|
||||
(u) => u.id == _highlightedRole?.userId,
|
||||
);
|
||||
final userSummary =
|
||||
widget.room.activitySummary?.participants.firstWhereOrNull(
|
||||
(p) => p.participantId == _highlightedRole!.userId,
|
||||
);
|
||||
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: _expanded
|
||||
? [
|
||||
if (summary != null) ...[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.expand_more,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () => _setExpanded(!_expanded),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
if (imageURL != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: imageURL.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(imageURL),
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
cacheKey: widget.room.activityPlan!.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: imageURL,
|
||||
fit: BoxFit.cover,
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
placeholder: (
|
||||
context,
|
||||
url,
|
||||
) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(
|
||||
summary.summary,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_highlightedRole != null &&
|
||||
user != null &&
|
||||
userSummary != null)
|
||||
ActivityResultsCarousel(
|
||||
selectedRole: _highlightedRole!,
|
||||
moveLeft: _canMoveLeft ? _moveLeft : null,
|
||||
moveRight: _canMoveRight ? _moveRight : null,
|
||||
user: user,
|
||||
summary: userSummary,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Wrap(
|
||||
spacing: 12.0,
|
||||
runSpacing: 12.0,
|
||||
children: widget.room.activityRoles
|
||||
.map(
|
||||
(role) => Opacity(
|
||||
opacity: _highlightedRole == role ? 1.0 : 0.5,
|
||||
child: ActivityParticipantIndicator(
|
||||
onTap: _highlightedRole == role
|
||||
? null
|
||||
: () => _highlightRole(role),
|
||||
role: role,
|
||||
displayname: role.userId.localpart,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
],
|
||||
if (!widget.room.isHiddenActivityRoom)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
foregroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
),
|
||||
onPressed: () async {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: _archiveToAnalytics,
|
||||
);
|
||||
|
||||
if (!resp.isError) {
|
||||
context.go(
|
||||
"/rooms/analytics?mode=activities",
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(L10n.of(context).archiveToAnalytics),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
if (summary != null)
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.expand_less,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () => _setExpanded(!_expanded),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
|
||||
class ActivityParticipantIndicator extends StatelessWidget {
|
||||
final bool selected;
|
||||
|
||||
final ActivityRoleModel? role;
|
||||
final String? displayname;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ActivityParticipantIndicator({
|
||||
super.key,
|
||||
this.selected = false,
|
||||
this.role,
|
||||
this.displayname,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AbsorbPointer(
|
||||
absorbing: onTap == null,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return Opacity(
|
||||
opacity: onTap == null ? 0.7 : 1.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: hovered || selected
|
||||
? theme.colorScheme.primaryContainer.withAlpha(
|
||||
selected ? 100 : 50,
|
||||
)
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 30.0,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
),
|
||||
Text(
|
||||
role?.role ?? L10n.of(context).participant,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
displayname ?? L10n.of(context).openRoleLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: displayname?.lightColorAvatar ??
|
||||
role?.role?.lightColorAvatar,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
187
lib/pangea/activity_planner/activity_pinned_message.dart
Normal file
187
lib/pangea/activity_planner/activity_pinned_message.dart
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_app_bar_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class ActivityPinnedMessage extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
const ActivityPinnedMessage(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
State<ActivityPinnedMessage> createState() => ActivityPinnedMessageState();
|
||||
}
|
||||
|
||||
class ActivityPinnedMessageState extends State<ActivityPinnedMessage> {
|
||||
bool _showDropdown = false;
|
||||
|
||||
Room get room => widget.controller.room;
|
||||
|
||||
void _scrollToActivity() {
|
||||
final eventId = widget.controller.timeline?.events
|
||||
.firstWhereOrNull(
|
||||
(e) => e.type == PangeaEventTypes.activityPlan,
|
||||
)
|
||||
?.eventId;
|
||||
if (eventId == null) return;
|
||||
widget.controller.scrollToEventId(eventId);
|
||||
}
|
||||
|
||||
void _setShowDropdown(bool value) {
|
||||
if (value != _showDropdown) {
|
||||
setState(() {
|
||||
_showDropdown = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _finishActivity() async {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await room.finishActivity();
|
||||
if (mounted) {
|
||||
_setShowDropdown(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (resp.isError) return;
|
||||
if (room.activityIsFinished) {
|
||||
await room.fetchSummaries();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!room.hasJoinedActivity || room.activityPlan == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: _showDropdown ? 0 : null,
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
color: _showDropdown
|
||||
? theme.colorScheme.surfaceContainerHighest
|
||||
: theme.colorScheme.surface,
|
||||
),
|
||||
child: ChatAppBarListTile(
|
||||
title: room.activityPlan?.markdown ??
|
||||
L10n.of(context).loadingPleaseWait,
|
||||
leading: const SizedBox(width: 18.0),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: room.hasFinishedActivity
|
||||
? null
|
||||
: ElevatedButton(
|
||||
onPressed:
|
||||
_showDropdown ? null : () => _setShowDropdown(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
backgroundColor: theme.colorScheme.onSurface,
|
||||
foregroundColor: theme.colorScheme.surface,
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(context).done,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: _scrollToActivity,
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: ClipRect(
|
||||
child: _showDropdown
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).endActivityDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
"${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.endActivityAssetPath}",
|
||||
width: isColumnMode ? 240.0 : 120.0,
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
foregroundColor: theme.colorScheme.onSecondary,
|
||||
backgroundColor: theme.colorScheme.secondary,
|
||||
),
|
||||
onPressed: _finishActivity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).endActivityTitle,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
if (_showDropdown)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _setShowDropdown(false),
|
||||
child: Container(color: Colors.black.withAlpha(100)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,12 +12,14 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_room_selection.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityPlanCard extends StatefulWidget {
|
||||
final VoidCallback regenerate;
|
||||
|
|
@ -70,38 +72,24 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
|
|||
}
|
||||
|
||||
Future<void> _onLaunch() async {
|
||||
if (widget.controller.room != null && !widget.controller.room!.isSpace) {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: widget.controller.launchToRoom,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms/${widget.controller.room!.id}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return showDialog(
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FullWidthDialog(
|
||||
dialogContent: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: ActivityRoomSelection(
|
||||
controller: widget.controller,
|
||||
backButton: IconButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxWidth: 400.0,
|
||||
maxHeight: 650.0,
|
||||
);
|
||||
future: () async {
|
||||
if (!widget.controller.room.isSpace) {
|
||||
throw Exception(
|
||||
"Cannot launch activity in a non-space room",
|
||||
);
|
||||
}
|
||||
|
||||
await widget.controller.launchToSpace();
|
||||
context.go("/rooms?spaceId=${widget.controller.room.id}");
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms?spaceId=${widget.controller.room.id}");
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isBookmarked => BookmarkedActivitiesRepo.isBookmarked(
|
||||
|
|
@ -134,33 +122,47 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
|
|||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: Alignment.center,
|
||||
child: widget.controller.imageURL != null ||
|
||||
widget.controller.avatar != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: widget.controller.avatar == null
|
||||
? CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: widget.controller.imageURL!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.memory(
|
||||
widget.controller.avatar!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
child: widget.controller.isLaunching
|
||||
? Avatar(
|
||||
mxContent: widget.controller.room.avatar,
|
||||
name: widget.controller.room
|
||||
.getLocalizedDisplayname(
|
||||
MatrixLocals(
|
||||
L10n.of(context),
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
size: 200.0,
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
),
|
||||
: widget.controller.imageURL != null ||
|
||||
widget.controller.avatar != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: widget.controller.avatar == null
|
||||
? CachedNetworkImage(
|
||||
fit: BoxFit.cover,
|
||||
imageUrl:
|
||||
widget.controller.imageURL!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.memory(
|
||||
widget.controller.avatar!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: const Padding(
|
||||
padding: EdgeInsets.all(28.0),
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
InkWell(
|
||||
|
|
@ -184,348 +186,534 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
|
|||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.event_note_outlined),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller:
|
||||
widget.controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).activityTitle,
|
||||
children: widget.controller.isLaunching
|
||||
? [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: widget.controller.room.avatar,
|
||||
name: widget.controller.room
|
||||
.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
),
|
||||
size: 24.0,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.controller.room
|
||||
.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
widget.controller.updatedActivity.imageURL !=
|
||||
null
|
||||
? ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(4.0),
|
||||
child: widget.controller.updatedActivity
|
||||
.imageURL!
|
||||
.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
widget
|
||||
.controller
|
||||
.updatedActivity
|
||||
.imageURL!,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
cacheKey: widget.controller
|
||||
.updatedActivity.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: widget.controller
|
||||
.updatedActivity.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
placeholder: (
|
||||
context,
|
||||
url,
|
||||
) =>
|
||||
const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.event_note_outlined,
|
||||
size: 24.0,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.controller.updatedActivity.title,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (!widget.controller.isEditing)
|
||||
IconButton(
|
||||
onPressed: _isBookmarked
|
||||
? () => _removeBookmark()
|
||||
: () => _addBookmark(
|
||||
widget.controller.updatedActivity,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.groups, size: 24.0),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context)
|
||||
.minimumActivityParticipants(
|
||||
widget.controller.updatedActivity.req
|
||||
.numberOfParticipants,
|
||||
),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.radar, size: 24.0),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).numberOfActivities,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
icon: Icon(
|
||||
_isBookmarked
|
||||
? Icons.save
|
||||
: Icons.save_outlined,
|
||||
),
|
||||
NumberCounter(
|
||||
count: widget.controller.numActivities,
|
||||
update:
|
||||
widget.controller.setNumActivities,
|
||||
min: 1,
|
||||
max: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.target,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget.controller
|
||||
.learningObjectivesController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.learningObjectiveLabel,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.learningObjective,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.steps_rounded,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget
|
||||
.controller.instructionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.instructions,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.instructions,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.school_outlined,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? LanguageLevelDropdown(
|
||||
initialLevel:
|
||||
widget.controller.languageLevel,
|
||||
onChanged:
|
||||
widget.controller.setLanguageLevel,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity.req
|
||||
.cefrLevel
|
||||
.title(context),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
if (widget.controller.vocab.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dictionary,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: List<Widget>.generate(
|
||||
widget.controller.vocab.length,
|
||||
(int index) {
|
||||
return widget.controller.isEditing
|
||||
? Chip(
|
||||
label: Text(
|
||||
widget
|
||||
.controller.vocab[index].lemma,
|
||||
),
|
||||
onDeleted: () => widget.controller
|
||||
.removeVocab(index),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Chip(
|
||||
label: Text(
|
||||
widget
|
||||
.controller.vocab[index].lemma,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
visualDensity: VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (widget.controller.isEditing) ...[
|
||||
const SizedBox(height: itemPadding),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: itemPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: widget.controller.vocabController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.addVocabulary,
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
widget.controller.addVocab();
|
||||
},
|
||||
const SizedBox(height: itemPadding),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: widget.controller.addVocab,
|
||||
onPressed: _onLaunch,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.send_outlined),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).launchToSpace,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.event_note_outlined),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller:
|
||||
widget.controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
L10n.of(context).activityTitle,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget
|
||||
.controller.updatedActivity.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (!widget.controller.isEditing)
|
||||
IconButton(
|
||||
onPressed: _isBookmarked
|
||||
? () => _removeBookmark()
|
||||
: () => _addBookmark(
|
||||
widget.controller.updatedActivity,
|
||||
),
|
||||
icon: Icon(
|
||||
_isBookmarked
|
||||
? Icons.save
|
||||
: Icons.save_outlined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.target,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget.controller
|
||||
.learningObjectivesController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
l10n.learningObjectiveLabel,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.learningObjective,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.steps_rounded,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? TextField(
|
||||
controller: widget.controller
|
||||
.instructionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.instructions,
|
||||
),
|
||||
maxLines: null,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity
|
||||
.instructions,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.school_outlined,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: widget.controller.isEditing
|
||||
? LanguageLevelDropdown(
|
||||
initialLevel:
|
||||
widget.controller.languageLevel,
|
||||
onChanged: widget
|
||||
.controller.setLanguageLevel,
|
||||
)
|
||||
: Text(
|
||||
widget.controller.updatedActivity.req
|
||||
.cefrLevel
|
||||
.title(context),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
if (widget.controller.vocab.isNotEmpty) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.dictionary,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: itemPadding),
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: List<Widget>.generate(
|
||||
widget.controller.vocab.length,
|
||||
(int index) {
|
||||
return widget.controller.isEditing
|
||||
? Chip(
|
||||
label: Text(
|
||||
widget.controller.vocab[index]
|
||||
.lemma,
|
||||
),
|
||||
onDeleted: () => widget
|
||||
.controller
|
||||
.removeVocab(index),
|
||||
backgroundColor:
|
||||
Colors.transparent,
|
||||
visualDensity:
|
||||
VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Chip(
|
||||
label: Text(
|
||||
widget.controller.vocab[index]
|
||||
.lemma,
|
||||
),
|
||||
backgroundColor:
|
||||
Colors.transparent,
|
||||
visualDensity:
|
||||
VisualDensity.compact,
|
||||
shape: const StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: itemPadding),
|
||||
widget.controller.isEditing
|
||||
? Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.controller.saveEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.save),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).save,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.controller.clearEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cancel),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
if (widget.controller.isEditing) ...[
|
||||
const SizedBox(height: itemPadding),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(top: itemPadding),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
child: TextField(
|
||||
controller:
|
||||
widget.controller.vocabController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.addVocabulary,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).edit,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
widget.controller.setEditing(true),
|
||||
onSubmitted: (value) {
|
||||
widget.controller.addVocab();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.regenerate,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.lightbulb_outline),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).regenerate,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: widget.controller.addVocab,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: _onLaunch,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.send),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context)
|
||||
.launchActivityButton,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: itemPadding),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.controller.saveEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.save),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).save,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.controller.clearEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cancel),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else
|
||||
Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
widget.controller.startEditing,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).edit,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: widget.regenerate,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).regenerate,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme
|
||||
.colorScheme.primaryContainer,
|
||||
foregroundColor: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
widget.controller.setLaunchState(
|
||||
ActivityLaunchState.launching,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.save_outlined),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context)
|
||||
.saveAndLaunch,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Visibility;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
|
@ -8,18 +8,27 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/utils/client_download_content_extension.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum ActivityLaunchState {
|
||||
base,
|
||||
editing,
|
||||
launching,
|
||||
}
|
||||
|
||||
class ActivityPlannerBuilder extends StatefulWidget {
|
||||
final ActivityPlanModel initialActivity;
|
||||
final String? initialFilename;
|
||||
final Room? room;
|
||||
final Room room;
|
||||
|
||||
final Widget Function(ActivityPlannerBuilderState) builder;
|
||||
|
||||
|
|
@ -27,7 +36,7 @@ class ActivityPlannerBuilder extends StatefulWidget {
|
|||
super.key,
|
||||
required this.initialActivity,
|
||||
this.initialFilename,
|
||||
this.room,
|
||||
required this.room,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
|
|
@ -36,11 +45,13 @@ class ActivityPlannerBuilder extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
||||
bool isEditing = false;
|
||||
ActivityLaunchState launchState = ActivityLaunchState.base;
|
||||
Uint8List? avatar;
|
||||
String? imageURL;
|
||||
String? filename;
|
||||
|
||||
int numActivities = 1;
|
||||
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
final TextEditingController instructionsController = TextEditingController();
|
||||
final TextEditingController vocabController = TextEditingController();
|
||||
|
|
@ -69,7 +80,10 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Room? get room => widget.room;
|
||||
Room get room => widget.room;
|
||||
|
||||
bool get isEditing => launchState == ActivityLaunchState.editing;
|
||||
bool get isLaunching => launchState == ActivityLaunchState.launching;
|
||||
|
||||
ActivityPlanRequest get updatedRequest {
|
||||
final int participants = int.tryParse(participantsController.text.trim()) ??
|
||||
|
|
@ -135,8 +149,10 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void setEditing(bool editting) {
|
||||
isEditing = editting;
|
||||
void startEditing() => setLaunchState(ActivityLaunchState.editing);
|
||||
|
||||
void setLaunchState(ActivityLaunchState state) {
|
||||
launchState = state;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +194,10 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
}
|
||||
}
|
||||
|
||||
void setNumActivities(int count) {
|
||||
if (mounted) setState(() => numActivities = count);
|
||||
}
|
||||
|
||||
Future<void> _setAvatarByURL(String url) async {
|
||||
try {
|
||||
if (avatar == null) {
|
||||
|
|
@ -223,7 +243,7 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
Future<void> saveEdits() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
await updateImageURL();
|
||||
setEditing(false);
|
||||
setLaunchState(ActivityLaunchState.base);
|
||||
|
||||
await BookmarkedActivitiesRepo.remove(widget.initialActivity.bookmarkId);
|
||||
await BookmarkedActivitiesRepo.save(updatedActivity);
|
||||
|
|
@ -232,20 +252,87 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
|
||||
Future<void> clearEdits() async {
|
||||
await resetActivity();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
setLaunchState(ActivityLaunchState.base);
|
||||
}
|
||||
|
||||
Future<void> launchToSpace() async {
|
||||
final List<String> activityRoomIDs = [];
|
||||
try {
|
||||
await Future.wait(
|
||||
List.generate(numActivities, (i) async {
|
||||
final id = await _launchActivityRoom(i);
|
||||
activityRoomIDs.add(id);
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
_cleanupFailedLaunch(activityRoomIDs);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> launchToRoom() async {
|
||||
if (room == null || room!.isSpace) return;
|
||||
return room?.sendActivityPlan(
|
||||
Future<String> _launchActivityRoom(int index) async {
|
||||
await updateImageURL();
|
||||
final roomID = await Matrix.of(context).client.createGroupChat(
|
||||
visibility: Visibility.private,
|
||||
groupName: "${updatedActivity.title} ${index + 1}",
|
||||
initialState: [
|
||||
if (imageURL != null)
|
||||
StateEvent(
|
||||
type: EventTypes.RoomAvatar,
|
||||
content: {'url': imageURL},
|
||||
),
|
||||
RoomDefaults.defaultPowerLevels(
|
||||
Matrix.of(context).client.userID!,
|
||||
),
|
||||
await Matrix.of(context).client.pangeaJoinRules(
|
||||
'knock_restricted',
|
||||
allow: [
|
||||
{
|
||||
"type": "m.room_membership",
|
||||
"room_id": room.id,
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
enableEncryption: false,
|
||||
);
|
||||
|
||||
Room? activityRoom = room.client.getRoomById(roomID);
|
||||
if (activityRoom == null) {
|
||||
await room.client.waitForRoomInSync(roomID);
|
||||
activityRoom = room.client.getRoomById(roomID);
|
||||
if (activityRoom == null) {
|
||||
throw Exception("Failed to create activity room");
|
||||
}
|
||||
}
|
||||
|
||||
await room.addToSpace(activityRoom.id);
|
||||
if (activityRoom.pangeaSpaceParents.isEmpty) {
|
||||
await room.client.waitForRoomInSync(activityRoom.id);
|
||||
}
|
||||
|
||||
await activityRoom.sendActivityPlan(
|
||||
updatedActivity,
|
||||
avatar: avatar,
|
||||
filename: filename,
|
||||
);
|
||||
|
||||
return activityRoom.id;
|
||||
}
|
||||
|
||||
Future<void> _cleanupFailedLaunch(List<String> roomIds) async {
|
||||
final futures = roomIds.map((id) async {
|
||||
final room = Matrix.of(context).client.getRoomById(id);
|
||||
if (room == null) return;
|
||||
|
||||
try {
|
||||
await room.leave();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to leave room $id: $e");
|
||||
}
|
||||
});
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/activity_planner/bookmarked_activity_list.dart
|
|||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_area.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/customized_svg.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum PageMode {
|
||||
|
|
@ -38,23 +39,27 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
Widget body = const SizedBox();
|
||||
Widget? body;
|
||||
switch (pageMode) {
|
||||
case PageMode.savedActivities:
|
||||
body = BookmarkedActivitiesList(
|
||||
room: room,
|
||||
controller: this,
|
||||
);
|
||||
if (room != null) {
|
||||
body = BookmarkedActivitiesList(
|
||||
room: room!,
|
||||
controller: this,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case PageMode.featuredActivities:
|
||||
body = Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: ActivitySuggestionsArea(
|
||||
scrollDirection: Axis.vertical,
|
||||
room: room,
|
||||
if (room != null) {
|
||||
body = Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: ActivitySuggestionsArea(
|
||||
scrollDirection: Axis.vertical,
|
||||
room: room!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +135,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
height: 24.0,
|
||||
width: 24.0,
|
||||
),
|
||||
Text(L10n.of(context).createActivity),
|
||||
Text(L10n.of(context).createActivityPlan),
|
||||
],
|
||||
),
|
||||
selected: false,
|
||||
|
|
@ -141,7 +146,10 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
body,
|
||||
body ??
|
||||
ErrorIndicator(
|
||||
message: L10n.of(context).oopsSomethingWentWrong,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
139
lib/pangea/activity_planner/activity_results_carousel.dart
Normal file
139
lib/pangea/activity_planner/activity_results_carousel.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class ActivityResultsCarousel extends StatelessWidget {
|
||||
final ActivityRoleModel selectedRole;
|
||||
final User user;
|
||||
final ParticipantSummaryModel summary;
|
||||
|
||||
final VoidCallback? moveLeft;
|
||||
final VoidCallback? moveRight;
|
||||
|
||||
const ActivityResultsCarousel({
|
||||
super.key,
|
||||
required this.selectedRole,
|
||||
required this.moveLeft,
|
||||
required this.moveRight,
|
||||
required this.user,
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new),
|
||||
onPressed: moveLeft,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: PressableButton(
|
||||
onPressed: null,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
color: theme.brightness == Brightness.dark
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
width: isColumnMode ? 225.0 : 175.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Avatar(
|
||||
size: isColumnMode ? 60.0 : 40.0,
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
userId: selectedRole.userId,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Text(
|
||||
selectedRole.role != null
|
||||
? "${selectedRole.role!} | ${selectedRole.userId.localpart}"
|
||||
: "${selectedRole.userId.localpart}",
|
||||
style: TextStyle(fontSize: isColumnMode ? 16.0 : 12.0),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Text(
|
||||
summary.feedback,
|
||||
style: TextStyle(fontSize: isColumnMode ? 12.0 : 8.0),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.school, size: 12.0),
|
||||
Text(
|
||||
summary.cefrLevel,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 12.0 : 8.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...summary.superlatives.map(
|
||||
(sup) => Container(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
sup,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 12.0 : 8.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward_ios),
|
||||
onPressed: moveRight,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/pangea/activity_planner/activity_role_model.dart
Normal file
53
lib/pangea/activity_planner/activity_role_model.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
class ActivityRoleModel {
|
||||
final String userId;
|
||||
final String? role;
|
||||
DateTime? finishedAt;
|
||||
DateTime? archivedAt;
|
||||
|
||||
ActivityRoleModel({
|
||||
required this.userId,
|
||||
this.role,
|
||||
this.finishedAt,
|
||||
this.archivedAt,
|
||||
});
|
||||
|
||||
bool get isFinished => finishedAt != null;
|
||||
|
||||
bool get isArchived => archivedAt != null;
|
||||
|
||||
factory ActivityRoleModel.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityRoleModel(
|
||||
userId: json['userId'],
|
||||
role: json['role'],
|
||||
finishedAt: json['finishedAt'] != null
|
||||
? DateTime.parse(json['finishedAt'])
|
||||
: null,
|
||||
archivedAt: json['archivedAt'] != null
|
||||
? DateTime.parse(json['archivedAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'userId': userId,
|
||||
'role': role,
|
||||
'finishedAt': finishedAt?.toIso8601String(),
|
||||
'archivedAt': archivedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ActivityRoleModel &&
|
||||
other.userId == userId &&
|
||||
other.role == role &&
|
||||
other.finishedAt == finishedAt;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
userId.hashCode ^ role.hashCode ^ (finishedAt?.hashCode ?? 0);
|
||||
}
|
||||
220
lib/pangea/activity_planner/activity_room_extension.dart
Normal file
220
lib/pangea/activity_planner/activity_room_extension.dart
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_role_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
||||
extension ActivityRoomExtension on Room {
|
||||
Future<void> sendActivityPlan(
|
||||
ActivityPlanModel activity, {
|
||||
Uint8List? avatar,
|
||||
String? filename,
|
||||
}) async {
|
||||
BookmarkedActivitiesRepo.save(activity);
|
||||
|
||||
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityPlan,
|
||||
"",
|
||||
activity.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setActivityRole({
|
||||
String? role,
|
||||
}) async {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
ActivityRoleModel(
|
||||
userId: client.userID!,
|
||||
role: role,
|
||||
).toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> finishActivity() async {
|
||||
final role = activityRole(client.userID!);
|
||||
if (role == null) return;
|
||||
|
||||
role.finishedAt = DateTime.now();
|
||||
final syncFuture = client.waitForRoomInSync(id);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
role.toJson(),
|
||||
);
|
||||
await syncFuture;
|
||||
}
|
||||
|
||||
Future<void> setActivitySummary(
|
||||
ActivitySummaryResponseModel summary,
|
||||
) async {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activitySummary,
|
||||
"",
|
||||
summary.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fetchSummaries() async {
|
||||
if (activitySummary != null) return;
|
||||
|
||||
final events = await getAllEvents(this);
|
||||
final List<ActivitySummaryResultsMessage> messages = [];
|
||||
for (final event in events) {
|
||||
if (event.type != EventTypes.Message ||
|
||||
event.messageType != MessageTypes.Text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final timeline = this.timeline ?? await getTimeline();
|
||||
final pangeaMessage = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
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",
|
||||
],
|
||||
);
|
||||
|
||||
messages.add(activityMessage);
|
||||
}
|
||||
|
||||
final resp = await ActivitySummaryRepo.get(
|
||||
ActivitySummaryRequestModel(
|
||||
activity: activityPlan!,
|
||||
activityResults: messages,
|
||||
contentFeedback: [],
|
||||
),
|
||||
);
|
||||
|
||||
await setActivitySummary(resp);
|
||||
}
|
||||
|
||||
Future<void> archiveActivity() async {
|
||||
final role = activityRole(client.userID!);
|
||||
if (role == null) return;
|
||||
|
||||
role.archivedAt = DateTime.now();
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRole,
|
||||
client.userID!,
|
||||
role.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
ActivityPlanModel? get activityPlan {
|
||||
final stateEvent = getState(PangeaEventTypes.activityPlan);
|
||||
if (stateEvent == null) return null;
|
||||
|
||||
try {
|
||||
return ActivityPlanModel.fromJson(stateEvent.content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"stateEvent": stateEvent.content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ActivityRoleModel? activityRole(String userId) {
|
||||
final stateEvent = getState(PangeaEventTypes.activityRole, userId);
|
||||
if (stateEvent == null) return null;
|
||||
|
||||
try {
|
||||
return ActivityRoleModel.fromJson(stateEvent.content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"userId": userId,
|
||||
"stateEvent": stateEvent.content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ActivitySummaryResponseModel? get activitySummary {
|
||||
final stateEvent = getState(PangeaEventTypes.activitySummary);
|
||||
if (stateEvent == null) return null;
|
||||
|
||||
try {
|
||||
return ActivitySummaryResponseModel.fromJson(stateEvent.content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"stateEvent": stateEvent.content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<StrippedStateEvent> get _activityRoleEvents {
|
||||
return states[PangeaEventTypes.activityRole]?.values.toList() ?? [];
|
||||
}
|
||||
|
||||
List<ActivityRoleModel> get activityRoles {
|
||||
return _activityRoleEvents
|
||||
.map((r) => ActivityRoleModel.fromJson(r.content))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool get hasJoinedActivity {
|
||||
return activityPlan == null || activityRole(client.userID!) != null;
|
||||
}
|
||||
|
||||
bool get hasFinishedActivity {
|
||||
final role = activityRole(client.userID!);
|
||||
return role != null && role.isFinished;
|
||||
}
|
||||
|
||||
bool get activityIsFinished {
|
||||
return activityRoles.isNotEmpty && activityRoles.every((r) => r.isFinished);
|
||||
}
|
||||
|
||||
int? get numberOfParticipants {
|
||||
return activityPlan?.req.numberOfParticipants;
|
||||
}
|
||||
|
||||
int get remainingRoles {
|
||||
if (numberOfParticipants == null) return 0;
|
||||
return max(0, numberOfParticipants! - activityRoles.length);
|
||||
}
|
||||
}
|
||||
67
lib/pangea/activity_planner/activity_status_message.dart
Normal file
67
lib/pangea/activity_planner/activity_status_message.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_finished_status_message.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_unfinished_status_message.dart';
|
||||
|
||||
class ActivityStatusMessage extends StatefulWidget {
|
||||
final Room room;
|
||||
|
||||
const ActivityStatusMessage({
|
||||
super.key,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivityStatusMessageState createState() => ActivityStatusMessageState();
|
||||
}
|
||||
|
||||
class ActivityStatusMessageState extends State<ActivityStatusMessage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.room.activityIsFinished && widget.room.activitySummary == null) {
|
||||
widget.room.fetchSummaries().then((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Material(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: !widget.room.hasJoinedActivity ||
|
||||
widget.room.activityIsFinished
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: FluffyThemes.isColumnMode(context) ? 32.0 : 16.0,
|
||||
left: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: widget.room.activityIsFinished
|
||||
? ActivityFinishedStatusMessage(room: widget.room)
|
||||
: ActivityUnfinishedStatusMessage(room: widget.room),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_participant_indicator.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class ActivityUnfinishedStatusMessage extends StatefulWidget {
|
||||
final Room room;
|
||||
const ActivityUnfinishedStatusMessage({
|
||||
super.key,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivityUnfinishedStatusMessageState createState() =>
|
||||
ActivityUnfinishedStatusMessageState();
|
||||
}
|
||||
|
||||
class ActivityUnfinishedStatusMessageState
|
||||
extends State<ActivityUnfinishedStatusMessage> {
|
||||
int? _selectedRole;
|
||||
|
||||
void _selectRole(int role) {
|
||||
if (_selectedRole == role) return;
|
||||
if (mounted) setState(() => _selectedRole = role);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final unassignedRoles = widget.room.remainingRoles;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (unassignedRoles > 0)
|
||||
Wrap(
|
||||
spacing: 12.0,
|
||||
runSpacing: 12.0,
|
||||
children: List.generate(unassignedRoles, (index) {
|
||||
return ActivityParticipantIndicator(
|
||||
selected: _selectedRole == index,
|
||||
onTap: () => _selectRole(index),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
unassignedRoles > 0
|
||||
? L10n.of(context).unjoinedActivityMessage
|
||||
: L10n.of(context).fullActivityMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
if (unassignedRoles > 0)
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
foregroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
),
|
||||
onPressed: _selectedRole != null
|
||||
? () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.room.setActivityRole(),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).confirmRole,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.
|
|||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
|
||||
class BookmarkedActivitiesList extends StatefulWidget {
|
||||
final Room? room;
|
||||
final Room room;
|
||||
|
||||
final ActivityPlannerPageState controller;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,628 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivityRoomSelection extends StatefulWidget {
|
||||
final ActivityPlannerBuilderState controller;
|
||||
final Widget backButton;
|
||||
|
||||
const ActivityRoomSelection({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.backButton,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityRoomSelection> createState() => ActivityRoomSelectionState();
|
||||
}
|
||||
|
||||
class ActivityRoomSelectionState extends State<ActivityRoomSelection> {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
|
||||
bool _loading = false;
|
||||
bool _complete = false;
|
||||
|
||||
bool _hasBotDM = true;
|
||||
List<Room> _launchableRooms = [];
|
||||
final List<String> _selectedRooms = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_launchableRooms = Matrix.of(context)
|
||||
.client
|
||||
.rooms
|
||||
.where((room) {
|
||||
return room.canSendDefaultStates &&
|
||||
!room.isSpace &&
|
||||
!room.isAnalyticsRoom;
|
||||
})
|
||||
.toList()
|
||||
.sorted((a, b) {
|
||||
final aIsBotDM = a.directChatMatrixID == BotName.byEnvironment;
|
||||
final bIsBotDM = b.directChatMatrixID == BotName.byEnvironment;
|
||||
if (aIsBotDM && !bIsBotDM) return -1;
|
||||
if (!aIsBotDM && bIsBotDM) return 1;
|
||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
final room = widget.controller.room;
|
||||
if (room != null && room.isSpace) {
|
||||
_launchableRooms = _launchableRooms.where((r) {
|
||||
return room.spaceChildren.any((child) => child.roomId == r.id);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
_hasBotDM = Matrix.of(context).client.rooms.any((room) {
|
||||
if (room.isDirectChat &&
|
||||
room.directChatMatrixID == BotName.byEnvironment) {
|
||||
return true;
|
||||
}
|
||||
if (room.botOptions?.mode == BotMode.directChat) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Room> get _filteredRooms {
|
||||
final searchText = searchController.text.toLowerCase();
|
||||
return _launchableRooms.where((room) {
|
||||
return room.name.toLowerCase().contains(searchText);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _toggleRoomSelection(String roomId) {
|
||||
_selectedRooms.contains(roomId)
|
||||
? _selectedRooms.remove(roomId)
|
||||
: _selectedRooms.add(roomId);
|
||||
if (_selectedRooms.contains(roomId)) {
|
||||
_complete = false;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Map<String, Room> get _spaceDelegateCandidates {
|
||||
final spaces = Matrix.of(context).client.rooms.where((r) => r.isSpace);
|
||||
final candidates = <String, Room>{};
|
||||
for (final space in spaces) {
|
||||
for (final spaceChild in space.spaceChildren) {
|
||||
final roomId = spaceChild.roomId;
|
||||
if (roomId == null) continue;
|
||||
candidates[roomId] = space;
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
final Map<String, int> _launchStatus = {};
|
||||
|
||||
Future<void> _sendActivityPlan(Room room) async {
|
||||
try {
|
||||
setState(() => _launchStatus[room.id] = 0);
|
||||
await room.sendActivityPlan(
|
||||
widget.controller.updatedActivity,
|
||||
avatar: widget.controller.avatar,
|
||||
filename: widget.controller.filename,
|
||||
);
|
||||
_launchStatus[room.id] = 1;
|
||||
} catch (e, s) {
|
||||
_launchStatus[room.id] = -1;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": room.id,
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _launchBotDM() async {
|
||||
try {
|
||||
setState(() => _launchStatus["placeholder"] = 0);
|
||||
|
||||
Uri? avatarUrl;
|
||||
final imageUrl = widget.controller.imageURL ??
|
||||
widget.controller.updatedActivity.imageURL;
|
||||
|
||||
Uint8List? avatar = widget.controller.avatar;
|
||||
if (avatar != null) {
|
||||
avatarUrl = await Matrix.of(context).client.uploadContent(
|
||||
widget.controller.avatar!,
|
||||
);
|
||||
} else if (imageUrl != null) {
|
||||
final Response response = await http.get(Uri.parse(imageUrl));
|
||||
avatar = response.bodyBytes;
|
||||
avatarUrl = await Matrix.of(context).client.uploadContent(
|
||||
avatar,
|
||||
);
|
||||
}
|
||||
|
||||
// avatar == null ? null : await client.uploadContent(avatar);
|
||||
final roomId = await Matrix.of(context).client.createRoom(
|
||||
name: widget.controller.updatedActivity.title,
|
||||
invite: [BotName.byEnvironment],
|
||||
isDirect: true,
|
||||
preset: CreateRoomPreset.trustedPrivateChat,
|
||||
initialState: [
|
||||
BotOptionsModel(mode: BotMode.directChat).toStateEvent,
|
||||
RoomDefaults.defaultPowerLevels(
|
||||
Matrix.of(context).client.userID!,
|
||||
),
|
||||
if (avatar != null && avatarUrl != null)
|
||||
StateEvent(
|
||||
type: EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
],
|
||||
);
|
||||
Room? room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
await Matrix.of(context).client.waitForRoomInSync(
|
||||
roomId,
|
||||
join: true,
|
||||
);
|
||||
|
||||
room = Matrix.of(context).client.getRoomById(roomId);
|
||||
if (room == null) {
|
||||
throw Exception("Room not found");
|
||||
}
|
||||
|
||||
await room.sendActivityPlan(
|
||||
widget.controller.updatedActivity,
|
||||
avatar: widget.controller.avatar,
|
||||
filename: widget.controller.filename,
|
||||
);
|
||||
}
|
||||
_launchStatus["placeholder"] = 1;
|
||||
return roomId;
|
||||
} catch (e, s) {
|
||||
_launchStatus["placeholder"] = -1;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _launch() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final List<Future> futures = [];
|
||||
for (final roomId in _selectedRooms) {
|
||||
if (_launchStatus[roomId] == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Room? room = _launchableRooms.firstWhereOrNull(
|
||||
(r) => r.id == roomId,
|
||||
);
|
||||
if (room == null) {
|
||||
if (roomId == 'placeholder') futures.add(_launchBotDM());
|
||||
} else {
|
||||
futures.add(_sendActivityPlan(room));
|
||||
}
|
||||
}
|
||||
|
||||
final resp = await Future.wait(futures);
|
||||
_complete = true;
|
||||
if (!mounted) return;
|
||||
if (_selectedRooms.length == 1 &&
|
||||
_launchStatus[_selectedRooms.first] == 1) {
|
||||
if (_selectedRooms.first == 'placeholder' && resp.first != null) {
|
||||
context.go("/rooms/${resp.first}");
|
||||
Navigator.of(context).pop();
|
||||
} else if (_selectedRooms.first != 'placeholder') {
|
||||
context.go('/rooms/${_selectedRooms.first}');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"activity": widget.controller.updatedActivity.toJson(),
|
||||
"filename": widget.controller.filename,
|
||||
"avatarURL": widget.controller.imageURL,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _tooltip(String roomId) {
|
||||
final status = _launchStatus[roomId];
|
||||
if (status == 0) {
|
||||
return "Sending...";
|
||||
} else if (status == 1) {
|
||||
return "Go to chat";
|
||||
} else if (status == -1) {
|
||||
return "Failed to send";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void _onTap(Room room) {
|
||||
final status = _launchStatus[room.id];
|
||||
if (status == 0) {
|
||||
return;
|
||||
} else if (status == 1) {
|
||||
context.go('/rooms/${room.id}');
|
||||
Navigator.of(context).pop();
|
||||
} else if (status == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint("Toggling room selection for ${room.id}");
|
||||
_toggleRoomSelection(room.id);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).selectChats),
|
||||
leading: widget.backButton,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
focusNode: searchFocusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (text) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
hintText: L10n.of(context).searchChats,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
suffixIcon: searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
searchController.clear();
|
||||
searchFocusNode.unfocus();
|
||||
});
|
||||
},
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () => searchFocusNode.requestFocus(),
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredRooms.length + (_hasBotDM ? 0 : 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (!_hasBotDM && index == 0) {
|
||||
return ChatActivityPlaceholder(
|
||||
activity: widget.controller.updatedActivity,
|
||||
selected: _selectedRooms.contains("placeholder"),
|
||||
onTap: () {
|
||||
_toggleRoomSelection("placeholder");
|
||||
},
|
||||
tooltip: _tooltip("placeholder"),
|
||||
status: _launchStatus["placeholder"],
|
||||
avatar: widget.controller.avatar,
|
||||
);
|
||||
}
|
||||
if (!_hasBotDM) index--;
|
||||
|
||||
final room = _filteredRooms[index];
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
final space = _spaceDelegateCandidates[room.id];
|
||||
return Tooltip(
|
||||
message: _tooltip(room.id),
|
||||
child: ListTile(
|
||||
title: Text(displayname),
|
||||
leading: SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
height: Avatar.defaultSize,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (space != null)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Avatar(
|
||||
border: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius / 4,
|
||||
),
|
||||
mxContent: space.avatar,
|
||||
size: Avatar.defaultSize * 0.75,
|
||||
name: space.getLocalizedDisplayname(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Avatar(
|
||||
border: space == null
|
||||
? room.isSpace
|
||||
? BorderSide(
|
||||
width: 1,
|
||||
color: theme.dividerColor,
|
||||
)
|
||||
: null
|
||||
: BorderSide(
|
||||
width: 2,
|
||||
color: theme.colorScheme.surface,
|
||||
),
|
||||
mxContent: room.avatar,
|
||||
size: Avatar.defaultSize * 0.75,
|
||||
name: displayname,
|
||||
presenceUserId: room.directChatMatrixID,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
width: 30.0,
|
||||
height: 30.0,
|
||||
alignment: Alignment.center,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final status = _launchStatus[room.id];
|
||||
|
||||
if (status == 0) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
} else if (status == 1) {
|
||||
return const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppConfig.success,
|
||||
);
|
||||
} else if (status == -1) {
|
||||
return Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
return Checkbox(
|
||||
value: _selectedRooms.contains(room.id),
|
||||
onChanged: (_) => _onTap(room),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () => _onTap(room),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: _complete
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(L10n.of(context).selectChatToStart),
|
||||
)
|
||||
: ElevatedButton(
|
||||
onPressed: _selectedRooms.isNotEmpty ? _launch : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
disabledBackgroundColor: theme.colorScheme.primary,
|
||||
disabledForegroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_loading
|
||||
? const Expanded(
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context).launchActivityToChats,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatActivityPlaceholder extends StatelessWidget {
|
||||
final ActivityPlanModel activity;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
final String tooltip;
|
||||
final Uint8List? avatar;
|
||||
final int? status;
|
||||
|
||||
const ChatActivityPlaceholder({
|
||||
required this.activity,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
required this.tooltip,
|
||||
required this.status,
|
||||
this.avatar,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const size = Avatar.defaultSize * 0.75;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: ListTile(
|
||||
title: Text(activity.title),
|
||||
leading: SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
height: Avatar.defaultSize,
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Material(
|
||||
color: theme.brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(size / 2),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: avatar != null
|
||||
? Image.memory(avatar!)
|
||||
: activity.imageURL != null
|
||||
? activity.imageURL!.startsWith('mxc')
|
||||
? MxcImage(
|
||||
uri: Uri.parse(activity.imageURL!),
|
||||
width: size,
|
||||
height: size,
|
||||
cacheKey: activity.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: Container(
|
||||
width: 30.0,
|
||||
height: 30.0,
|
||||
alignment: Alignment.center,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (status == 0) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
} else if (status == 1) {
|
||||
return const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: AppConfig.success,
|
||||
);
|
||||
} else if (status == -1) {
|
||||
return Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.error,
|
||||
);
|
||||
}
|
||||
|
||||
return Checkbox(
|
||||
value: selected,
|
||||
onChanged: (_) => onTap(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ActivitySuggestionCardRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final IconData? icon;
|
||||
final Widget? leading;
|
||||
final Widget child;
|
||||
|
||||
const ActivitySuggestionCardRow({
|
||||
required this.icon,
|
||||
required this.child,
|
||||
this.icon,
|
||||
this.leading,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -15,12 +17,14 @@ class ActivitySuggestionCardRow extends StatelessWidget {
|
|||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16.0,
|
||||
),
|
||||
if (leading != null) leading!,
|
||||
if (icon != null)
|
||||
Icon(
|
||||
icon,
|
||||
size: 24.0,
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,353 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivitySuggestionCarousel extends StatefulWidget {
|
||||
final Function(
|
||||
ActivityPlanModel?,
|
||||
Uint8List? avatar,
|
||||
String? filename,
|
||||
) onActivitySelected;
|
||||
final ActivityPlanModel? selectedActivity;
|
||||
final Uint8List? selectedActivityImage;
|
||||
final bool enabled;
|
||||
|
||||
const ActivitySuggestionCarousel({
|
||||
required this.onActivitySelected,
|
||||
required this.selectedActivity,
|
||||
required this.selectedActivityImage,
|
||||
this.enabled = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ActivitySuggestionCarouselState createState() =>
|
||||
ActivitySuggestionCarouselState();
|
||||
}
|
||||
|
||||
class ActivitySuggestionCarouselState
|
||||
extends State<ActivitySuggestionCarousel> {
|
||||
bool _loading = true;
|
||||
bool _closed = false;
|
||||
String? _error;
|
||||
|
||||
double get _cardWidth => _isColumnMode ? 250.0 : 175.0;
|
||||
double get _cardHeight => _isColumnMode ? 360.0 : 275.0;
|
||||
|
||||
ActivityPlanModel? _currentActivity;
|
||||
final List<ActivityPlanModel> _activityItems = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setActivityItems();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ActivitySuggestionCarousel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedActivity != oldWidget.selectedActivity &&
|
||||
_currentIndex != null &&
|
||||
widget.selectedActivity != null) {
|
||||
final prevIndex = _currentIndex!;
|
||||
setState(
|
||||
() {
|
||||
_activityItems[prevIndex] = widget.selectedActivity!;
|
||||
_currentActivity = widget.selectedActivity;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setActivityItems() async {
|
||||
try {
|
||||
final ActivityPlanRequest request = ActivityPlanRequest(
|
||||
topic: "",
|
||||
mode: "",
|
||||
objective: "",
|
||||
media: MediaEnum.nan,
|
||||
cefrLevel: LanguageLevelTypeEnum.a1,
|
||||
languageOfInstructions: LanguageKeys.defaultLanguage,
|
||||
targetLanguage:
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
numberOfParticipants: 3,
|
||||
count: 5,
|
||||
);
|
||||
final resp = await ActivitySearchRepo.get(request);
|
||||
_activityItems.addAll(resp.activityPlans);
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_loading = false;
|
||||
_currentActivity =
|
||||
_activityItems.isNotEmpty ? _activityItems.first : null;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
|
||||
|
||||
int? get _currentIndex {
|
||||
if (_currentActivity == null) return null;
|
||||
final index = _activityItems.indexOf(_currentActivity!);
|
||||
return index == -1 ? null : index;
|
||||
}
|
||||
|
||||
bool get _canMoveLeft =>
|
||||
widget.enabled && _currentIndex != null && _currentIndex! > 0;
|
||||
|
||||
bool get _canMoveRight =>
|
||||
widget.enabled &&
|
||||
_currentIndex != null &&
|
||||
_currentIndex! < _activityItems.length - 1;
|
||||
|
||||
void _moveLeft() {
|
||||
if (!_canMoveLeft) return;
|
||||
_setActivityByIndex(_currentIndex! - 1);
|
||||
}
|
||||
|
||||
void _moveRight() {
|
||||
if (!_canMoveRight) return;
|
||||
_setActivityByIndex(_currentIndex! + 1);
|
||||
}
|
||||
|
||||
void _setActivityByIndex(int index) {
|
||||
if (index < 0 || index >= _activityItems.length) return;
|
||||
setState(() {
|
||||
_currentActivity = _activityItems[index];
|
||||
});
|
||||
}
|
||||
|
||||
void _close() {
|
||||
widget.onActivitySelected(null, null, null);
|
||||
setState(() {
|
||||
_closed = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _onReplaceActivity(ActivityPlanModel a) {
|
||||
final index = _currentIndex;
|
||||
if (index == null || index < 0 || index >= _activityItems.length) {
|
||||
return;
|
||||
}
|
||||
_activityItems[index] = a;
|
||||
setState(() => _currentActivity = a);
|
||||
}
|
||||
|
||||
void _onClickCard() {
|
||||
if (widget.selectedActivity == _currentActivity) {
|
||||
widget.onActivitySelected(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivityPlannerBuilder(
|
||||
initialActivity: _currentActivity!,
|
||||
builder: (controller) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: controller,
|
||||
buttonText: L10n.of(context).selectActivity,
|
||||
replaceActivity: _onReplaceActivity,
|
||||
onLaunch: () => widget.onActivitySelected(
|
||||
controller.updatedActivity,
|
||||
controller.avatar,
|
||||
controller.filename,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: AnimatedOpacity(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
opacity: widget.enabled ? 1.0 : 0.5,
|
||||
child: _closed
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.dividerColor),
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16.0,
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).newChatActivityTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: widget.enabled ? _close : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text(L10n.of(context).newChatActivityDesc),
|
||||
),
|
||||
Row(
|
||||
spacing: _isColumnMode ? 16.0 : 4.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: _canMoveLeft
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveLeft ? _moveLeft : null,
|
||||
child: Icon(
|
||||
Icons.chevron_left_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveLeft ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints:
|
||||
BoxConstraints(maxHeight: _cardHeight + 12.0),
|
||||
child: _error != null ||
|
||||
(_currentActivity == null && !_loading)
|
||||
? const SizedBox.shrink()
|
||||
: _loading
|
||||
? Shimmer.fromColors(
|
||||
baseColor: theme.colorScheme.primary
|
||||
.withAlpha(50),
|
||||
highlightColor: theme.colorScheme.primary
|
||||
.withAlpha(150),
|
||||
child: Container(
|
||||
height: _cardHeight,
|
||||
width: _cardWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.surfaceContainer,
|
||||
borderRadius:
|
||||
BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ActivitySuggestionCard(
|
||||
selected: widget.selectedActivity ==
|
||||
_currentActivity,
|
||||
activity: _currentActivity!,
|
||||
onPressed:
|
||||
widget.enabled ? _onClickCard : null,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
image: _currentActivity ==
|
||||
widget.selectedActivity
|
||||
? widget.selectedActivityImage
|
||||
: null,
|
||||
onChange: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: _canMoveRight
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: _canMoveRight ? _moveRight : null,
|
||||
child: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
size: 32.0,
|
||||
color: _canMoveRight ? null : theme.disabledColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16.0,
|
||||
children: _activityItems.mapIndexed((i, activity) {
|
||||
final selected = activity == _currentActivity;
|
||||
return InkWell(
|
||||
enableFeedback: widget.enabled,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
onTap: widget.enabled
|
||||
? () => _setActivityByIndex(i)
|
||||
: null,
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: selected ? 0.0 : 0.5,
|
||||
sigmaY: selected ? 0.0 : 0.5,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: selected ? 1.0 : 0.5,
|
||||
child: ClipOval(
|
||||
child: SizedBox.fromSize(
|
||||
size: const Size.fromRadius(12.0),
|
||||
child: activity.imageURL != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: activity.imageURL!,
|
||||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
progressIndicatorBuilder:
|
||||
(context, url, progress) {
|
||||
return CircularProgressIndicator(
|
||||
value: progress.progress,
|
||||
);
|
||||
},
|
||||
)
|
||||
: CircleAvatar(
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
radius: 12.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_room_selection.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog_content.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
enum _PageMode {
|
||||
activity,
|
||||
roomSelection,
|
||||
}
|
||||
|
||||
class ActivitySuggestionDialog extends StatefulWidget {
|
||||
final ActivityPlannerBuilderState controller;
|
||||
final String buttonText;
|
||||
|
||||
final VoidCallback? onLaunch;
|
||||
final Function(ActivityPlanModel)? replaceActivity;
|
||||
|
||||
const ActivitySuggestionDialog({
|
||||
required this.controller,
|
||||
required this.buttonText,
|
||||
this.onLaunch,
|
||||
this.replaceActivity,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -46,40 +32,33 @@ class ActivitySuggestionDialog extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
||||
_PageMode _pageMode = _PageMode.activity;
|
||||
|
||||
bool _loading = false;
|
||||
Object? _error;
|
||||
String? _regenerateError;
|
||||
String? _launchError;
|
||||
|
||||
double get _width => FluffyThemes.isColumnMode(context)
|
||||
? 400.0
|
||||
: MediaQuery.of(context).size.width;
|
||||
|
||||
Future<void> _launchActivity() async {
|
||||
Future<void> launchActivity() async {
|
||||
try {
|
||||
if (!widget.controller.room.isSpace) {
|
||||
throw Exception(
|
||||
"Cannot launch activity in a non-space room",
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
_regenerateError = null;
|
||||
_launchError = null;
|
||||
});
|
||||
|
||||
if (widget.onLaunch != null) {
|
||||
widget.onLaunch!.call();
|
||||
Navigator.of(context).pop();
|
||||
} else if (widget.controller.room != null &&
|
||||
!widget.controller.room!.isSpace) {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: widget.controller.launchToRoom,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms/${widget.controller.room!.id}");
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} else {
|
||||
_setPageMode(_PageMode.roomSelection);
|
||||
}
|
||||
await widget.controller.launchToSpace();
|
||||
context.go("/rooms?spaceId=${widget.controller.room.id}");
|
||||
Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
_error = e;
|
||||
_launchError = L10n.of(context).errorLaunchActivityMessage;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -96,16 +75,11 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
}
|
||||
}
|
||||
|
||||
void _setPageMode(_PageMode mode) {
|
||||
setState(() {
|
||||
_pageMode = mode;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onRegenerate() async {
|
||||
Future<void> onRegenerate() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
_regenerateError = null;
|
||||
_launchError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -121,7 +95,7 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
widget.replaceActivity?.call(plan);
|
||||
await widget.controller.overrideActivity(plan);
|
||||
} catch (e, s) {
|
||||
_error = e;
|
||||
_regenerateError = L10n.of(context).errorRegenerateActivityMessage;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
|
|
@ -142,22 +116,28 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
void _resetActivity() {
|
||||
widget.controller.resetActivity();
|
||||
setState(() {
|
||||
_pageMode = _PageMode.activity;
|
||||
_loading = false;
|
||||
_error = null;
|
||||
_regenerateError = null;
|
||||
_launchError = null;
|
||||
});
|
||||
}
|
||||
|
||||
ButtonStyle get buttonStyle => ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
);
|
||||
|
||||
double get width => FluffyThemes.isColumnMode(context)
|
||||
? 400.0
|
||||
: MediaQuery.of(context).size.width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final buttonStyle = ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
foregroundColor: theme.colorScheme.onPrimaryContainer,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
),
|
||||
);
|
||||
final buttonStyle = this.buttonStyle;
|
||||
|
||||
final body = Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
|
|
@ -168,23 +148,22 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (_pageMode == _PageMode.activity) {
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ErrorIndicator(
|
||||
message:
|
||||
L10n.of(context).errorRegenerateActivityMessage,
|
||||
),
|
||||
if (_regenerateError != null || _launchError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
spacing: 16.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ErrorIndicator(
|
||||
message: _regenerateError ?? _launchError!,
|
||||
),
|
||||
if (_regenerateError != null)
|
||||
Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _onRegenerate,
|
||||
onPressed: onRegenerate,
|
||||
style: buttonStyle,
|
||||
child: Text(L10n.of(context).tryAgain),
|
||||
),
|
||||
|
|
@ -194,552 +173,46 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
child: Text(L10n.of(context).reset),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: launchActivity,
|
||||
style: buttonStyle,
|
||||
child: Text(L10n.of(context).tryAgain),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
return Form(
|
||||
key: widget.controller.formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
width: (_width / 2) + 42.0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: widget.controller.avatar != null
|
||||
? Image.memory(
|
||||
widget.controller.avatar!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: widget.controller.updatedActivity
|
||||
.imageURL !=
|
||||
null
|
||||
? widget.controller
|
||||
.updatedActivity.imageURL!
|
||||
.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
widget
|
||||
.controller
|
||||
.updatedActivity
|
||||
.imageURL!,
|
||||
),
|
||||
width: _width / 2,
|
||||
height: 200,
|
||||
cacheKey: widget
|
||||
.controller
|
||||
.updatedActivity
|
||||
.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: widget
|
||||
.controller
|
||||
.updatedActivity
|
||||
.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (
|
||||
context,
|
||||
url,
|
||||
) =>
|
||||
const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
onTap: widget.controller.selectAvatar,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
radius: 20.0,
|
||||
child: Icon(
|
||||
Icons.add_a_photo_outlined,
|
||||
size: 20.0,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.event_note_outlined,
|
||||
child: TextFormField(
|
||||
controller:
|
||||
widget.controller.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
L10n.of(context).activityTitle,
|
||||
),
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.event_note_outlined,
|
||||
child: Text(
|
||||
widget
|
||||
.controller.updatedActivity.title,
|
||||
style: theme.textTheme.titleLarge
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.target,
|
||||
child: TextFormField(
|
||||
controller: widget.controller
|
||||
.learningObjectivesController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context)
|
||||
.learningObjectiveLabel,
|
||||
),
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.target,
|
||||
child: Text(
|
||||
widget.controller.updatedActivity
|
||||
.learningObjective,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.steps,
|
||||
child: TextFormField(
|
||||
controller: widget.controller
|
||||
.instructionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
L10n.of(context).instructions,
|
||||
),
|
||||
maxLines: 8,
|
||||
minLines: 1,
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.steps,
|
||||
child: Text(
|
||||
widget.controller.updatedActivity
|
||||
.instructions,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.group_outlined,
|
||||
child: TextFormField(
|
||||
controller: widget.controller
|
||||
.participantsController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
L10n.of(context).classRoster,
|
||||
),
|
||||
maxLines: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final val = int.parse(value);
|
||||
if (val <= 0) {
|
||||
return L10n.of(context)
|
||||
.pleaseEnterInt;
|
||||
}
|
||||
if (val > 50) {
|
||||
return L10n.of(context)
|
||||
.maxFifty;
|
||||
}
|
||||
} catch (e) {
|
||||
return L10n.of(context)
|
||||
.pleaseEnterANumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.group_outlined,
|
||||
child: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
widget.controller.updatedActivity
|
||||
.req.numberOfParticipants,
|
||||
),
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.school_outlined,
|
||||
child: LanguageLevelDropdown(
|
||||
initialLevel:
|
||||
widget.controller.languageLevel,
|
||||
onChanged: widget
|
||||
.controller.setLanguageLevel,
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.school_outlined,
|
||||
child: Text(
|
||||
widget.controller.updatedActivity.req
|
||||
.cefrLevel
|
||||
.title(context),
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.dictionary,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 60.0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: widget.controller.vocab
|
||||
.mapIndexed(
|
||||
(i, vocab) => Container(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.primary
|
||||
.withAlpha(
|
||||
20,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor:
|
||||
SystemMouseCursors
|
||||
.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => widget
|
||||
.controller
|
||||
.removeVocab(
|
||||
i,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize:
|
||||
MainAxisSize
|
||||
.min,
|
||||
children: [
|
||||
Text(
|
||||
vocab.lemma,
|
||||
),
|
||||
const Icon(
|
||||
Icons.close,
|
||||
size: 12.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.dictionary,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 60.0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: widget.controller.vocab
|
||||
.map(
|
||||
(vocab) => Container(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.primary
|
||||
.withAlpha(
|
||||
20,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
vocab.lemma,
|
||||
style: theme.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.controller.isEditing)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget
|
||||
.controller.vocabController,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(
|
||||
context,
|
||||
).addVocabulary,
|
||||
),
|
||||
maxLines: 1,
|
||||
onFieldSubmitted: (_) => widget
|
||||
.controller
|
||||
.addVocab(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(
|
||||
0.0,
|
||||
),
|
||||
constraints:
|
||||
const BoxConstraints(), // override default min size of 48px
|
||||
iconSize: 16.0,
|
||||
icon: const Icon(
|
||||
Icons.add_outlined,
|
||||
),
|
||||
onPressed:
|
||||
widget.controller.addVocab,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: widget.controller.isEditing
|
||||
? Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: widget.controller.saveEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.save),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).save,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: widget.controller.clearEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cancel),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: buttonStyle,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).edit,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () => widget.controller
|
||||
.setEditing(true),
|
||||
),
|
||||
),
|
||||
if (widget.replaceActivity != null)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: _onRegenerate,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).regenerate,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: _launchActivity,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.send),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.buttonText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ActivityRoomSelection(
|
||||
controller: widget.controller,
|
||||
backButton: BackButton(
|
||||
onPressed: () => _setPageMode(
|
||||
_PageMode.activity,
|
||||
),
|
||||
),
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
return Form(
|
||||
key: widget.controller.formKey,
|
||||
child: ActivitySuggestionDialogContent(controller: this),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_pageMode == _PageMode.activity)
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
left: 4.0,
|
||||
child: IconButton.filled(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.surface.withAlpha(170),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.close_outlined,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
tooltip: L10n.of(context).close,
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
left: 4.0,
|
||||
child: IconButton.filled(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.surface.withAlpha(170),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.close_outlined,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
tooltip: L10n.of(context).close,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -750,3 +223,69 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NumberCounter extends StatelessWidget {
|
||||
final int count;
|
||||
final Function(int) update;
|
||||
|
||||
final int? min;
|
||||
final int? max;
|
||||
|
||||
const NumberCounter({
|
||||
super.key,
|
||||
required this.count,
|
||||
required this.update,
|
||||
this.min,
|
||||
this.max,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
iconSize: 24.0,
|
||||
style: IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
),
|
||||
onPressed: min == null || count - 1 >= min!
|
||||
? () {
|
||||
if (count > 0) {
|
||||
update(count - 1);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
iconSize: 24.0,
|
||||
style: max == null || count + 1 <= max!
|
||||
? IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
)
|
||||
: null,
|
||||
onPressed: max == null || count + 1 <= max!
|
||||
? () {
|
||||
update(count + 1);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,712 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card_row.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivitySuggestionDialogContent extends StatelessWidget {
|
||||
final ActivitySuggestionDialogState controller;
|
||||
|
||||
const ActivitySuggestionDialogContent({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (controller.widget.controller.launchState) {
|
||||
case ActivityLaunchState.base:
|
||||
return _ActivitySuggestionBaseContent(controller: controller);
|
||||
case ActivityLaunchState.editing:
|
||||
return _ActivitySuggestionEditContent(controller: controller);
|
||||
case ActivityLaunchState.launching:
|
||||
return _ActivitySuggestionLaunchContent(controller: controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySuggestionDialogImage extends StatelessWidget {
|
||||
final ActivityPlannerBuilderState activityController;
|
||||
final double width;
|
||||
|
||||
const _ActivitySuggestionDialogImage({
|
||||
required this.activityController,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
width: (width / 2) + 42.0,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: activityController.avatar != null
|
||||
? Image.memory(
|
||||
activityController.avatar!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: activityController.updatedActivity.imageURL != null
|
||||
? activityController.updatedActivity.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityController.updatedActivity.imageURL!,
|
||||
),
|
||||
width: width / 2,
|
||||
height: 200,
|
||||
cacheKey: activityController.updatedActivity.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityController.updatedActivity.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (
|
||||
context,
|
||||
url,
|
||||
) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySuggestionDialogFrame extends StatelessWidget {
|
||||
final Widget topContent;
|
||||
final List<Widget> centerContent;
|
||||
final Widget bottomContent;
|
||||
|
||||
const _ActivitySuggestionDialogFrame({
|
||||
required this.topContent,
|
||||
required this.centerContent,
|
||||
required this.bottomContent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
topContent,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 14.0,
|
||||
children: centerContent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: bottomContent,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySuggestionBaseContent extends StatelessWidget {
|
||||
final ActivitySuggestionDialogState controller;
|
||||
|
||||
const _ActivitySuggestionBaseContent({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
ActivityPlannerBuilderState get activityController =>
|
||||
controller.widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
final topContent = _ActivitySuggestionDialogImage(
|
||||
activityController: activityController,
|
||||
width: controller.width,
|
||||
);
|
||||
|
||||
final centerContent = [
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.event_note_outlined,
|
||||
child: Text(
|
||||
activityController.updatedActivity.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.target,
|
||||
child: Text(
|
||||
activityController.updatedActivity.learningObjective,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.steps,
|
||||
child: Text(
|
||||
activityController.updatedActivity.instructions,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.group_outlined,
|
||||
child: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
activityController.updatedActivity.req.numberOfParticipants,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.school_outlined,
|
||||
child: Text(
|
||||
activityController.updatedActivity.req.cefrLevel.title(context),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.dictionary,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 60.0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: activityController.vocab
|
||||
.map(
|
||||
(vocab) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withAlpha(
|
||||
20,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
vocab.lemma,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final bottomContent = Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
onPressed: activityController.startEditing,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).edit,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.widget.replaceActivity != null)
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
onPressed: controller.onRegenerate,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).regenerate,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
// onPressed: _launchActivity,
|
||||
onPressed: () {
|
||||
activityController.setLaunchState(
|
||||
ActivityLaunchState.launching,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.save_outlined),
|
||||
Text(
|
||||
controller.widget.buttonText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return _ActivitySuggestionDialogFrame(
|
||||
topContent: topContent,
|
||||
centerContent: centerContent,
|
||||
bottomContent: bottomContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySuggestionEditContent extends StatelessWidget {
|
||||
final ActivitySuggestionDialogState controller;
|
||||
|
||||
const _ActivitySuggestionEditContent({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
ActivityPlannerBuilderState get activityController =>
|
||||
controller.widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
final topContent = Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
_ActivitySuggestionDialogImage(
|
||||
activityController: activityController,
|
||||
width: controller.width,
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
onTap: activityController.selectAvatar,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
radius: 20.0,
|
||||
child: Icon(
|
||||
Icons.add_a_photo_outlined,
|
||||
size: 20.0,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final centerContent = [
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.event_note_outlined,
|
||||
child: TextFormField(
|
||||
controller: activityController.titleController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).activityTitle,
|
||||
),
|
||||
maxLines: 2,
|
||||
minLines: 1,
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.target,
|
||||
child: TextFormField(
|
||||
controller: activityController.learningObjectivesController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).learningObjectiveLabel,
|
||||
),
|
||||
maxLines: 4,
|
||||
minLines: 1,
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.steps,
|
||||
child: TextFormField(
|
||||
controller: activityController.instructionsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).instructions,
|
||||
),
|
||||
maxLines: 8,
|
||||
minLines: 1,
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.group_outlined,
|
||||
child: TextFormField(
|
||||
controller: activityController.participantsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).classRoster,
|
||||
),
|
||||
maxLines: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final val = int.parse(value);
|
||||
if (val <= 0) {
|
||||
return L10n.of(context).pleaseEnterInt;
|
||||
}
|
||||
if (val > 50) {
|
||||
return L10n.of(context).maxFifty;
|
||||
}
|
||||
} catch (e) {
|
||||
return L10n.of(context).pleaseEnterANumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.school_outlined,
|
||||
child: LanguageLevelDropdown(
|
||||
initialLevel: activityController.languageLevel,
|
||||
onChanged: activityController.setLanguageLevel,
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Symbols.dictionary,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 60.0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: activityController.vocab
|
||||
.mapIndexed(
|
||||
(i, vocab) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withAlpha(
|
||||
20,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
24.0,
|
||||
),
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => activityController.removeVocab(
|
||||
i,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
vocab.lemma,
|
||||
),
|
||||
const Icon(
|
||||
Icons.close,
|
||||
size: 12.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: activityController.vocabController,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(
|
||||
context,
|
||||
).addVocabulary,
|
||||
),
|
||||
maxLines: 1,
|
||||
onFieldSubmitted: (_) => activityController.addVocab(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(
|
||||
0.0,
|
||||
),
|
||||
constraints:
|
||||
const BoxConstraints(), // override default min size of 48px
|
||||
iconSize: 16.0,
|
||||
icon: const Icon(
|
||||
Icons.add_outlined,
|
||||
),
|
||||
onPressed: activityController.addVocab,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final bottomContent = Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
onPressed: activityController.saveEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.save),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).save,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
onPressed: activityController.clearEdits,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cancel),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).cancel,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return _ActivitySuggestionDialogFrame(
|
||||
topContent: topContent,
|
||||
centerContent: centerContent,
|
||||
bottomContent: bottomContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivitySuggestionLaunchContent extends StatelessWidget {
|
||||
final ActivitySuggestionDialogState controller;
|
||||
|
||||
const _ActivitySuggestionLaunchContent({
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
ActivityPlannerBuilderState get activityController =>
|
||||
controller.widget.controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topContent = Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Avatar(
|
||||
mxContent: activityController.room.avatar,
|
||||
name: activityController.room.getLocalizedDisplayname(
|
||||
MatrixLocals(
|
||||
L10n.of(context),
|
||||
),
|
||||
),
|
||||
size: (controller.width / 2),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
);
|
||||
|
||||
final centerContent = [
|
||||
ActivitySuggestionCardRow(
|
||||
leading: Avatar(
|
||||
mxContent: activityController.room.avatar,
|
||||
name: activityController.room.getLocalizedDisplayname(
|
||||
MatrixLocals(
|
||||
L10n.of(context),
|
||||
),
|
||||
),
|
||||
size: 24.0,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
activityController.room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
leading: activityController.updatedActivity.imageURL != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
child: activityController.updatedActivity.imageURL!
|
||||
.startsWith("mxc")
|
||||
? MxcImage(
|
||||
uri: Uri.parse(
|
||||
activityController.updatedActivity.imageURL!,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
cacheKey: activityController.updatedActivity.bookmarkId,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: CachedNetworkImage(
|
||||
imageUrl: activityController.updatedActivity.imageURL!,
|
||||
fit: BoxFit.cover,
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
placeholder: (
|
||||
context,
|
||||
url,
|
||||
) =>
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
errorWidget: (
|
||||
context,
|
||||
url,
|
||||
error,
|
||||
) =>
|
||||
const SizedBox(),
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.event_note_outlined,
|
||||
size: 24.0,
|
||||
),
|
||||
child: Text(
|
||||
activityController.updatedActivity.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.groups,
|
||||
child: Text(
|
||||
L10n.of(context).minimumActivityParticipants(
|
||||
activityController.updatedActivity.req.numberOfParticipants,
|
||||
),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
ActivitySuggestionCardRow(
|
||||
icon: Icons.radar,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).numberOfActivities,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
NumberCounter(
|
||||
count: activityController.numActivities,
|
||||
update: activityController.setNumActivities,
|
||||
min: 1,
|
||||
max: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final bottomContent = ElevatedButton(
|
||||
style: controller.buttonStyle,
|
||||
onPressed: controller.launchActivity,
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.send_outlined),
|
||||
Text(
|
||||
L10n.of(context).launchToSpace,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return _ActivitySuggestionDialogFrame(
|
||||
topContent: topContent,
|
||||
centerContent: centerContent,
|
||||
bottomContent: bottomContent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,12 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
|
||||
class ActivitySuggestionsArea extends StatefulWidget {
|
||||
final Axis? scrollDirection;
|
||||
final Room? room;
|
||||
final Room room;
|
||||
|
||||
const ActivitySuggestionsArea({
|
||||
super.key,
|
||||
this.scrollDirection,
|
||||
this.room,
|
||||
required this.room,
|
||||
});
|
||||
@override
|
||||
ActivitySuggestionsAreaState createState() => ActivitySuggestionsAreaState();
|
||||
|
|
@ -173,7 +173,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
|
|||
builder: (controller) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: controller,
|
||||
buttonText: L10n.of(context).launchActivityButton,
|
||||
buttonText: L10n.of(context).saveAndLaunch,
|
||||
replaceActivity: (a) =>
|
||||
_onReplaceActivity(index, a),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ class ActivitySuggestionsConstants {
|
|||
static const String crayonIconPath = "make_your_own_icon.svg";
|
||||
static const String modeImageFileStart = "activityplanner_mode_";
|
||||
static const String makeActivityAssetPath = "Spark+imaginative.png";
|
||||
static const String endActivityAssetPath = "EndActivityMsg.png";
|
||||
}
|
||||
|
|
|
|||
55
lib/pangea/activity_summary/activity_summary_repo.dart
Normal file
55
lib/pangea/activity_summary/activity_summary_repo.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivitySummaryRepo {
|
||||
static final GetStorage _activitySummaryStorage =
|
||||
GetStorage('activity_summary_storage');
|
||||
|
||||
static void set(
|
||||
ActivitySummaryRequestModel request,
|
||||
ActivitySummaryResponseModel response,
|
||||
) {
|
||||
_activitySummaryStorage.write(_storageKey(request), response.toJson());
|
||||
}
|
||||
|
||||
static String _storageKey(ActivitySummaryRequestModel request) {
|
||||
// You may want to customize this key based on request fields
|
||||
return request.activity.hashCode.toString();
|
||||
}
|
||||
|
||||
static Future<ActivitySummaryResponseModel> get(
|
||||
ActivitySummaryRequestModel request,
|
||||
) async {
|
||||
final cachedJson = _activitySummaryStorage.read(_storageKey(request));
|
||||
if (cachedJson != null) {
|
||||
final cached = ActivitySummaryResponseModel.fromJson(cachedJson);
|
||||
return cached;
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.activitySummary,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = ActivitySummaryResponseModel.fromJson(decodedBody);
|
||||
|
||||
set(request, response);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Add this import for the participant summary model
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart';
|
||||
|
||||
class ActivitySummaryResultsMessage {
|
||||
final String userId;
|
||||
final String sent;
|
||||
final String? written;
|
||||
final List<String> tool;
|
||||
final DateTime time;
|
||||
|
||||
ActivitySummaryResultsMessage({
|
||||
required this.userId,
|
||||
required this.sent,
|
||||
this.written,
|
||||
required this.tool,
|
||||
required this.time,
|
||||
});
|
||||
|
||||
factory ActivitySummaryResultsMessage.fromJson(Map<String, dynamic> json) {
|
||||
return ActivitySummaryResultsMessage(
|
||||
userId: json['user_id'] as String,
|
||||
sent: json['sent'] as String,
|
||||
written: json['written'] as String?,
|
||||
tool: (json['tool'] as List).map((e) => e as String).toList(),
|
||||
time: DateTime.parse(json['time'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_id': userId,
|
||||
'sent': sent,
|
||||
if (written != null) 'written': written,
|
||||
'tool': tool,
|
||||
'time': time.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ContentFeedbackModel {
|
||||
final String feedback;
|
||||
final ActivitySummaryResponseModel content;
|
||||
|
||||
ContentFeedbackModel({
|
||||
required this.feedback,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
factory ContentFeedbackModel.fromJson(Map<String, dynamic> json) {
|
||||
return ContentFeedbackModel(
|
||||
feedback: json['feedback'] as String,
|
||||
content: ActivitySummaryResponseModel.fromJson(json['content']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'feedback': feedback,
|
||||
'content': content.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ActivitySummaryRequestModel {
|
||||
final ActivityPlanModel activity;
|
||||
final List<ActivitySummaryResultsMessage> activityResults;
|
||||
final List<ContentFeedbackModel> contentFeedback;
|
||||
|
||||
ActivitySummaryRequestModel({
|
||||
required this.activity,
|
||||
required this.activityResults,
|
||||
required this.contentFeedback,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity': activity.toJson(),
|
||||
'activity_results': activityResults.map((e) => e.toJson()).toList(),
|
||||
'content_feedback': contentFeedback.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
factory ActivitySummaryRequestModel.fromJson(Map<String, dynamic> json) {
|
||||
return ActivitySummaryRequestModel(
|
||||
activity: ActivityPlanModel.fromJson(json['activity']),
|
||||
activityResults: (json['activity_results'] as List)
|
||||
.map((e) => ActivitySummaryResultsMessage.fromJson(e))
|
||||
.toList(),
|
||||
contentFeedback: (json['content_feedback'] as List)
|
||||
.map((e) => ContentFeedbackModel.fromJson(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
class ParticipantSummaryModel {
|
||||
final String participantId;
|
||||
final String feedback;
|
||||
final String cefrLevel;
|
||||
final List<String> superlatives;
|
||||
|
||||
ParticipantSummaryModel({
|
||||
required this.participantId,
|
||||
required this.feedback,
|
||||
required this.cefrLevel,
|
||||
required this.superlatives,
|
||||
});
|
||||
|
||||
factory ParticipantSummaryModel.fromJson(Map<String, dynamic> json) {
|
||||
return ParticipantSummaryModel(
|
||||
participantId: json['participant_id'] as String,
|
||||
feedback: json['feedback'] as String,
|
||||
cefrLevel: json['cefr_level'] as String,
|
||||
superlatives:
|
||||
(json['superlatives'] as List).map((e) => e as String).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'participant_id': participantId,
|
||||
'feedback': feedback,
|
||||
'cefr_level': cefrLevel,
|
||||
'superlatives': superlatives,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ActivitySummaryResponseModel {
|
||||
final List<ParticipantSummaryModel> participants;
|
||||
final String summary;
|
||||
|
||||
ActivitySummaryResponseModel({
|
||||
required this.participants,
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
factory ActivitySummaryResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return ActivitySummaryResponseModel(
|
||||
participants: (json['participants'] as List)
|
||||
.map((e) => ParticipantSummaryModel.fromJson(e))
|
||||
.toList(),
|
||||
summary: json['summary'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'participants': participants.map((e) => e.toJson()).toList(),
|
||||
'summary': summary,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,11 @@ class GetAnalyticsController extends BaseController {
|
|||
data: {},
|
||||
);
|
||||
} finally {
|
||||
_updateAnalyticsStream(points: 0, newConstructs: []);
|
||||
_updateAnalyticsStream(
|
||||
type: AnalyticsUpdateType.local,
|
||||
points: 0,
|
||||
newConstructs: [],
|
||||
);
|
||||
if (!initCompleter.isCompleted) initCompleter.complete();
|
||||
_initializing = false;
|
||||
}
|
||||
|
|
@ -130,7 +134,18 @@ class GetAnalyticsController extends BaseController {
|
|||
Future<void> _onAnalyticsUpdate(
|
||||
AnalyticsUpdate analyticsUpdate,
|
||||
) async {
|
||||
final validTypes = [AnalyticsUpdateType.local, AnalyticsUpdateType.server];
|
||||
if (!validTypes.contains(analyticsUpdate.type)) {
|
||||
_updateAnalyticsStream(
|
||||
type: analyticsUpdate.type,
|
||||
points: 0,
|
||||
newConstructs: [],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (analyticsUpdate.isLogout) return;
|
||||
|
||||
final oldLevel = constructListModel.level;
|
||||
|
||||
final offset =
|
||||
|
|
@ -176,6 +191,7 @@ class GetAnalyticsController extends BaseController {
|
|||
_onUnlockMorphLemmas(newUnlockedMorphs);
|
||||
}
|
||||
_updateAnalyticsStream(
|
||||
type: analyticsUpdate.type,
|
||||
points: analyticsUpdate.newConstructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.xp,
|
||||
|
|
@ -193,12 +209,14 @@ class GetAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
void _updateAnalyticsStream({
|
||||
required AnalyticsUpdateType type,
|
||||
required int points,
|
||||
required List<ConstructIdentifier> newConstructs,
|
||||
String? targetID,
|
||||
}) =>
|
||||
analyticsStream.add(
|
||||
AnalyticsStreamUpdate(
|
||||
type: type,
|
||||
points: points,
|
||||
newConstructs: newConstructs,
|
||||
targetID: targetID,
|
||||
|
|
@ -570,6 +588,13 @@ class GetAnalyticsController extends BaseController {
|
|||
|
||||
return summary;
|
||||
}
|
||||
|
||||
List<Room> get archivedActivities {
|
||||
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
|
||||
if (analyticsRoom == null) return [];
|
||||
final ids = analyticsRoom.activityRoomIds;
|
||||
return ids.map((id) => _client.getRoomById(id)).whereType<Room>().toList();
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsCacheEntry {
|
||||
|
|
@ -602,11 +627,13 @@ class AnalyticsCacheEntry {
|
|||
}
|
||||
|
||||
class AnalyticsStreamUpdate {
|
||||
final AnalyticsUpdateType type;
|
||||
final int points;
|
||||
final List<ConstructIdentifier> newConstructs;
|
||||
final String? targetID;
|
||||
|
||||
AnalyticsStreamUpdate({
|
||||
required this.type,
|
||||
required this.points,
|
||||
required this.newConstructs,
|
||||
this.targetID,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
|||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum AnalyticsUpdateType { server, local }
|
||||
enum AnalyticsUpdateType { server, local, activities }
|
||||
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
|
|
@ -24,6 +24,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
late PangeaController _pangeaController;
|
||||
StreamController<AnalyticsUpdate> analyticsUpdateStream =
|
||||
StreamController.broadcast();
|
||||
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
StreamSubscription? _languageStream;
|
||||
Timer? _updateTimer;
|
||||
|
|
@ -414,6 +415,41 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
_pangeaController.getAnalytics.locallyCachedSentConstructs,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sendActivityAnalytics(String roomId) async {
|
||||
if (_pangeaController.matrixState.client.userID == null) return;
|
||||
if (_pangeaController.languageController.userL2 == null) return;
|
||||
|
||||
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(
|
||||
_pangeaController.languageController.userL2!,
|
||||
);
|
||||
if (analyticsRoom == null) return;
|
||||
await analyticsRoom.addActivityRoomId(roomId);
|
||||
|
||||
analyticsUpdateStream.add(
|
||||
AnalyticsUpdate(
|
||||
AnalyticsUpdateType.activities,
|
||||
[],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeActivityAnalytics(String roomId) async {
|
||||
if (_pangeaController.matrixState.client.userID == null) return;
|
||||
if (_pangeaController.languageController.userL2 == null) return;
|
||||
|
||||
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(
|
||||
_pangeaController.languageController.userL2!,
|
||||
);
|
||||
if (analyticsRoom == null) return;
|
||||
await analyticsRoom.removeActivityRoomId(roomId);
|
||||
analyticsUpdateStream.add(
|
||||
AnalyticsUpdate(
|
||||
AnalyticsUpdateType.activities,
|
||||
[],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsStream {
|
||||
|
|
|
|||
|
|
@ -268,4 +268,51 @@ extension AnalyticsRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> get activityRoomIds {
|
||||
final state = getState(PangeaEventTypes.activityRoomIds);
|
||||
if (state?.content[ModelKey.roomIds] is List) {
|
||||
return List<String>.from(state!.content[ModelKey.roomIds] as List);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> addActivityRoomId(String roomId) async {
|
||||
final List<String> ids = List.from(activityRoomIds);
|
||||
if (ids.contains(roomId)) return;
|
||||
|
||||
final prevLength = ids.length;
|
||||
ids.add(roomId);
|
||||
|
||||
final syncFuture = client.waitForRoomInSync(id, join: true);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRoomIds,
|
||||
"",
|
||||
{ModelKey.roomIds: ids},
|
||||
);
|
||||
final newLength = activityRoomIds.length;
|
||||
if (newLength == prevLength) {
|
||||
await syncFuture;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeActivityRoomId(String roomId) async {
|
||||
final List<String> ids = List.from(activityRoomIds);
|
||||
if (!ids.contains(roomId)) return;
|
||||
final prevLength = ids.length;
|
||||
ids.remove(roomId);
|
||||
|
||||
final syncFuture = client.waitForRoomInSync(id, join: true);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityRoomIds,
|
||||
"",
|
||||
{ModelKey.roomIds: ids},
|
||||
);
|
||||
final newLength = activityRoomIds.length;
|
||||
if (newLength == prevLength) {
|
||||
await syncFuture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
lib/pangea/analytics_page/activity_archive.dart
Normal file
29
lib/pangea/analytics_page/activity_archive.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_page/activity_archive_view.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ActivityArchive extends StatefulWidget {
|
||||
const ActivityArchive({super.key});
|
||||
|
||||
@override
|
||||
ActivityArchiveState createState() => ActivityArchiveState();
|
||||
}
|
||||
|
||||
class ActivityArchiveState extends State<ActivityArchive> {
|
||||
List<Room> get archive =>
|
||||
MatrixState.pangeaController.getAnalytics.archivedActivities;
|
||||
|
||||
Future<void> removeArchivedChat(Room room) async {
|
||||
await room.leave();
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.removeActivityAnalytics(room.id);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ActivityArchiveView(controller: this);
|
||||
}
|
||||
48
lib/pangea/analytics_page/activity_archive_view.dart
Normal file
48
lib/pangea/analytics_page/activity_archive_view.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class ActivityArchiveView extends StatelessWidget {
|
||||
final ActivityArchiveState controller;
|
||||
const ActivityArchiveView({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
if (controller.archive.isEmpty) {
|
||||
return const Center(
|
||||
child: Icon(Icons.archive_outlined, size: 80),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int i) => ChatListItem(
|
||||
controller.archive[i],
|
||||
onForget: () {
|
||||
showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => controller.removeArchivedChat(
|
||||
controller.archive[i],
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () =>
|
||||
context.go('/rooms/analytics/${controller.archive[i].id}'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/analytics_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart';
|
||||
|
|
@ -67,6 +68,9 @@ class AnalyticsPageView extends StatelessWidget {
|
|||
constructZoom: controller.widget.constructZoom,
|
||||
view: ConstructTypeEnum.vocab,
|
||||
);
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.activities) {
|
||||
return const ActivityArchive();
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/get_analytics_controller.dart';
|
||||
|
|
@ -99,6 +100,8 @@ class LearningProgressIndicatorsState
|
|||
final userL1 = MatrixState.pangeaController.languageController.userL1;
|
||||
final userL2 = MatrixState.pangeaController.languageController.userL2;
|
||||
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
@ -111,24 +114,53 @@ class LearningProgressIndicatorsState
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
spacing: 16.0,
|
||||
children: ConstructTypeEnum.values
|
||||
.map(
|
||||
(c) => HoverButton(
|
||||
selected: widget.selected == c.indicator,
|
||||
onPressed: () {
|
||||
context.go(
|
||||
"/rooms/analytics?mode=${c.string}",
|
||||
);
|
||||
},
|
||||
child: ProgressIndicatorBadge(
|
||||
indicator: c.indicator,
|
||||
loading: _loading,
|
||||
points: uniqueLemmas(c.indicator),
|
||||
),
|
||||
spacing: isColumnMode ? 16.0 : 4.0,
|
||||
children: [
|
||||
...ConstructTypeEnum.values.map(
|
||||
(c) => HoverButton(
|
||||
selected: widget.selected == c.indicator,
|
||||
onPressed: () {
|
||||
context.go(
|
||||
"/rooms/analytics?mode=${c.string}",
|
||||
);
|
||||
},
|
||||
child: ProgressIndicatorBadge(
|
||||
indicator: c.indicator,
|
||||
loading: _loading,
|
||||
points: uniqueLemmas(c.indicator),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
HoverButton(
|
||||
selected: widget.selected ==
|
||||
ProgressIndicatorEnum.activities,
|
||||
onPressed: () {
|
||||
context.go(
|
||||
"/rooms/analytics?mode=activities",
|
||||
);
|
||||
},
|
||||
child: Tooltip(
|
||||
message: ProgressIndicatorEnum.activities
|
||||
.tooltip(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
size: 18,
|
||||
Icons.radar,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
weight: 1000,
|
||||
),
|
||||
const SizedBox(width: 6.0),
|
||||
AnimatedFloatingNumber(
|
||||
number: MatrixState.pangeaController
|
||||
.getAnalytics.archivedActivities.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
HoverButton(
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@ class ProgressIndicatorBadge extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 6.0),
|
||||
!loading
|
||||
? _AnimatedFloatingNumber(
|
||||
? AnimatedFloatingNumber(
|
||||
number: points,
|
||||
indicator: indicator,
|
||||
)
|
||||
: const SizedBox(
|
||||
height: 8,
|
||||
|
|
@ -47,21 +46,19 @@ class ProgressIndicatorBadge extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _AnimatedFloatingNumber extends StatefulWidget {
|
||||
class AnimatedFloatingNumber extends StatefulWidget {
|
||||
final int number;
|
||||
final ProgressIndicatorEnum indicator;
|
||||
|
||||
const _AnimatedFloatingNumber({
|
||||
const AnimatedFloatingNumber({
|
||||
super.key,
|
||||
required this.number,
|
||||
required this.indicator,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AnimatedFloatingNumber> createState() =>
|
||||
_AnimatedFloatingNumberState();
|
||||
State<AnimatedFloatingNumber> createState() => AnimatedFloatingNumberState();
|
||||
}
|
||||
|
||||
class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber>
|
||||
class AnimatedFloatingNumberState extends State<AnimatedFloatingNumber>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _fadeAnim;
|
||||
|
|
@ -85,7 +82,7 @@ class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber>
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _AnimatedFloatingNumber oldWidget) {
|
||||
void didUpdateWidget(covariant AnimatedFloatingNumber oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.number > _lastNumber!) {
|
||||
_floatingNumber = widget.number;
|
||||
|
|
@ -109,7 +106,7 @@ class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber>
|
|||
final TextStyle indicatorStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.indicator.color(context),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
|||
enum ProgressIndicatorEnum {
|
||||
level,
|
||||
wordsUsed,
|
||||
morphsUsed;
|
||||
morphsUsed,
|
||||
activities;
|
||||
|
||||
static ProgressIndicatorEnum? fromString(String value) {
|
||||
switch (value) {
|
||||
|
|
@ -18,6 +19,8 @@ enum ProgressIndicatorEnum {
|
|||
return ProgressIndicatorEnum.morphsUsed;
|
||||
case 'level':
|
||||
return ProgressIndicatorEnum.level;
|
||||
case 'activities':
|
||||
return ProgressIndicatorEnum.activities;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
@ -33,6 +36,8 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
|
|||
return Symbols.toys_and_games;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return Icons.star;
|
||||
case ProgressIndicatorEnum.activities:
|
||||
return Icons.radar;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +56,8 @@ extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
|
|||
return L10n.of(context).level;
|
||||
case ProgressIndicatorEnum.morphsUsed:
|
||||
return L10n.of(context).grammar;
|
||||
case ProgressIndicatorEnum.activities:
|
||||
return L10n.of(context).activities;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ class RoomDefaults {
|
|||
"redact": 50,
|
||||
"events": {
|
||||
PangeaEventTypes.activityPlan: 0,
|
||||
PangeaEventTypes.activityRole: 0,
|
||||
PangeaEventTypes.activitySummary: 0,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.pinned_events": 50,
|
||||
},
|
||||
|
|
@ -38,6 +40,8 @@ class RoomDefaults {
|
|||
"redact": 50,
|
||||
"events": {
|
||||
PangeaEventTypes.activityPlan: 50,
|
||||
PangeaEventTypes.activityRole: 0,
|
||||
PangeaEventTypes.activitySummary: 0,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.pinned_events": 50,
|
||||
},
|
||||
|
|
|
|||
63
lib/pangea/chat/widgets/activity_role_state_message.dart
Normal file
63
lib/pangea/chat/widgets/activity_role_state_message.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class ActivityRoleStateMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
const ActivityRoleStateMessage(this.event, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final senderName = event.senderId == event.room.client.userID
|
||||
? L10n.of(context).you
|
||||
: event.senderFromMemoryOrFallback.calcDisplayname();
|
||||
|
||||
String role = L10n.of(context).participant;
|
||||
bool finished = false;
|
||||
|
||||
try {
|
||||
final roleContent = event.content['role'] as String?;
|
||||
if (roleContent != null) {
|
||||
role = roleContent;
|
||||
}
|
||||
|
||||
finished = event.content['finishedAt'] != null;
|
||||
} catch (e) {
|
||||
// If the role is not found, we keep the default participant role.
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Material(
|
||||
color: theme.colorScheme.surface.withAlpha(128),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 3),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Text(
|
||||
finished
|
||||
? L10n.of(context).finishedTheActivity(senderName)
|
||||
: L10n.of(context).joinedTheActivity(senderName, role),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12 * AppConfig.fontSizeFactor,
|
||||
decoration:
|
||||
event.redacted ? TextDecoration.lineThrough : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
47
lib/pangea/chat/widgets/activity_state_event.dart
Normal file
47
lib/pangea/chat/widgets/activity_state_event.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/events/state_message.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_participant_indicator.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
|
||||
class ActivityStateEvent extends StatelessWidget {
|
||||
final Event event;
|
||||
const ActivityStateEvent({super.key, required this.event});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
final activity = ActivityPlanModel.fromJson(event.content);
|
||||
final roles = event.room.activityRoles;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24.0,
|
||||
vertical: 16.0,
|
||||
),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(activity.markdown),
|
||||
if (roles.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 12.0,
|
||||
runSpacing: 12.0,
|
||||
children: event.room.activityRoles.map((role) {
|
||||
return ActivityParticipantIndicator(
|
||||
role: role,
|
||||
displayname: role.userId.localpart,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return StateMessage(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -321,9 +321,9 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
title: l10n.activities,
|
||||
icon: const Icon(Icons.event_note_outlined, size: 30.0),
|
||||
onPressed: () => context.go("/rooms/${room.id}/details/planner"),
|
||||
visible: room.canChangeStateEvent(PangeaEventTypes.activityPlan) ||
|
||||
visible: room.isSpace,
|
||||
enabled: room.canChangeStateEvent(PangeaEventTypes.activityPlan) &&
|
||||
room.isSpace,
|
||||
enabled: room.canChangeStateEvent(PangeaEventTypes.activityPlan),
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.permissions,
|
||||
|
|
|
|||
|
|
@ -179,4 +179,5 @@ class ModelKey {
|
|||
"number_of_participants";
|
||||
|
||||
static const String autoIGC = "auto_igc";
|
||||
static const String roomIds = "room_ids";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class PangeaController {
|
|||
int? randomint;
|
||||
PangeaController({required this.matrix, required this.matrixState}) {
|
||||
_setup();
|
||||
_setLanguageStream();
|
||||
_setLanguageSubscription();
|
||||
randomint = Random().nextInt(2000);
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +184,7 @@ class PangeaController {
|
|||
// Initialize analytics data
|
||||
putAnalytics.initialize();
|
||||
getAnalytics.initialize();
|
||||
_setLanguageStream();
|
||||
_setLanguageSubscription();
|
||||
|
||||
userController.reinitialize().then((_) {
|
||||
final l1 = userController.profile.userSettings.sourceLanguage;
|
||||
|
|
@ -212,7 +212,7 @@ class PangeaController {
|
|||
await getAnalytics.initialize();
|
||||
}
|
||||
|
||||
void _setLanguageStream() {
|
||||
void _setLanguageSubscription() {
|
||||
_languageStream?.cancel();
|
||||
_languageStream = userController.languageStream.stream.listen(
|
||||
(_) => clearCache(exclude: ["analytics_storage"]),
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class PApiUrls {
|
|||
static String topicList = "${PApiUrls.choreoEndpoint}/topics";
|
||||
static String activityPlanSearch =
|
||||
"${PApiUrls.choreoEndpoint}/activity_plan/search";
|
||||
static String activitySummary = "${PApiUrls.choreoEndpoint}/activity_summary";
|
||||
|
||||
static String morphFeaturesAndTags = "${PApiUrls.choreoEndpoint}/morphs";
|
||||
static String constructSummary =
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class PangeaEventTypes {
|
|||
static const capacity = "pangea.capacity";
|
||||
|
||||
static const activityPlan = "pangea.activity_plan";
|
||||
static const activityRole = "pangea.activity_roles";
|
||||
static const activitySummary = "pangea.activity_summary";
|
||||
|
||||
static const userAge = "pangea.user_age";
|
||||
|
||||
|
|
@ -44,4 +46,6 @@ class PangeaEventTypes {
|
|||
|
||||
/// Profile information related to a user's analytics
|
||||
static const profileAnalytics = "pangea.analytics_profile";
|
||||
|
||||
static const activityRoomIds = "pangea.activity_room_ids";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,24 +9,27 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html_unescape/html_unescape.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:matrix/src/utils/markdown.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/activity_planner/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import '../choreographer/models/choreo_record.dart';
|
||||
import '../events/constants/pangea_event_types.dart';
|
||||
import '../events/models/representation_content_model.dart';
|
||||
|
|
|
|||
|
|
@ -94,8 +94,53 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
int get spaceChildCount => client.rooms
|
||||
.where(
|
||||
(r) => spaceChildren.any(
|
||||
(child) => r.id == child.roomId && !r.isAnalyticsRoom,
|
||||
(child) => r.id == child.roomId && !r.isHiddenRoom,
|
||||
),
|
||||
)
|
||||
.length;
|
||||
|
||||
Future<void> addSubspace(BuildContext context) async {
|
||||
if (!isSpace) return;
|
||||
final names = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).createNewSpace,
|
||||
hintText: L10n.of(context).spaceName,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
maxLength: 64,
|
||||
validator: (text) {
|
||||
if (text.isEmpty) {
|
||||
return L10n.of(context).pleaseChoose;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
okLabel: L10n.of(context).create,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
);
|
||||
if (names == null) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
await postLoad();
|
||||
final resp = await client.createRoom(
|
||||
name: names,
|
||||
visibility: RoomDefaults.spaceChildVisibility,
|
||||
creationContent: {'type': 'm.space'},
|
||||
initialState: [
|
||||
RoomDefaults.defaultSpacePowerLevels(client.userID!),
|
||||
await client.pangeaJoinRules(
|
||||
'knock_restricted',
|
||||
allow: [
|
||||
{
|
||||
"type": "m.room_membership",
|
||||
"room_id": id,
|
||||
}
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
await addToSpace(resp);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ extension EventsRoomExtension on Room {
|
|||
for (final child in spaceChildren) {
|
||||
if (child.roomId == null) continue;
|
||||
final Room? room = client.getRoomById(child.roomId!);
|
||||
if (room == null || room.isAnalyticsRoom) continue;
|
||||
if (room == null || room.isHiddenRoom) continue;
|
||||
try {
|
||||
await room.leave();
|
||||
} catch (e, s) {
|
||||
|
|
@ -274,78 +274,6 @@ extension EventsRoomExtension on Room {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> sendActivityPlan(
|
||||
ActivityPlanModel activity, {
|
||||
Uint8List? avatar,
|
||||
String? filename,
|
||||
}) async {
|
||||
BookmarkedActivitiesRepo.save(activity);
|
||||
|
||||
String? imageURL = activity.imageURL;
|
||||
final eventId = await pangeaSendTextEvent(
|
||||
activity.markdown,
|
||||
messageTag: ModelKey.messageTagActivityPlan,
|
||||
);
|
||||
|
||||
Uint8List? bytes = avatar;
|
||||
if (imageURL != null && bytes == null) {
|
||||
try {
|
||||
final resp = await http
|
||||
.get(Uri.parse(imageURL))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
bytes = resp.bodyBytes;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"avatarURL": imageURL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes != null && imageURL == null) {
|
||||
final url = await client.uploadContent(
|
||||
bytes,
|
||||
filename: filename,
|
||||
);
|
||||
imageURL = url.toString();
|
||||
}
|
||||
|
||||
MatrixFile? file;
|
||||
if (filename != null && bytes != null) {
|
||||
file = MatrixFile(
|
||||
bytes: bytes,
|
||||
name: filename,
|
||||
);
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
final content = <String, dynamic>{
|
||||
'msgtype': file.msgType,
|
||||
'body': file.name,
|
||||
'filename': file.name,
|
||||
'url': imageURL,
|
||||
ModelKey.messageTags: ModelKey.messageTagActivityPlan,
|
||||
};
|
||||
await sendEvent(content);
|
||||
}
|
||||
|
||||
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.activityPlan,
|
||||
"",
|
||||
activity.toJson(),
|
||||
);
|
||||
|
||||
if (eventId != null && canChangeStateEvent(EventTypes.RoomPinnedEvents)) {
|
||||
await setPinnedEvents([eventId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of events in the room that are of type [PangeaEventTypes.construct]
|
||||
/// and have the sender as [userID]. If [count] is provided, the function will
|
||||
/// return at most [count] events.
|
||||
|
|
|
|||
|
|
@ -34,4 +34,9 @@ extension RoomInformationRoomExtension on Room {
|
|||
bool get isAnalyticsRoom =>
|
||||
getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
|
||||
PangeaRoomTypes.analytics;
|
||||
|
||||
bool get isHiddenActivityRoom =>
|
||||
activityRole(client.userID!)?.isArchived ?? false;
|
||||
|
||||
bool get isHiddenRoom => isAnalyticsRoom || isHiddenActivityRoom;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,53 +14,10 @@ extension RoomSettingsRoomExtension on Room {
|
|||
return t is int ? t : null;
|
||||
}
|
||||
|
||||
IconData? get roomTypeIcon {
|
||||
if (membership == Membership.invite) return Icons.add;
|
||||
if (isSpace) return Icons.school;
|
||||
if (isAnalyticsRoom) return Icons.analytics;
|
||||
if (isDirectChat) return Icons.forum;
|
||||
return Icons.group;
|
||||
}
|
||||
|
||||
Text nameAndRoomTypeIcon([TextStyle? textStyle]) => Text.rich(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textStyle,
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Icon(roomTypeIcon),
|
||||
),
|
||||
TextSpan(
|
||||
text: ' $name',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
BotOptionsModel? get botOptions {
|
||||
if (isSpace) return null;
|
||||
final stateEvent = getState(PangeaEventTypes.botOptions);
|
||||
if (stateEvent == null) return null;
|
||||
return BotOptionsModel.fromJson(stateEvent.content);
|
||||
}
|
||||
|
||||
ActivityPlanModel? get activityPlan {
|
||||
final stateEvent = getState(PangeaEventTypes.activityPlan);
|
||||
if (stateEvent == null) return null;
|
||||
|
||||
try {
|
||||
return ActivityPlanModel.fromJson(stateEvent.content);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"roomID": id,
|
||||
"stateEvent": stateEvent.content,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
146
lib/pangea/spaces/widgets/space_view_appbar.dart
Normal file
146
lib/pangea/spaces/widgets/space_view_appbar.dart
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
|
||||
class SpaceViewAppbar extends StatelessWidget {
|
||||
final Function(SpaceActions) onSpaceAction;
|
||||
final VoidCallback onBack;
|
||||
final List<Room>? joinedParents;
|
||||
final Function(String) toParentSpace;
|
||||
final Room? room;
|
||||
|
||||
const SpaceViewAppbar({
|
||||
super.key,
|
||||
required this.onSpaceAction,
|
||||
required this.onBack,
|
||||
required this.toParentSpace,
|
||||
this.joinedParents,
|
||||
this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayname =
|
||||
room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onSpaceAction(SpaceActions.settings),
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: joinedParents?.isNotEmpty ?? false ? 0.0 : null,
|
||||
title: Row(
|
||||
children: [
|
||||
if (joinedParents?.isNotEmpty ?? false)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_outlined),
|
||||
onPressed: () => toParentSpace(joinedParents!.first.id),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
if (room != null)
|
||||
Text(
|
||||
L10n.of(context).countChatsAndCountParticipants(
|
||||
room!.spaceChildren.length,
|
||||
room!.summary.mJoinedMemberCount ?? 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<SpaceActions>(
|
||||
useRootNavigator: true,
|
||||
onSelected: onSpaceAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.settings,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.invite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).invite),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.groupChat,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.chat_add_on),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).groupChat),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.subspace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.add),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).subspace),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.logout_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).leave),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SpaceActions.delete,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.delete_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).delete),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +136,10 @@ abstract class ClientManager {
|
|||
PangeaEventTypes.userSetLemmaInfo,
|
||||
EventTypes.RoomJoinRules,
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityRole,
|
||||
PangeaEventTypes.activitySummary,
|
||||
PangeaEventTypes.constructSummary,
|
||||
PangeaEventTypes.activityRoomIds,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
|
|
@ -1,36 +1,13 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import '../../config/app_config.dart';
|
||||
|
||||
extension VisibleInGuiExtension on List<Event> {
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) {
|
||||
final visibleEvents =
|
||||
where((e) => e.isVisibleInGui || e.eventId == exceptionEventId)
|
||||
.toList();
|
||||
|
||||
// Hide creation state events:
|
||||
if (visibleEvents.isNotEmpty &&
|
||||
visibleEvents.last.type == EventTypes.RoomCreate) {
|
||||
var i = visibleEvents.length - 2;
|
||||
while (i > 0) {
|
||||
final event = visibleEvents[i];
|
||||
if (!event.isState) break;
|
||||
if (event.type == EventTypes.Encryption) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
if (event.type == EventTypes.RoomMember &&
|
||||
event.roomMemberChangeType == RoomMemberChangeType.acceptInvite) {
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
visibleEvents.removeAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return visibleEvents;
|
||||
}
|
||||
List<Event> filterByVisibleInGui({String? exceptionEventId}) => where(
|
||||
(event) => event.isVisibleInGui || event.eventId == exceptionEventId,
|
||||
).toList();
|
||||
}
|
||||
|
||||
extension IsStateExtension on Event {
|
||||
|
|
@ -46,7 +23,10 @@ extension IsStateExtension on Event {
|
|||
// if we enabled to hide all redacted events, don't show those
|
||||
(!AppConfig.hideRedactedEvents || !redacted) &&
|
||||
// if we enabled to hide all unknown events, don't show those
|
||||
(!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
// #Pangea
|
||||
// (!AppConfig.hideUnknownEvents || isEventTypeKnown) &&
|
||||
(!AppConfig.hideUnknownEvents || pangeaIsEventTypeKnown) &&
|
||||
// Pangea#
|
||||
// remove state events that we don't want to render
|
||||
(isState || !AppConfig.hideAllStateEvents) &&
|
||||
// #Pangea
|
||||
|
|
@ -75,6 +55,13 @@ extension IsStateExtension on Event {
|
|||
}.contains(type);
|
||||
|
||||
// #Pangea
|
||||
bool get pangeaIsEventTypeKnown =>
|
||||
isEventTypeKnown ||
|
||||
[
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityRole,
|
||||
].contains(type);
|
||||
|
||||
// we're filtering out some state events that we don't want to render
|
||||
static const Set<String> importantStateEvents = {
|
||||
EventTypes.Encryption,
|
||||
|
|
@ -82,6 +69,8 @@ extension IsStateExtension on Event {
|
|||
EventTypes.RoomMember,
|
||||
EventTypes.RoomTombstone,
|
||||
EventTypes.CallInvite,
|
||||
PangeaEventTypes.activityPlan,
|
||||
PangeaEventTypes.activityRole,
|
||||
};
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class _ShareScaffoldDialogState extends State<ShareScaffoldDialog> {
|
|||
room.canSendDefaultMessages &&
|
||||
!room.isSpace &&
|
||||
// #Pangea
|
||||
!room.isAnalyticsRoom &&
|
||||
!room.isHiddenRoom &&
|
||||
// Pangea#
|
||||
room.membership == Membership.join,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class UnreadRoomsBadge extends StatelessWidget {
|
|||
.client
|
||||
.rooms
|
||||
// #Pangea
|
||||
.where((r) => !r.isAnalyticsRoom)
|
||||
.where((r) => !r.isHiddenRoom)
|
||||
// Pangea#
|
||||
.where(filter)
|
||||
.where((r) => (r.isUnread || r.membership == Membership.invite))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue