feat: course planner
This commit is contained in:
parent
b5fe810aa1
commit
f9ee134cdd
70 changed files with 12301 additions and 3201 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -3387,7 +3387,7 @@
|
|||
"chatTopic": "Chat topic",
|
||||
"chatTopicDesc": "Set a chat topic",
|
||||
"inviteStudentByUserNameDesc": "If your student already has an account, you can search for them.",
|
||||
"classRoster": "Participants",
|
||||
"participants": "Participants",
|
||||
"almostPerfect": "That seems right! Here's what I would have said.",
|
||||
"prettyGood": "Pretty good! Here's what I would have said.",
|
||||
"letMeThink": "Hmm, let's see how you did!",
|
||||
|
|
@ -5192,5 +5192,54 @@
|
|||
}
|
||||
},
|
||||
"activityFinishedMessage": "All Finished!",
|
||||
"endForAll": "End for all"
|
||||
"endForAll": "End for all",
|
||||
"newCourse": "New Course",
|
||||
"newCourseSubtitle": "Which course template would you like to use?",
|
||||
"failedToLoadCourses": "Failed to load courses",
|
||||
"numModules": "{num} modules",
|
||||
"@numModules": {
|
||||
"type": "int",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"numActivityPlans": "{num} activity plans",
|
||||
"@numActivityPlans": {
|
||||
"type": "int",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"coursePlan": "Course Plan",
|
||||
"editCourseLater": "You can edit template title, descriptions, and course image later.",
|
||||
"newCourseAccess": "By default, courses are private and require admin approval to join. You can edit these settings at any time.",
|
||||
"createCourse": "Create course",
|
||||
"stats": "Stats",
|
||||
"createGroupChat": "Create group chat",
|
||||
"editCourse": "Edit course",
|
||||
"inviteDesc": "By username, by code or link",
|
||||
"editCourseDesc": "Here you can edit course title, description, etc.",
|
||||
"permissionsDesc": "Set permissions such as who can invite users, send messages, create chats, etc.",
|
||||
"accessDesc": "You can make your course open to the world! Or, make your course private and secure.",
|
||||
"createGroupChatDesc": "Whereas activity sessions start and end, group chats will stay open for routine communication.",
|
||||
"deleteDesc": "Only space admin can delete a chat. This is a destructive action which removes all users and deletes all chats. Proceed with caution.",
|
||||
"failedToLoadCourseInfo": "Failed to load course information",
|
||||
"noCourseFound": "No course information found",
|
||||
"additionalParticipants": "+ {num} others",
|
||||
"@additionalParticipants": {
|
||||
"type": "int",
|
||||
"placeholders": {
|
||||
"num": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"activityNotFoundForCourse": "This activity was not found within the course",
|
||||
"courseChats": "Course Chats",
|
||||
"noSessionsFound": "None found. Ready to start?",
|
||||
"myActivitySessions": "My Activity Sessions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,19 @@ import 'package:flutter/material.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
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/pages/pangea_room_details.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/download/download_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/download/download_type_enum.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';
|
||||
|
|
@ -221,7 +224,7 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
|
||||
@override
|
||||
// #Pangea
|
||||
Widget build(BuildContext context) => PangeaChatDetailsView(this);
|
||||
Widget build(BuildContext context) => PangeaRoomDetailsView(this);
|
||||
// Widget build(BuildContext context) => ChatDetailsView(this);
|
||||
// Pangea#
|
||||
|
||||
|
|
@ -343,10 +346,69 @@ class ChatDetailsController extends State<ChatDetails> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> addSubspace() async {
|
||||
Future<void> addGroupChat() async {
|
||||
final activeSpace = Matrix.of(context).client.getRoomById(roomId!);
|
||||
if (activeSpace == null || !activeSpace.isSpace) return;
|
||||
await activeSpace.addSubspace(context);
|
||||
|
||||
final names = await showTextInputDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).createGroup,
|
||||
hintText: L10n.of(context).groupName,
|
||||
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 resp = await showFutureLoadingDialog<String>(
|
||||
context: context,
|
||||
future: () async {
|
||||
final newRoomId = await Matrix.of(context).client.createGroupChat(
|
||||
visibility: sdk.Visibility.private,
|
||||
groupName: names,
|
||||
initialState: [
|
||||
RoomDefaults.defaultPowerLevels(
|
||||
Matrix.of(context).client.userID!,
|
||||
),
|
||||
await Matrix.of(context).client.pangeaJoinRules(
|
||||
'knock_restricted',
|
||||
allow: roomId != null
|
||||
? [
|
||||
{
|
||||
"type": "m.room_membership",
|
||||
"room_id": roomId,
|
||||
}
|
||||
]
|
||||
: null,
|
||||
),
|
||||
],
|
||||
enableEncryption: false,
|
||||
);
|
||||
final client = Matrix.of(context).client;
|
||||
Room? room = client.getRoomById(newRoomId);
|
||||
if (room == null) {
|
||||
await client.waitForRoomInSync(newRoomId);
|
||||
room = client.getRoomById(newRoomId);
|
||||
}
|
||||
if (room == null) newRoomId;
|
||||
await activeSpace.addToSpace(room!.id);
|
||||
if (room.spaceParents.isEmpty) {
|
||||
await client.waitForRoomInSync(newRoomId);
|
||||
}
|
||||
return newRoomId;
|
||||
},
|
||||
);
|
||||
|
||||
if (resp.isError || resp.result == null || !mounted) return;
|
||||
context.go('/rooms/${resp.result}/invite');
|
||||
}
|
||||
// Pangea#
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ class ChatEncryptionSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
||||
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
// #Pangea
|
||||
// String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
String? get roomId {
|
||||
final pathParameters = GoRouterState.of(context).pathParameters;
|
||||
return pathParameters['roomid'] ?? pathParameters['spaceid'];
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:receive_sharing_intent/receive_sharing_intent.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_list/chat_list_view.dart';
|
||||
import 'package:fluffychat/pangea/chat_list/utils/app_version_util.dart';
|
||||
|
|
@ -113,33 +112,25 @@ class ChatListController extends State<ChatList>
|
|||
? ActiveFilter.messages
|
||||
: ActiveFilter.allChats;
|
||||
|
||||
String? _activeSpaceId;
|
||||
String? get activeSpaceId => _activeSpaceId;
|
||||
|
||||
void setActiveSpace(String spaceId) async {
|
||||
await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
|
||||
|
||||
// #Pangea
|
||||
if (FluffyThemes.isColumnMode(context)) {
|
||||
context.push("/rooms/$spaceId/details");
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
setState(() {
|
||||
_activeSpaceId = spaceId;
|
||||
});
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
String? get activeSpaceId => widget.activeSpaceId;
|
||||
// String? _activeSpaceId;
|
||||
// String? get activeSpaceId => _activeSpaceId;
|
||||
|
||||
// void setActiveSpace(String spaceId) async {
|
||||
// await Matrix.of(context).client.getRoomById(spaceId)!.postLoad();
|
||||
|
||||
// setState(() {
|
||||
// _activeSpaceId = spaceId;
|
||||
// });
|
||||
// }
|
||||
|
||||
// void clearActiveSpace() => setState(() {
|
||||
// _activeSpaceId = null;
|
||||
// });
|
||||
void clearActiveSpace() {
|
||||
setState(() {
|
||||
_activeSpaceId = null;
|
||||
});
|
||||
context.go("/rooms");
|
||||
}
|
||||
void clearActiveSpace() => context.go("/rooms");
|
||||
void setActiveSpace(String spaceId) =>
|
||||
context.go("/rooms/spaces/$spaceId/details");
|
||||
// Pangea#
|
||||
|
||||
void onChatTap(Room room) async {
|
||||
|
|
@ -524,9 +515,7 @@ class ChatListController extends State<ChatList>
|
|||
//#Pangea
|
||||
StreamSubscription? _invitedSpaceSubscription;
|
||||
StreamSubscription? _subscriptionStatusStream;
|
||||
StreamSubscription? _spaceChildSubscription;
|
||||
StreamSubscription? _roomCapacitySubscription;
|
||||
final Set<String> hasUpdates = {};
|
||||
//Pangea#
|
||||
|
||||
@override
|
||||
|
|
@ -625,11 +614,6 @@ class ChatListController extends State<ChatList>
|
|||
// so that when the user navigates to the space that was updated, it will
|
||||
// reload any rooms that have been added / removed
|
||||
final client = MatrixState.pangeaController.matrixState.client;
|
||||
_spaceChildSubscription ??= client.onRoomState.stream.where((u) {
|
||||
return u.state.type == EventTypes.SpaceChild && u.roomId != activeSpaceId;
|
||||
}).listen((update) {
|
||||
hasUpdates.add(update.roomId);
|
||||
});
|
||||
|
||||
// listen for room join events and leave room if over capacity
|
||||
_roomCapacitySubscription ??= client.onSync.stream
|
||||
|
|
@ -669,9 +653,6 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
});
|
||||
|
||||
_activeSpaceId =
|
||||
widget.activeSpaceId == 'clear' ? null : widget.activeSpaceId;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_joinInvitedSpaces();
|
||||
});
|
||||
|
|
@ -681,17 +662,6 @@ class ChatListController extends State<ChatList>
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
@override
|
||||
void didUpdateWidget(ChatList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.activeSpaceId != oldWidget.activeSpaceId &&
|
||||
widget.activeSpaceId != null) {
|
||||
widget.activeSpaceId == 'clear'
|
||||
? clearActiveSpace()
|
||||
: setActiveSpace(widget.activeSpaceId!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _joinInvitedSpaces() async {
|
||||
final invitedSpaces = Matrix.of(context).client.rooms.where(
|
||||
(r) => r.isSpace && r.membership == Membership.invite,
|
||||
|
|
@ -711,7 +681,6 @@ class ChatListController extends State<ChatList>
|
|||
//#Pangea
|
||||
_invitedSpaceSubscription?.cancel();
|
||||
_subscriptionStatusStream?.cancel();
|
||||
_spaceChildSubscription?.cancel();
|
||||
_roomCapacitySubscription?.cancel();
|
||||
//Pangea#
|
||||
scrollController.removeListener(_onScroll);
|
||||
|
|
@ -1097,7 +1066,7 @@ class ChatListController extends State<ChatList>
|
|||
builder: (_) => DeleteSpaceDialog(space: room),
|
||||
);
|
||||
if (resp == true && mounted) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
context.go("/rooms");
|
||||
}
|
||||
} else {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
|
|
@ -1118,7 +1087,9 @@ class ChatListController extends State<ChatList>
|
|||
future: room.delete,
|
||||
);
|
||||
if (mounted && !resp.isError) {
|
||||
context.go("/rooms");
|
||||
activeSpaceId != null
|
||||
? context.go('/rooms/spaces/$activeSpaceId/details')
|
||||
: context.go("/rooms");
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
@ -1257,29 +1228,31 @@ class ChatListController extends State<ChatList>
|
|||
});
|
||||
}
|
||||
|
||||
void setActiveClient(Client client) {
|
||||
context.go('/rooms');
|
||||
setState(() {
|
||||
activeFilter = ActiveFilter.allChats;
|
||||
_activeSpaceId = null;
|
||||
Matrix.of(context).setActiveClient(client);
|
||||
});
|
||||
_clientStream.add(client);
|
||||
}
|
||||
// #Pangea
|
||||
// void setActiveClient(Client client) {
|
||||
// context.go('/rooms');
|
||||
// setState(() {
|
||||
// activeFilter = ActiveFilter.allChats;
|
||||
// _activeSpaceId = null;
|
||||
// Matrix.of(context).setActiveClient(client);
|
||||
// });
|
||||
// _clientStream.add(client);
|
||||
// }
|
||||
|
||||
void setActiveBundle(String bundle) {
|
||||
context.go('/rooms');
|
||||
setState(() {
|
||||
_activeSpaceId = null;
|
||||
Matrix.of(context).activeBundle = bundle;
|
||||
if (!Matrix.of(context)
|
||||
.currentBundle!
|
||||
.any((client) => client == Matrix.of(context).client)) {
|
||||
Matrix.of(context)
|
||||
.setActiveClient(Matrix.of(context).currentBundle!.first);
|
||||
}
|
||||
});
|
||||
}
|
||||
// void setActiveBundle(String bundle) {
|
||||
// context.go('/rooms');
|
||||
// setState(() {
|
||||
// _activeSpaceId = null;
|
||||
// Matrix.of(context).activeBundle = bundle;
|
||||
// if (!Matrix.of(context)
|
||||
// .currentBundle!
|
||||
// .any((client) => client == Matrix.of(context).client)) {
|
||||
// Matrix.of(context)
|
||||
// .setActiveClient(Matrix.of(context).currentBundle!.first);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// Pangea#
|
||||
|
||||
void editBundlesForAccount(String? userId, String? activeBundle) async {
|
||||
final l10n = L10n.of(context);
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
|||
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/dummy_chat_list_item.dart';
|
||||
import 'package:fluffychat/pages/chat_list/search_title.dart';
|
||||
import 'package:fluffychat/pages/chat_list/space_view.dart';
|
||||
import 'package:fluffychat/pangea/chat_list/widgets/pangea_chat_list_header.dart';
|
||||
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
|
||||
import 'package:fluffychat/pangea/onboarding/onboarding.dart';
|
||||
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
|
|
@ -31,19 +31,24 @@ class ChatListViewBody extends StatelessWidget {
|
|||
final client = Matrix.of(context).client;
|
||||
final activeSpace = controller.activeSpaceId;
|
||||
if (activeSpace != null) {
|
||||
return SpaceView(
|
||||
// #Pangea
|
||||
// return SpaceView(
|
||||
// key: ValueKey(activeSpace),
|
||||
// spaceId: activeSpace,
|
||||
// onBack: controller.clearActiveSpace,
|
||||
// onChatTab: (room) => controller.onChatTap(room),
|
||||
// onChatContext: (room, context) =>
|
||||
// controller.chatContextAction(room, context),
|
||||
// activeChat: controller.activeChat,
|
||||
// toParentSpace: controller.setActiveSpace,
|
||||
// );
|
||||
return CourseChats(
|
||||
activeSpace,
|
||||
key: ValueKey(activeSpace),
|
||||
spaceId: activeSpace,
|
||||
onBack: controller.clearActiveSpace,
|
||||
onChatTab: (room) => controller.onChatTap(room),
|
||||
onChatContext: (room, context) =>
|
||||
controller.chatContextAction(room, context),
|
||||
activeChat: controller.activeChat,
|
||||
toParentSpace: controller.setActiveSpace,
|
||||
// #Pangea
|
||||
controller: controller,
|
||||
// Pangea#
|
||||
client: client,
|
||||
);
|
||||
// Pangea#
|
||||
}
|
||||
final spaces = client.rooms.where((r) => r.isSpace);
|
||||
final spaceDelegateCandidates = <String, Room>{};
|
||||
|
|
|
|||
|
|
@ -1,140 +1,142 @@
|
|||
import 'package:flutter/material.dart';
|
||||
// #Pangea
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
// import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
|
||||
import 'package:fluffychat/utils/sync_status_localization.dart';
|
||||
import '../../widgets/matrix.dart';
|
||||
// import 'package:fluffychat/config/themes.dart';
|
||||
// import 'package:fluffychat/l10n/l10n.dart';
|
||||
// import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
// import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
|
||||
// import 'package:fluffychat/utils/sync_status_localization.dart';
|
||||
// import '../../widgets/matrix.dart';
|
||||
|
||||
class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final ChatListController controller;
|
||||
final bool globalSearch;
|
||||
// class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
// final ChatListController controller;
|
||||
// final bool globalSearch;
|
||||
|
||||
const ChatListHeader({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.globalSearch = true,
|
||||
});
|
||||
// const ChatListHeader({
|
||||
// super.key,
|
||||
// required this.controller,
|
||||
// this.globalSearch = true,
|
||||
// });
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final client = Matrix.of(context).client;
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final theme = Theme.of(context);
|
||||
// final client = Matrix.of(context).client;
|
||||
|
||||
return SliverAppBar(
|
||||
floating: true,
|
||||
toolbarHeight: 72,
|
||||
pinned: FluffyThemes.isColumnMode(context),
|
||||
scrolledUnderElevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
automaticallyImplyLeading: false,
|
||||
title: StreamBuilder(
|
||||
stream: client.onSyncStatus.stream,
|
||||
builder: (context, snapshot) {
|
||||
final status = client.onSyncStatus.value ??
|
||||
const SyncStatusUpdate(SyncStatus.waitingForResponse);
|
||||
final hide = client.onSync.value != null &&
|
||||
status.status != SyncStatus.error &&
|
||||
client.prevBatch != null;
|
||||
return TextField(
|
||||
controller: controller.searchController,
|
||||
focusNode: controller.searchFocusNode,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (text) => controller.onSearchEnter(
|
||||
text,
|
||||
globalSearch: globalSearch,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: hide
|
||||
? L10n.of(context).searchChatsRooms
|
||||
: status.calcLocalizedString(context),
|
||||
hintStyle: TextStyle(
|
||||
color: status.error != null
|
||||
? Colors.orange
|
||||
: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
prefixIcon: hide
|
||||
? controller.isSearchMode
|
||||
? IconButton(
|
||||
tooltip: L10n.of(context).cancel,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: controller.cancelSearch,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: controller.startSearch,
|
||||
icon: Icon(
|
||||
Icons.search_outlined,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
width: 8,
|
||||
height: 8,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
value: status.progress,
|
||||
valueColor: status.error != null
|
||||
? const AlwaysStoppedAnimation<Color>(
|
||||
Colors.orange,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
suffixIcon: controller.isSearchMode && globalSearch
|
||||
? controller.isSearching
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 12,
|
||||
),
|
||||
child: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextButton.icon(
|
||||
onPressed: controller.setServer,
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
label: Text(
|
||||
controller.searchServer ??
|
||||
Matrix.of(context).client.homeserver!.host,
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 0,
|
||||
child: ClientChooserButton(controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// return SliverAppBar(
|
||||
// floating: true,
|
||||
// toolbarHeight: 72,
|
||||
// pinned: FluffyThemes.isColumnMode(context),
|
||||
// scrolledUnderElevation: 0,
|
||||
// backgroundColor: Colors.transparent,
|
||||
// automaticallyImplyLeading: false,
|
||||
// title: StreamBuilder(
|
||||
// stream: client.onSyncStatus.stream,
|
||||
// builder: (context, snapshot) {
|
||||
// final status = client.onSyncStatus.value ??
|
||||
// const SyncStatusUpdate(SyncStatus.waitingForResponse);
|
||||
// final hide = client.onSync.value != null &&
|
||||
// status.status != SyncStatus.error &&
|
||||
// client.prevBatch != null;
|
||||
// return TextField(
|
||||
// controller: controller.searchController,
|
||||
// focusNode: controller.searchFocusNode,
|
||||
// textInputAction: TextInputAction.search,
|
||||
// onChanged: (text) => controller.onSearchEnter(
|
||||
// text,
|
||||
// globalSearch: globalSearch,
|
||||
// ),
|
||||
// decoration: InputDecoration(
|
||||
// filled: true,
|
||||
// fillColor: theme.colorScheme.secondaryContainer,
|
||||
// border: OutlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// hintText: hide
|
||||
// ? L10n.of(context).searchChatsRooms
|
||||
// : status.calcLocalizedString(context),
|
||||
// hintStyle: TextStyle(
|
||||
// color: status.error != null
|
||||
// ? Colors.orange
|
||||
// : theme.colorScheme.onPrimaryContainer,
|
||||
// fontWeight: FontWeight.normal,
|
||||
// ),
|
||||
// prefixIcon: hide
|
||||
// ? controller.isSearchMode
|
||||
// ? IconButton(
|
||||
// tooltip: L10n.of(context).cancel,
|
||||
// icon: const Icon(Icons.close_outlined),
|
||||
// onPressed: controller.cancelSearch,
|
||||
// color: theme.colorScheme.onPrimaryContainer,
|
||||
// )
|
||||
// : IconButton(
|
||||
// onPressed: controller.startSearch,
|
||||
// icon: Icon(
|
||||
// Icons.search_outlined,
|
||||
// color: theme.colorScheme.onPrimaryContainer,
|
||||
// ),
|
||||
// )
|
||||
// : Container(
|
||||
// margin: const EdgeInsets.all(12),
|
||||
// width: 8,
|
||||
// height: 8,
|
||||
// child: Center(
|
||||
// child: CircularProgressIndicator.adaptive(
|
||||
// strokeWidth: 2,
|
||||
// value: status.progress,
|
||||
// valueColor: status.error != null
|
||||
// ? const AlwaysStoppedAnimation<Color>(
|
||||
// Colors.orange,
|
||||
// )
|
||||
// : null,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// suffixIcon: controller.isSearchMode && globalSearch
|
||||
// ? controller.isSearching
|
||||
// ? const Padding(
|
||||
// padding: EdgeInsets.symmetric(
|
||||
// vertical: 10.0,
|
||||
// horizontal: 12,
|
||||
// ),
|
||||
// child: SizedBox.square(
|
||||
// dimension: 24,
|
||||
// child: CircularProgressIndicator.adaptive(
|
||||
// strokeWidth: 2,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : TextButton.icon(
|
||||
// onPressed: controller.setServer,
|
||||
// style: TextButton.styleFrom(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// ),
|
||||
// textStyle: const TextStyle(fontSize: 12),
|
||||
// ),
|
||||
// icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
// label: Text(
|
||||
// controller.searchServer ??
|
||||
// Matrix.of(context).client.homeserver!.host,
|
||||
// maxLines: 2,
|
||||
// ),
|
||||
// )
|
||||
// : SizedBox(
|
||||
// width: 0,
|
||||
// child: ClientChooserButton(controller),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(56);
|
||||
}
|
||||
// @override
|
||||
// Size get preferredSize => const Size.fromHeight(56);
|
||||
// }
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:go_router/go_router.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_list/chat_list.dart';
|
||||
import 'package:fluffychat/pangea/chat_list/widgets/chat_list_view_body_wrapper.dart';
|
||||
import 'package:fluffychat/pangea/onboarding/onboarding.dart';
|
||||
import 'package:fluffychat/pangea/onboarding/onboarding_steps_enum.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
|
||||
class ChatListView extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
|
@ -33,21 +30,20 @@ class ChatListView extends StatelessWidget {
|
|||
},
|
||||
child: Row(
|
||||
children: [
|
||||
if (FluffyThemes.isColumnMode(context) ||
|
||||
AppConfig.displayNavigationRail) ...[
|
||||
SpacesNavigationRail(
|
||||
activeSpaceId: controller.activeSpaceId,
|
||||
onGoToChats: controller.clearActiveSpace,
|
||||
onGoToSpaceId: controller.setActiveSpace,
|
||||
// #Pangea
|
||||
clearActiveSpace: controller.clearActiveSpace,
|
||||
// Pangea#
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
// #Pangea
|
||||
// if (FluffyThemes.isColumnMode(context) ||
|
||||
// AppConfig.displayNavigationRail) ...[
|
||||
// SpacesNavigationRail(
|
||||
// activeSpaceId: controller.activeSpaceId,
|
||||
// onGoToChats: controller.clearActiveSpace,
|
||||
// onGoToSpaceId: controller.setActiveSpace,
|
||||
// ),
|
||||
// Container(
|
||||
// color: Theme.of(context).dividerColor,
|
||||
// width: 1,
|
||||
// ),
|
||||
// ],
|
||||
// Pangea#
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: FocusManager.instance.primaryFocus?.unfocus,
|
||||
|
|
|
|||
|
|
@ -1,231 +1,233 @@
|
|||
import 'package:flutter/material.dart';
|
||||
// #Pangea
|
||||
// import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.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/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/fluffy_share.dart';
|
||||
import 'chat_list.dart';
|
||||
// import 'package:fluffychat/config/themes.dart';
|
||||
// import 'package:fluffychat/l10n/l10n.dart';
|
||||
// import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
// import 'package:fluffychat/widgets/avatar.dart';
|
||||
// import 'package:fluffychat/widgets/matrix.dart';
|
||||
// import '../../utils/fluffy_share.dart';
|
||||
// import 'chat_list.dart';
|
||||
|
||||
class ClientChooserButton extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
// class ClientChooserButton extends StatelessWidget {
|
||||
// final ChatListController controller;
|
||||
|
||||
const ClientChooserButton(this.controller, {super.key});
|
||||
// const ClientChooserButton(this.controller, {super.key});
|
||||
|
||||
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||
final matrix = Matrix.of(context);
|
||||
final bundles = matrix.accountBundles.keys.toList()
|
||||
..sort(
|
||||
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
? 0
|
||||
: a.isValidMatrixId && !b.isValidMatrixId
|
||||
? -1
|
||||
: 1,
|
||||
);
|
||||
return <PopupMenuEntry<Object>>[
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.newGroup,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.group_add_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).createGroup),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.setStatus,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.edit_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).setStatus),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.invite,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.adaptive.share_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).inviteContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.archive,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.archive_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).archive),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.settings,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).settings),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
for (final bundle in bundles) ...[
|
||||
if (matrix.accountBundles[bundle]!.length != 1 ||
|
||||
matrix.accountBundles[bundle]!.single!.userID != bundle)
|
||||
PopupMenuItem(
|
||||
value: null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
bundle!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleMedium!.color,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
...matrix.accountBundles[bundle]!
|
||||
.whereType<Client>()
|
||||
.where((client) => client.isLogged())
|
||||
.map(
|
||||
(client) => PopupMenuItem(
|
||||
value: client,
|
||||
child: FutureBuilder<Profile?>(
|
||||
future: client.fetchOwnProfile(),
|
||||
builder: (context, snapshot) => Row(
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name: snapshot.data?.displayName ??
|
||||
client.userID!.localpart,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
snapshot.data?.displayName ??
|
||||
client.userID!.localpart!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
onPressed: () => controller.editBundlesForAccount(
|
||||
client.userID,
|
||||
bundle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
PopupMenuItem(
|
||||
value: SettingsAction.addAccount,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add_outlined),
|
||||
const SizedBox(width: 18),
|
||||
Text(L10n.of(context).addAccount),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
// List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
|
||||
// final matrix = Matrix.of(context);
|
||||
// final bundles = matrix.accountBundles.keys.toList()
|
||||
// ..sort(
|
||||
// (a, b) => a!.isValidMatrixId == b!.isValidMatrixId
|
||||
// ? 0
|
||||
// : a.isValidMatrixId && !b.isValidMatrixId
|
||||
// ? -1
|
||||
// : 1,
|
||||
// );
|
||||
// return <PopupMenuEntry<Object>>[
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.newGroup,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.group_add_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).createGroup),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.setStatus,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.edit_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).setStatus),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.invite,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Icon(Icons.adaptive.share_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).inviteContact),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.archive,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.archive_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).archive),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.settings,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.settings_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).settings),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const PopupMenuDivider(),
|
||||
// for (final bundle in bundles) ...[
|
||||
// if (matrix.accountBundles[bundle]!.length != 1 ||
|
||||
// matrix.accountBundles[bundle]!.single!.userID != bundle)
|
||||
// PopupMenuItem(
|
||||
// value: null,
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Text(
|
||||
// bundle!,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).textTheme.titleMedium!.color,
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// ),
|
||||
// const Divider(height: 1),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ...matrix.accountBundles[bundle]!
|
||||
// .whereType<Client>()
|
||||
// .where((client) => client.isLogged())
|
||||
// .map(
|
||||
// (client) => PopupMenuItem(
|
||||
// value: client,
|
||||
// child: FutureBuilder<Profile?>(
|
||||
// future: client.fetchOwnProfile(),
|
||||
// builder: (context, snapshot) => Row(
|
||||
// children: [
|
||||
// Avatar(
|
||||
// mxContent: snapshot.data?.avatarUrl,
|
||||
// name: snapshot.data?.displayName ??
|
||||
// client.userID!.localpart,
|
||||
// size: 32,
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: Text(
|
||||
// snapshot.data?.displayName ??
|
||||
// client.userID!.localpart!,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.edit_outlined),
|
||||
// onPressed: () => controller.editBundlesForAccount(
|
||||
// client.userID,
|
||||
// bundle,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// PopupMenuItem(
|
||||
// value: SettingsAction.addAccount,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// const Icon(Icons.person_add_outlined),
|
||||
// const SizedBox(width: 18),
|
||||
// Text(L10n.of(context).addAccount),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ];
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final matrix = Matrix.of(context);
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final matrix = Matrix.of(context);
|
||||
|
||||
var clientCount = 0;
|
||||
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
|
||||
return FutureBuilder<Profile>(
|
||||
future: matrix.client.isLogged() ? matrix.client.fetchOwnProfile() : null,
|
||||
builder: (context, snapshot) => Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
color: Colors.transparent,
|
||||
child: PopupMenuButton<Object>(
|
||||
popUpAnimationStyle: FluffyThemes.isColumnMode(context)
|
||||
? AnimationStyle.noAnimation
|
||||
: null, // https://github.com/flutter/flutter/issues/167180
|
||||
onSelected: (o) => _clientSelected(o, context),
|
||||
itemBuilder: _bundleMenuItems,
|
||||
child: Center(
|
||||
child: Avatar(
|
||||
mxContent: snapshot.data?.avatarUrl,
|
||||
name:
|
||||
snapshot.data?.displayName ?? matrix.client.userID?.localpart,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// var clientCount = 0;
|
||||
// matrix.accountBundles.forEach((key, value) => clientCount += value.length);
|
||||
// return FutureBuilder<Profile>(
|
||||
// future: matrix.client.isLogged() ? matrix.client.fetchOwnProfile() : null,
|
||||
// builder: (context, snapshot) => Material(
|
||||
// clipBehavior: Clip.hardEdge,
|
||||
// borderRadius: BorderRadius.circular(99),
|
||||
// color: Colors.transparent,
|
||||
// child: PopupMenuButton<Object>(
|
||||
// popUpAnimationStyle: FluffyThemes.isColumnMode(context)
|
||||
// ? AnimationStyle.noAnimation
|
||||
// : null, // https://github.com/flutter/flutter/issues/167180
|
||||
// onSelected: (o) => _clientSelected(o, context),
|
||||
// itemBuilder: _bundleMenuItems,
|
||||
// child: Center(
|
||||
// child: Avatar(
|
||||
// mxContent: snapshot.data?.avatarUrl,
|
||||
// name:
|
||||
// snapshot.data?.displayName ?? matrix.client.userID?.localpart,
|
||||
// size: 32,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
void _clientSelected(
|
||||
Object object,
|
||||
BuildContext context,
|
||||
) async {
|
||||
if (object is Client) {
|
||||
controller.setActiveClient(object);
|
||||
} else if (object is String) {
|
||||
controller.setActiveBundle(object);
|
||||
} else if (object is SettingsAction) {
|
||||
switch (object) {
|
||||
case SettingsAction.addAccount:
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).addAccount,
|
||||
message: L10n.of(context).enableMultiAccounts,
|
||||
okLabel: L10n.of(context).next,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
context.go('/rooms/settings/addaccount');
|
||||
break;
|
||||
case SettingsAction.newGroup:
|
||||
context.go('/rooms/newgroup');
|
||||
break;
|
||||
case SettingsAction.invite:
|
||||
FluffyShare.shareInviteLink(context);
|
||||
break;
|
||||
case SettingsAction.settings:
|
||||
context.go('/rooms/settings');
|
||||
break;
|
||||
case SettingsAction.archive:
|
||||
context.go('/rooms/archive');
|
||||
break;
|
||||
case SettingsAction.setStatus:
|
||||
controller.setStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// void _clientSelected(
|
||||
// Object object,
|
||||
// BuildContext context,
|
||||
// ) async {
|
||||
// if (object is Client) {
|
||||
// controller.setActiveClient(object);
|
||||
// } else if (object is String) {
|
||||
// controller.setActiveBundle(object);
|
||||
// } else if (object is SettingsAction) {
|
||||
// switch (object) {
|
||||
// case SettingsAction.addAccount:
|
||||
// final consent = await showOkCancelAlertDialog(
|
||||
// context: context,
|
||||
// title: L10n.of(context).addAccount,
|
||||
// message: L10n.of(context).enableMultiAccounts,
|
||||
// okLabel: L10n.of(context).next,
|
||||
// cancelLabel: L10n.of(context).cancel,
|
||||
// );
|
||||
// if (consent != OkCancelResult.ok) return;
|
||||
// context.go('/rooms/settings/addaccount');
|
||||
// break;
|
||||
// case SettingsAction.newGroup:
|
||||
// context.go('/rooms/newgroup');
|
||||
// break;
|
||||
// case SettingsAction.invite:
|
||||
// FluffyShare.shareInviteLink(context);
|
||||
// break;
|
||||
// case SettingsAction.settings:
|
||||
// context.go('/rooms/settings');
|
||||
// break;
|
||||
// case SettingsAction.archive:
|
||||
// context.go('/rooms/archive');
|
||||
// break;
|
||||
// case SettingsAction.setStatus:
|
||||
// controller.setStatus();
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
enum SettingsAction {
|
||||
addAccount,
|
||||
newGroup,
|
||||
setStatus,
|
||||
invite,
|
||||
settings,
|
||||
archive,
|
||||
}
|
||||
// enum SettingsAction {
|
||||
// addAccount,
|
||||
// newGroup,
|
||||
// setStatus,
|
||||
// invite,
|
||||
// settings,
|
||||
// archive,
|
||||
// }
|
||||
// Pangea#
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,14 @@ class ChatPermissionsSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
|
||||
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
// #Pangea
|
||||
// String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
String? get roomId {
|
||||
final pathParameters = GoRouterState.of(context).pathParameters;
|
||||
return pathParameters['roomid'] ?? pathParameters['spaceid'];
|
||||
}
|
||||
|
||||
// Pangea#
|
||||
void editPowerLevel(
|
||||
BuildContext context,
|
||||
String key,
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class NewGroupController extends State<NewGroup> {
|
|||
GoogleAnalytics.createClass(room.name, spaceCode);
|
||||
}
|
||||
|
||||
context.go("/rooms?spaceId=$spaceId");
|
||||
context.go("/rooms/spaces/$spaceId/details");
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ import 'package:package_info_plus/package_info_plus.dart';
|
|||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
import '../../widgets/mxc_image_viewer.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
|
|
@ -43,19 +41,17 @@ class SettingsView extends StatelessWidget {
|
|||
children: [
|
||||
// #Pangea
|
||||
// if (FluffyThemes.isColumnMode(context)) ...[
|
||||
if (FluffyThemes.isColumnMode(context) ||
|
||||
AppConfig.displayNavigationRail) ...[
|
||||
// Pangea#
|
||||
SpacesNavigationRail(
|
||||
activeSpaceId: null,
|
||||
onGoToChats: () => context.go('/rooms'),
|
||||
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
// SpacesNavigationRail(
|
||||
// activeSpaceId: null,
|
||||
// onGoToChats: () => context.go('/rooms'),
|
||||
// onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
|
||||
// ),
|
||||
// Container(
|
||||
// color: Theme.of(context).dividerColor,
|
||||
// width: 1,
|
||||
// ),
|
||||
// ],
|
||||
// Pangea#
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
// #Pangea
|
||||
|
|
|
|||
|
|
@ -27,7 +27,13 @@ class EmotesSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class EmotesSettingsController extends State<EmotesSettings> {
|
||||
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
// #Pangea
|
||||
// String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
String? get roomId {
|
||||
final pathParameters = GoRouterState.of(context).pathParameters;
|
||||
return pathParameters['roomid'] ?? pathParameters['spaceid'];
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
Room? get room =>
|
||||
roomId != null ? Matrix.of(context).client.getRoomById(roomId!) : null;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,14 @@ class MultipleEmotesSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class MultipleEmotesSettingsController extends State<MultipleEmotesSettings> {
|
||||
String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
// #Pangea
|
||||
// String? get roomId => GoRouterState.of(context).pathParameters['roomid'];
|
||||
String? get roomId {
|
||||
final pathParameters = GoRouterState.of(context).pathParameters;
|
||||
return pathParameters['roomid'] ?? pathParameters['spaceid'];
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MultipleEmotesSettingsView(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,14 @@ class ActivityPlanCard extends StatelessWidget {
|
|||
|
||||
final ids = await controller.launchToSpace();
|
||||
ids.length == 1
|
||||
? context.go("/rooms/${ids.first}")
|
||||
: context.go("/rooms?spaceId=${controller.room.id}");
|
||||
? context.go("/rooms/spaces/${controller.room.id}/${ids.first}")
|
||||
: context.go("/rooms/spaces/${controller.room.id}/details");
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms?spaceId=${controller.room.id}");
|
||||
context.go("/rooms/spaces/${controller.room.id}/details");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
|
|||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_repo.dart';
|
||||
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.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';
|
||||
|
|
@ -33,6 +34,9 @@ class ActivityPlannerBuilder extends StatefulWidget {
|
|||
final String? initialFilename;
|
||||
final Room room;
|
||||
|
||||
final bool enabledEdits;
|
||||
final bool enableMultiLaunch;
|
||||
|
||||
final Widget Function(ActivityPlannerBuilderState) builder;
|
||||
|
||||
const ActivityPlannerBuilder({
|
||||
|
|
@ -41,6 +45,8 @@ class ActivityPlannerBuilder extends StatefulWidget {
|
|||
this.initialFilename,
|
||||
required this.room,
|
||||
required this.builder,
|
||||
this.enabledEdits = false,
|
||||
this.enableMultiLaunch = false,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -334,9 +340,13 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
|
||||
Future<String> _launchActivityRoom(int index) async {
|
||||
await updateImageURL();
|
||||
final roomID = await Matrix.of(context).client.createGroupChat(
|
||||
final roomID = await Matrix.of(context).client.createRoom(
|
||||
creationContent: {
|
||||
'type':
|
||||
"${PangeaRoomTypes.activitySession}:${updatedActivity.bookmarkId}",
|
||||
},
|
||||
visibility: Visibility.private,
|
||||
groupName: "${updatedActivity.title} ${index + 1}",
|
||||
name: "${updatedActivity.title} ${index + 1}",
|
||||
initialState: [
|
||||
if (imageURL != null)
|
||||
StateEvent(
|
||||
|
|
@ -356,7 +366,6 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
|
|||
],
|
||||
),
|
||||
],
|
||||
enableEncryption: false,
|
||||
);
|
||||
|
||||
Room? activityRoom = room.client.getRoomById(roomID);
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
|
|||
),
|
||||
selected: false,
|
||||
onSelected: (_) => context.go(
|
||||
'/rooms/${widget.roomID}/details/planner/generator',
|
||||
'/rooms/spaces/${widget.roomID}/details/planner/generator',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
|
|||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_repo.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
|
|
@ -28,10 +30,28 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
Map<String, ActivityRole> get _roles =>
|
||||
controller.room.activityPlan?.roles ?? {};
|
||||
|
||||
Future<void> _archiveToAnalytics() async {
|
||||
Future<void> _archiveToAnalytics(BuildContext context) async {
|
||||
await controller.room.archiveActivity();
|
||||
await MatrixState.pangeaController.putAnalytics
|
||||
.sendActivityAnalytics(controller.room.id);
|
||||
|
||||
final courseParent = controller.room.courseParent;
|
||||
if (courseParent?.coursePlan == null) return;
|
||||
final coursePlan = await CourseRepo.get(
|
||||
courseParent!.coursePlan!.uuid,
|
||||
);
|
||||
|
||||
if (coursePlan == null) {
|
||||
throw L10n.of(context).noCourseFound;
|
||||
}
|
||||
|
||||
final activityId = controller.room.activityPlan!.bookmarkId;
|
||||
final topicId = coursePlan.topicID(activityId);
|
||||
if (topicId == null) {
|
||||
throw L10n.of(context).activityNotFoundForCourse;
|
||||
}
|
||||
|
||||
await courseParent.finishCourseActivity(activityId, topicId);
|
||||
}
|
||||
|
||||
List<ActivityRoleModel> get _rolesWithSummaries {
|
||||
|
|
@ -226,7 +246,7 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
|
|||
onPressed: () async {
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: _archiveToAnalytics,
|
||||
future: () => _archiveToAnalytics(context),
|
||||
);
|
||||
|
||||
if (!resp.isError) {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_mo
|
|||
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
|
||||
import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.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/pangea/extensions/pangea_room_extension.dart';
|
||||
|
|
@ -322,4 +324,16 @@ extension ActivityRoomExtension on Room {
|
|||
}
|
||||
|
||||
bool get isHiddenActivityRoom => ownRole?.isArchived ?? false;
|
||||
|
||||
Room? get courseParent => pangeaSpaceParents.firstWhereOrNull(
|
||||
(parent) => parent.coursePlan != null,
|
||||
);
|
||||
|
||||
bool get isActivitySession =>
|
||||
getState(EventTypes.RoomCreate)
|
||||
?.content
|
||||
.tryGet<String>('type')
|
||||
?.startsWith(PangeaRoomTypes.activitySession) ==
|
||||
true ||
|
||||
activityPlan != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:cached_network_image/cached_network_image.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/common/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
||||
class ActivitySuggestionCard extends StatelessWidget {
|
||||
|
|
@ -13,12 +12,19 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
final double width;
|
||||
final double height;
|
||||
|
||||
final double? fontSize;
|
||||
final double? fontSizeSmall;
|
||||
final double? iconSize;
|
||||
|
||||
const ActivitySuggestionCard({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.onPressed,
|
||||
required this.width,
|
||||
required this.height,
|
||||
this.fontSize,
|
||||
this.fontSizeSmall,
|
||||
this.iconSize,
|
||||
});
|
||||
|
||||
ActivityPlanModel get activity => controller.updatedActivity;
|
||||
|
|
@ -26,40 +32,26 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return PressableButton(
|
||||
onPressed: onPressed,
|
||||
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),
|
||||
),
|
||||
height: height,
|
||||
width: width,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Container(
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
height: width,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: width,
|
||||
width: width,
|
||||
child: activity.imageURL != null
|
||||
? activity.imageURL!.startsWith("mxc")
|
||||
? MxcImage(
|
||||
|
|
@ -77,104 +69,70 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
errorWidget: (context, url, error) =>
|
||||
const SizedBox(),
|
||||
fit: BoxFit.cover,
|
||||
width: width,
|
||||
height: width,
|
||||
)
|
||||
: null,
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
activity.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 12.0,
|
||||
),
|
||||
Text(
|
||||
"${activity.req.numberOfParticipants}",
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (activity.req.mode.isNotEmpty)
|
||||
Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
if (activity.req.mode.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
activity.req.mode,
|
||||
style: theme.textTheme.labelSmall,
|
||||
style: fontSizeSmall != null
|
||||
? TextStyle(fontSize: fontSizeSmall)
|
||||
: theme.textTheme.labelSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.group_outlined,
|
||||
size: iconSize ?? 12.0,
|
||||
),
|
||||
Text(
|
||||
"${activity.req.numberOfParticipants}",
|
||||
style: fontSizeSmall != null
|
||||
? TextStyle(fontSize: fontSizeSmall)
|
||||
: theme.textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 4.0,
|
||||
right: 4.0,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
controller.isBookmarked ? Icons.save : Icons.save_outlined,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
onPressed: controller.toggleBookmarkedActivity,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withAlpha(180),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
|
|||
|
||||
final ids = await widget.controller.launchToSpace();
|
||||
ids.length == 1
|
||||
? context.go("/rooms/${ids.first}")
|
||||
: context.go("/rooms?spaceId=${widget.controller.room.id}");
|
||||
? context
|
||||
.go("/rooms/spaces/${widget.controller.room.id}/${ids.first}")
|
||||
: context.go("/rooms/spaces/${widget.controller.room.id}/details");
|
||||
Navigator.of(context).pop();
|
||||
} catch (e, s) {
|
||||
_launchError = L10n.of(context).errorLaunchActivityMessage;
|
||||
|
|
|
|||
|
|
@ -241,23 +241,24 @@ class _ActivitySuggestionBaseContent extends StatelessWidget {
|
|||
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 (activityController.widget.enabledEdits)
|
||||
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(
|
||||
|
|
@ -287,9 +288,11 @@ class _ActivitySuggestionBaseContent extends StatelessWidget {
|
|||
style: controller.buttonStyle,
|
||||
// onPressed: _launchActivity,
|
||||
onPressed: () {
|
||||
activityController.setLaunchState(
|
||||
ActivityLaunchState.launching,
|
||||
);
|
||||
!activityController.widget.enableMultiLaunch
|
||||
? controller.launchActivity()
|
||||
: activityController.setLaunchState(
|
||||
ActivityLaunchState.launching,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
spacing: 12.0,
|
||||
|
|
@ -393,7 +396,7 @@ class _ActivitySuggestionEditContent extends StatelessWidget {
|
|||
child: TextFormField(
|
||||
controller: activityController.participantsController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context).classRoster,
|
||||
labelText: L10n.of(context).participants,
|
||||
),
|
||||
maxLines: 1,
|
||||
keyboardType: TextInputType.number,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
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';
|
||||
|
|
@ -12,7 +8,6 @@ import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators
|
|||
import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
|
||||
class AnalyticsPageView extends StatelessWidget {
|
||||
final AnalyticsPageState controller;
|
||||
|
|
@ -23,70 +18,51 @@ class AnalyticsPageView extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LearningProgressIndicators(
|
||||
selected: controller.selectedIndicator,
|
||||
canSelect:
|
||||
controller.selectedIndicator != ProgressIndicatorEnum.level,
|
||||
),
|
||||
Expanded(
|
||||
child: StreamBuilder(
|
||||
stream: MatrixState
|
||||
.pangeaController.getAnalytics.analyticsStream.stream,
|
||||
builder: (context, _) {
|
||||
if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.level) {
|
||||
return const LevelDialogContent();
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.morphsUsed) {
|
||||
return AnalyticsPopupWrapper(
|
||||
constructZoom: controller.widget.constructZoom,
|
||||
view: ConstructTypeEnum.morph,
|
||||
);
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.wordsUsed) {
|
||||
return AnalyticsPopupWrapper(
|
||||
constructZoom: controller.widget.constructZoom,
|
||||
view: ConstructTypeEnum.vocab,
|
||||
);
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.activities) {
|
||||
return const ActivityArchive();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (!isColumnMode && AppConfig.displayNavigationRail) ...[
|
||||
SpacesNavigationRail(
|
||||
activeSpaceId: null,
|
||||
onGoToChats: () => context.go('/rooms'),
|
||||
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
LearningProgressIndicators(
|
||||
selected: controller.selectedIndicator,
|
||||
canSelect: controller.selectedIndicator !=
|
||||
ProgressIndicatorEnum.level,
|
||||
),
|
||||
Expanded(
|
||||
child: StreamBuilder(
|
||||
stream: MatrixState.pangeaController.getAnalytics
|
||||
.analyticsStream.stream,
|
||||
builder: (context, _) {
|
||||
if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.level) {
|
||||
return const LevelDialogContent();
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.morphsUsed) {
|
||||
return AnalyticsPopupWrapper(
|
||||
constructZoom: controller.widget.constructZoom,
|
||||
view: ConstructTypeEnum.morph,
|
||||
);
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.wordsUsed) {
|
||||
return AnalyticsPopupWrapper(
|
||||
constructZoom: controller.widget.constructZoom,
|
||||
view: ConstructTypeEnum.vocab,
|
||||
);
|
||||
} else if (controller.selectedIndicator ==
|
||||
ProgressIndicatorEnum.activities) {
|
||||
return const ActivityArchive();
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ class RoomDefaults {
|
|||
},
|
||||
);
|
||||
|
||||
static StateEvent defaultSpacePowerLevels(String userID) => StateEvent(
|
||||
static StateEvent defaultSpacePowerLevels(
|
||||
String userID, {
|
||||
int spaceChild = 50,
|
||||
}) =>
|
||||
StateEvent(
|
||||
type: EventTypes.RoomPowerLevels,
|
||||
stateKey: '',
|
||||
content: {
|
||||
|
|
@ -68,7 +72,7 @@ class RoomDefaults {
|
|||
"events": {
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.join_rules": 100,
|
||||
"m.space.child": 50,
|
||||
"m.space.child": spaceChild,
|
||||
},
|
||||
"events_default": 0,
|
||||
"state_default": 50,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ Future<void> showInviteDialog(Room room, BuildContext context) async {
|
|||
if (acceptInvite == OkCancelResult.ok) {
|
||||
await room.join();
|
||||
context.go(
|
||||
room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}",
|
||||
room.isSpace
|
||||
? "/rooms/spaces/${room.id}/details"
|
||||
: "/rooms/${room.id}",
|
||||
);
|
||||
return room.id;
|
||||
} else if (acceptInvite == OkCancelResult.cancel) {
|
||||
|
|
@ -38,7 +40,7 @@ Future<void> showInviteDialog(Room room, BuildContext context) async {
|
|||
);
|
||||
|
||||
if (!resp.isError && resp.result is String) {
|
||||
context.go("/rooms?spaceId=${resp.result}");
|
||||
context.go("/rooms/spaces/${resp.result}/details");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ void chatListHandleSpaceTap(
|
|||
Room space,
|
||||
) {
|
||||
void setActiveSpaceAndCloseChat() {
|
||||
context.go("/rooms?spaceId=${space.id}");
|
||||
context.go("/rooms/spaces/${space.id}/details");
|
||||
}
|
||||
|
||||
void autoJoin(Room space) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
class PangeaRoomTypes {
|
||||
static const analytics = 'p.analytics';
|
||||
static const activitySession = 'p.activity.session';
|
||||
}
|
||||
|
|
|
|||
271
lib/pangea/chat_settings/pages/chat_details_button_row.dart
Normal file
271
lib/pangea/chat_settings/pages/chat_details_button_row.dart
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class ChatDetailsButtonRow extends StatefulWidget {
|
||||
final ChatDetailsController controller;
|
||||
final Room room;
|
||||
|
||||
const ChatDetailsButtonRow({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatDetailsButtonRow> createState() => ChatDetailsButtonRowState();
|
||||
}
|
||||
|
||||
class ChatDetailsButtonRowState extends State<ChatDetailsButtonRow> {
|
||||
StreamSubscription? notificationChangeSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notificationChangeSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.listen(
|
||||
(u) => setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
notificationChangeSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final double _buttonHeight = 84.0;
|
||||
final double _miniButtonWidth = 50.0;
|
||||
|
||||
Room get room => widget.room;
|
||||
|
||||
List<ButtonDetails> _buttons(BuildContext context) {
|
||||
final L10n l10n = L10n.of(context);
|
||||
return [
|
||||
ButtonDetails(
|
||||
title: l10n.permissions,
|
||||
icon: const Icon(Icons.edit_attributes_outlined, size: 30.0),
|
||||
onPressed: () => context.go('/rooms/${room.id}/details/permissions'),
|
||||
enabled: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: room.pushRuleState == PushRuleState.notify
|
||||
? l10n.notificationsOn
|
||||
: l10n.notificationsOff,
|
||||
icon: Icon(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? Icons.notifications_on_outlined
|
||||
: Icons.notifications_off_outlined,
|
||||
size: 30.0,
|
||||
),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? PushRuleState.mentionsOnly
|
||||
: PushRuleState.notify,
|
||||
),
|
||||
),
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.invite,
|
||||
icon: const Icon(Icons.person_add_outlined, size: 30.0),
|
||||
onPressed: () {
|
||||
String filter = 'knocking';
|
||||
if (room.getParticipants([Membership.knock]).isEmpty) {
|
||||
filter = room.pangeaSpaceParents.isNotEmpty ? 'space' : 'contacts';
|
||||
}
|
||||
context.go('/rooms/${room.id}/details/invite?filter=$filter');
|
||||
},
|
||||
enabled: room.canInvite && !room.isDirectChat,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.download,
|
||||
icon: const Icon(Icons.download_outlined, size: 30.0),
|
||||
onPressed: widget.controller.downloadChatAction,
|
||||
visible: kIsWeb,
|
||||
enabled: room.ownPowerLevel >= 50,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.botSettings,
|
||||
icon: const BotFace(
|
||||
width: 30.0,
|
||||
expression: BotExpression.idle,
|
||||
),
|
||||
onPressed: () => showDialog<BotOptionsModel?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => ConversationBotSettingsDialog(
|
||||
room: room,
|
||||
onSubmit: widget.controller.setBotOptions,
|
||||
),
|
||||
),
|
||||
visible: !room.isDirectChat || room.botOptions != null,
|
||||
enabled: room.canInvite,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.chatCapacity,
|
||||
icon: const Icon(Icons.reduce_capacity, size: 30.0),
|
||||
onPressed: widget.controller.setRoomCapacity,
|
||||
enabled: !room.isDirectChat && room.canSendDefaultStates,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.leave,
|
||||
icon: const Icon(Icons.logout_outlined, size: 30.0),
|
||||
onPressed: () async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).leave,
|
||||
cancelLabel: L10n.of(context).no,
|
||||
message: L10n.of(context).leaveRoomDescription,
|
||||
isDestructive: true,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.leave,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
},
|
||||
enabled: room.membership == Membership.join,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.delete,
|
||||
icon: const Icon(Icons.delete_outline, size: 30.0),
|
||||
onPressed: () async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).delete,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
message: L10n.of(context).deleteChatDesc,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (resp.isError) return;
|
||||
context.go("/rooms");
|
||||
},
|
||||
enabled: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttons = _buttons(context)
|
||||
.where(
|
||||
(button) => button.visible,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final fullButtonCapacity = (availableWidth / 120.0).floor() - 1;
|
||||
|
||||
final mini = fullButtonCapacity < 4;
|
||||
|
||||
final List<ButtonDetails> mainViewButtons =
|
||||
buttons.where((button) => button.showInMainView).toList();
|
||||
final List<ButtonDetails> otherButtons =
|
||||
buttons.where((button) => !button.showInMainView).toList();
|
||||
|
||||
return Row(
|
||||
spacing: FluffyThemes.isColumnMode(context) ? 12.0 : 0.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(mainViewButtons.length + 1, (index) {
|
||||
if (index == mainViewButtons.length) {
|
||||
if (otherButtons.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
itemBuilder: (context) {
|
||||
return otherButtons
|
||||
.map(
|
||||
(button) => PopupMenuItem(
|
||||
value: button,
|
||||
onTap: button.enabled ? button.onPressed : null,
|
||||
enabled: button.enabled,
|
||||
child: Row(
|
||||
children: [
|
||||
button.icon,
|
||||
const SizedBox(width: 8),
|
||||
Text(button.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: ButtonDetails(
|
||||
title: L10n.of(context).more,
|
||||
icon: const Icon(Icons.more_horiz_outlined),
|
||||
),
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final button = mainViewButtons[index];
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: button,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
206
lib/pangea/chat_settings/pages/chat_details_content.dart
Normal file
206
lib/pangea/chat_settings/pages/chat_details_content.dart
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/chat_details_button_row.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_participants_widget.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class ChatDetailsContent extends StatelessWidget {
|
||||
final ChatDetailsController controller;
|
||||
final Room room;
|
||||
|
||||
const ChatDetailsContent(this.controller, this.room, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: 2,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
if (i == 0) {
|
||||
final theme = Theme.of(context);
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: controller.widget.embeddedCloseButton != null
|
||||
? 'embedded_content_banner'
|
||||
: 'content_banner',
|
||||
child: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
userId: room.directChatMatrixID,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
borderRadius: room.isSpace
|
||||
? BorderRadius.circular(24.0)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat &&
|
||||
room.canChangeStateEvent(
|
||||
EventTypes.RoomAvatar,
|
||||
))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: controller.setAvatarAction,
|
||||
heroTag: null,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: room.isDirectChat
|
||||
? null
|
||||
: () => room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? controller.setDisplaynameAction()
|
||||
: FluffyShare.share(
|
||||
displayname,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: Icon(
|
||||
room.isDirectChat
|
||||
? Icons.chat_bubble_outline
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? Icons.edit_outlined
|
||||
: Icons.copy_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
disabledForegroundColor:
|
||||
theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
room.isDirectChat
|
||||
? L10n.of(context).directChat
|
||||
: displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: room.isDirectChat
|
||||
? null
|
||||
: () => context.push(
|
||||
'/rooms/${controller.roomId}/details/invite?filter=participants',
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.secondary,
|
||||
disabledForegroundColor:
|
||||
theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
room.getParticipants().length,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
if (room.isRoomAdmin)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: IconButton(
|
||||
onPressed: controller.setTopicAction,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32.0,
|
||||
right: 32.0,
|
||||
top: 16.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? room.isSpace
|
||||
? L10n.of(context).noSpaceDescriptionYet
|
||||
: L10n.of(context).noChatDescriptionYet
|
||||
: room.topic,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.blueAccent,
|
||||
decorationColor: Colors.blueAccent,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color: theme.textTheme.bodyMedium!.color,
|
||||
decorationColor: theme.textTheme.bodyMedium!.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ChatDetailsButtonRow(
|
||||
controller: controller,
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: RoomParticipantsSection(room: room),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,882 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.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_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/conversation_bot/conversation_bot_settings.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
|
||||
import 'package:fluffychat/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/utils/url_launcher.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
|
||||
|
||||
class PangeaChatDetailsView extends StatelessWidget {
|
||||
final ChatDetailsController controller;
|
||||
|
||||
const PangeaChatDetailsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final room = Matrix.of(context).client.getRoomById(controller.roomId!);
|
||||
if (room == null || room.membership == Membership.leave) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return StreamBuilder(
|
||||
stream: room.client.onRoomState.stream
|
||||
.where((update) => update.roomId == room.id),
|
||||
builder: (context, snapshot) {
|
||||
var members = room.getParticipants().toList()
|
||||
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
|
||||
members = members.take(10).toList();
|
||||
final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) +
|
||||
(room.summary.mJoinedMemberCount ?? 0);
|
||||
final displayname = room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: controller.widget.embeddedCloseButton ??
|
||||
(room.isSpace
|
||||
? FluffyThemes.isColumnMode(context)
|
||||
? const SizedBox()
|
||||
: BackButton(
|
||||
onPressed: () =>
|
||||
context.go("/rooms?spaceId=${room.id}"),
|
||||
)
|
||||
: const Center(child: BackButton())),
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
maxWidth: 900,
|
||||
showBorder: false,
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: 2,
|
||||
itemBuilder: (BuildContext context, int i) => i == 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag:
|
||||
controller.widget.embeddedCloseButton !=
|
||||
null
|
||||
? 'embedded_content_banner'
|
||||
: 'content_banner',
|
||||
child: Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
userId: room.directChatMatrixID,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
borderRadius: room.isSpace
|
||||
? BorderRadius.circular(24.0)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (!room.isDirectChat &&
|
||||
room.canChangeStateEvent(
|
||||
EventTypes.RoomAvatar,
|
||||
))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: controller.setAvatarAction,
|
||||
heroTag: null,
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: room.isDirectChat
|
||||
? null
|
||||
: () => room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? controller
|
||||
.setDisplaynameAction()
|
||||
: FluffyShare.share(
|
||||
displayname,
|
||||
context,
|
||||
copyOnly: true,
|
||||
),
|
||||
icon: Icon(
|
||||
room.isDirectChat
|
||||
? Icons.chat_bubble_outline
|
||||
: room.canChangeStateEvent(
|
||||
EventTypes.RoomName,
|
||||
)
|
||||
? Icons.edit_outlined
|
||||
: Icons.copy_outlined,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
theme.colorScheme.onSurface,
|
||||
disabledForegroundColor:
|
||||
theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
room.isDirectChat
|
||||
? L10n.of(context).directChat
|
||||
: displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: room.isDirectChat
|
||||
? null
|
||||
: () => context.push(
|
||||
'/rooms/${controller.roomId}/details/invite?filter=participants',
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.group_outlined,
|
||||
size: 14,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
disabledForegroundColor:
|
||||
theme.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
L10n.of(context).countParticipants(
|
||||
actualMembersCount,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
if (room.isRoomAdmin)
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 4,
|
||||
child: IconButton(
|
||||
onPressed: controller.setTopicAction,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32.0,
|
||||
right: 32.0,
|
||||
top: 16.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: room.topic.isEmpty
|
||||
? room.isSpace
|
||||
? L10n.of(context).noSpaceDescriptionYet
|
||||
: L10n.of(context).noChatDescriptionYet
|
||||
: room.topic,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.blueAccent,
|
||||
decorationColor: Colors.blueAccent,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: room.topic.isEmpty
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
color: theme.textTheme.bodyMedium!.color,
|
||||
decorationColor:
|
||||
theme.textTheme.bodyMedium!.color,
|
||||
),
|
||||
onOpen: (url) =>
|
||||
UrlLauncher(context, url.url).launchUrl(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: RoomDetailsButtonRow(
|
||||
controller: controller,
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: RoomParticipantsSection(room: room),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomDetailsButtonRow extends StatefulWidget {
|
||||
final ChatDetailsController controller;
|
||||
final Room room;
|
||||
|
||||
const RoomDetailsButtonRow({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RoomDetailsButtonRow> createState() => RoomDetailsButtonRowState();
|
||||
}
|
||||
|
||||
class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
||||
StreamSubscription? notificationChangeSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notificationChangeSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.listen(
|
||||
(u) => setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
notificationChangeSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final double _buttonHeight = 84.0;
|
||||
final double _miniButtonWidth = 50.0;
|
||||
|
||||
Room get room => widget.room;
|
||||
|
||||
List<ButtonDetails> _buttons(BuildContext context) {
|
||||
final L10n l10n = L10n.of(context);
|
||||
return [
|
||||
ButtonDetails(
|
||||
title: l10n.activities,
|
||||
icon: const Icon(Icons.event_note_outlined, size: 30.0),
|
||||
onPressed: () => context.go("/rooms/${room.id}/details/planner"),
|
||||
visible: room.isSpace,
|
||||
enabled: room.canChangeStateEvent(PangeaEventTypes.activityPlan) &&
|
||||
room.isSpace,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.permissions,
|
||||
icon: const Icon(Icons.edit_attributes_outlined, size: 30.0),
|
||||
onPressed: () => context.go('/rooms/${room.id}/details/permissions'),
|
||||
visible: (room.isRoomAdmin && !room.isDirectChat) || room.isSpace,
|
||||
enabled: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.access,
|
||||
icon: const Icon(Icons.shield_outlined, size: 30.0),
|
||||
onPressed: () => context.go('/rooms/${room.id}/details/access'),
|
||||
visible: room.isSpace && room.spaceParents.isEmpty,
|
||||
enabled: room.isSpace && room.isRoomAdmin,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: room.pushRuleState == PushRuleState.notify
|
||||
? l10n.notificationsOn
|
||||
: l10n.notificationsOff,
|
||||
icon: Icon(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? Icons.notifications_on_outlined
|
||||
: Icons.notifications_off_outlined,
|
||||
size: 30.0,
|
||||
),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? PushRuleState.mentionsOnly
|
||||
: PushRuleState.notify,
|
||||
),
|
||||
),
|
||||
visible: !room.isSpace,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.invite,
|
||||
icon: const Icon(Icons.person_add_outlined, size: 30.0),
|
||||
onPressed: () {
|
||||
String filter = 'knocking';
|
||||
if (room.getParticipants([Membership.knock]).isEmpty) {
|
||||
filter = room.pangeaSpaceParents.isNotEmpty ? 'space' : 'contacts';
|
||||
}
|
||||
context.go('/rooms/${room.id}/details/invite?filter=$filter');
|
||||
},
|
||||
visible: (room.canInvite && !room.isDirectChat) || room.isSpace,
|
||||
enabled: room.canInvite && !room.isDirectChat,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.addSubspace,
|
||||
icon: const Icon(Icons.add_outlined, size: 30.0),
|
||||
onPressed: widget.controller.addSubspace,
|
||||
visible: room.isSpace &&
|
||||
room.canChangeStateEvent(
|
||||
EventTypes.SpaceChild,
|
||||
),
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.spaceAnalytics,
|
||||
icon: const Icon(Icons.bar_chart, size: 30.0),
|
||||
onPressed: () => context.go('/rooms/${room.id}/details/analytics'),
|
||||
visible: room.isSpace,
|
||||
enabled: room.isSpace && room.isRoomAdmin,
|
||||
showInMainView: true,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.download,
|
||||
icon: const Icon(Icons.download_outlined, size: 30.0),
|
||||
onPressed: widget.controller.downloadChatAction,
|
||||
visible: room.ownPowerLevel >= 50 && !room.isSpace && kIsWeb,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.botSettings,
|
||||
icon: const BotFace(
|
||||
width: 30.0,
|
||||
expression: BotExpression.idle,
|
||||
),
|
||||
onPressed: () => showDialog<BotOptionsModel?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => ConversationBotSettingsDialog(
|
||||
room: room,
|
||||
onSubmit: widget.controller.setBotOptions,
|
||||
),
|
||||
),
|
||||
visible: !room.isSpace &&
|
||||
(!room.isDirectChat || room.botOptions != null) &&
|
||||
room.canInvite,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.chatCapacity,
|
||||
icon: const Icon(Icons.reduce_capacity, size: 30.0),
|
||||
onPressed: widget.controller.setRoomCapacity,
|
||||
visible:
|
||||
!room.isSpace && !room.isDirectChat && room.canSendDefaultStates,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.leave,
|
||||
icon: const Icon(Icons.logout_outlined, size: 30.0),
|
||||
onPressed: () async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).leave,
|
||||
cancelLabel: L10n.of(context).no,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).leaveSpaceDescription
|
||||
: L10n.of(context).leaveRoomDescription,
|
||||
isDestructive: true,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.isSpace ? room.leaveSpace : room.leave,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
},
|
||||
visible: room.membership == Membership.join,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.delete,
|
||||
icon: const Icon(Icons.delete_outline, size: 30.0),
|
||||
onPressed: () async {
|
||||
if (room.isSpace) {
|
||||
final resp = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (_) => DeleteSpaceDialog(space: room),
|
||||
);
|
||||
|
||||
if (resp == true) {
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
} else {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).delete,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).deleteSpaceDesc
|
||||
: L10n.of(context).deleteChatDesc,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (resp.isError) return;
|
||||
context.go("/rooms?spaceId=clear");
|
||||
}
|
||||
},
|
||||
visible: room.isRoomAdmin && !room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttons = _buttons(context)
|
||||
.where(
|
||||
(button) => button.visible,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final fullButtonCapacity = (availableWidth / 120.0).floor() - 1;
|
||||
|
||||
final mini = fullButtonCapacity < 4;
|
||||
|
||||
final List<ButtonDetails> mainViewButtons =
|
||||
buttons.where((button) => button.showInMainView).toList();
|
||||
final List<ButtonDetails> otherButtons =
|
||||
buttons.where((button) => !button.showInMainView).toList();
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(mainViewButtons.length + 1, (index) {
|
||||
if (index == mainViewButtons.length) {
|
||||
if (otherButtons.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
onSelected: (button) => button.onPressed?.call(),
|
||||
itemBuilder: (context) {
|
||||
return otherButtons
|
||||
.map(
|
||||
(button) => PopupMenuItem(
|
||||
value: button,
|
||||
child: Row(
|
||||
children: [
|
||||
button.icon,
|
||||
const SizedBox(width: 8),
|
||||
Text(button.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: ButtonDetails(
|
||||
title: L10n.of(context).more,
|
||||
icon: const Icon(Icons.more_horiz_outlined),
|
||||
visible: true,
|
||||
),
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final button = mainViewButtons[index];
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: button,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomDetailsButton extends StatelessWidget {
|
||||
final bool mini;
|
||||
final double height;
|
||||
|
||||
final ButtonDetails buttonDetails;
|
||||
|
||||
const RoomDetailsButton({
|
||||
super.key,
|
||||
required this.buttonDetails,
|
||||
required this.mini,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!buttonDetails.visible) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return TooltipVisibility(
|
||||
visible: mini,
|
||||
child: Tooltip(
|
||||
message: buttonDetails.title,
|
||||
child: AbsorbPointer(
|
||||
absorbing: !buttonDetails.enabled,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return GestureDetector(
|
||||
onTap: buttonDetails.onPressed,
|
||||
child: Opacity(
|
||||
opacity: buttonDetails.enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: hovered
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: EdgeInsets.all(mini ? 6 : 12.0),
|
||||
child: mini
|
||||
? buttonDetails.icon
|
||||
: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
buttonDetails.icon,
|
||||
Text(
|
||||
buttonDetails.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonDetails {
|
||||
final String title;
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final bool visible;
|
||||
final bool enabled;
|
||||
final bool showInMainView;
|
||||
|
||||
const ButtonDetails({
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.visible,
|
||||
this.onPressed,
|
||||
this.enabled = true,
|
||||
this.showInMainView = true,
|
||||
});
|
||||
}
|
||||
|
||||
class RoomParticipantsSection extends StatelessWidget {
|
||||
final Room room;
|
||||
|
||||
const RoomParticipantsSection({
|
||||
required this.room,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double _width = 100.0;
|
||||
final double _spacing = 15.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return LoadParticipantsUtil(
|
||||
space: room,
|
||||
builder: (participantsLoader) {
|
||||
final filteredParticipants =
|
||||
participantsLoader.filteredParticipants("");
|
||||
|
||||
final originalLeaders = filteredParticipants.take(3).toList();
|
||||
filteredParticipants.sort((a, b) {
|
||||
// always sort bot to the end
|
||||
final aIsBot = a.id == BotName.byEnvironment;
|
||||
final bIsBot = b.id == BotName.byEnvironment;
|
||||
if (aIsBot && !bIsBot) {
|
||||
return 1;
|
||||
} else if (bIsBot && !aIsBot) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// put knocking users at the front
|
||||
if (a.membership == Membership.knock &&
|
||||
b.membership != Membership.knock) {
|
||||
return -1;
|
||||
} else if (b.membership == Membership.knock &&
|
||||
a.membership != Membership.knock) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// then invited users
|
||||
if (a.membership == Membership.invite &&
|
||||
b.membership != Membership.invite) {
|
||||
return -1;
|
||||
} else if (b.membership == Membership.invite &&
|
||||
a.membership != Membership.invite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// then admins
|
||||
if (a.powerLevel == 100 && b.powerLevel != 100) {
|
||||
return -1;
|
||||
} else if (b.powerLevel == 100 && a.powerLevel != 100) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return Wrap(
|
||||
spacing: _spacing,
|
||||
runSpacing: _spacing,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
...filteredParticipants.mapIndexed((index, user) {
|
||||
final permissionBatch = user.powerLevel >= 100
|
||||
? L10n.of(context).admin
|
||||
: user.powerLevel >= 50
|
||||
? L10n.of(context).moderator
|
||||
: '';
|
||||
|
||||
final membershipBatch = switch (user.membership) {
|
||||
Membership.ban => null,
|
||||
Membership.invite => L10n.of(context).invited,
|
||||
Membership.join => null,
|
||||
Membership.knock => L10n.of(context).knocking,
|
||||
Membership.leave => null,
|
||||
};
|
||||
|
||||
final publicProfile = participantsLoader.getAnalyticsProfile(
|
||||
user.id,
|
||||
);
|
||||
|
||||
final leaderIndex = originalLeaders.indexOf(user);
|
||||
LinearGradient? gradient;
|
||||
if (leaderIndex != -1) {
|
||||
gradient = leaderIndex.leaderboardGradient;
|
||||
if (user.id == BotName.byEnvironment ||
|
||||
publicProfile == null ||
|
||||
publicProfile.level == null) {
|
||||
gradient = null;
|
||||
}
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: _width,
|
||||
child: Opacity(
|
||||
opacity: user.membership == Membership.join ? 1.0 : 0.5,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (gradient != null)
|
||||
CircleAvatar(
|
||||
radius: _width / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: gradient,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _width,
|
||||
width: _width,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => showMemberActionsPopupMenu(
|
||||
context: context,
|
||||
user: user,
|
||||
),
|
||||
child: Center(
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: _width - 6.0,
|
||||
presenceUserId: user.id,
|
||||
presenceOffset: const Offset(0, 0),
|
||||
presenceSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
user.calcDisplayname(),
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Container(
|
||||
height: 20.0,
|
||||
alignment: Alignment.center,
|
||||
child: LevelDisplayName(
|
||||
userId: user.id,
|
||||
textStyle: theme.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 24.0,
|
||||
alignment: Alignment.center,
|
||||
child: membershipBatch != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
membershipBatch,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color:
|
||||
theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
)
|
||||
: permissionBatch.isNotEmpty
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.tertiary
|
||||
: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
permissionBatch,
|
||||
style:
|
||||
theme.textTheme.labelSmall?.copyWith(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.onTertiary
|
||||
: theme.colorScheme
|
||||
.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ class PangeaInvitationSelectionController
|
|||
case InvitationFilter.public:
|
||||
return l10n.public;
|
||||
case InvitationFilter.participants:
|
||||
return l10n.classRoster;
|
||||
return l10n.participants;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class PangeaInvitationSelectionView extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
onPressed: () => context.go(
|
||||
room.isSpace ? "/rooms?spaceId=${room.id}" : "/rooms/${room.id}",
|
||||
room.isSpace ? "/rooms/spaces/${room.id}/details" : "/rooms/${room.id}",
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
66
lib/pangea/chat_settings/pages/pangea_room_details.dart
Normal file
66
lib/pangea/chat_settings/pages/pangea_room_details.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/chat_details_content.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/space_details_content.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PangeaRoomDetailsView extends StatelessWidget {
|
||||
final ChatDetailsController controller;
|
||||
|
||||
const PangeaRoomDetailsView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.roomId!);
|
||||
if (room == null || room.membership == Membership.leave) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
),
|
||||
body: Center(
|
||||
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
return StreamBuilder(
|
||||
stream: room.client.onRoomState.stream
|
||||
.where((update) => update.roomId == room.id),
|
||||
builder: (context, snapshot) {
|
||||
var members = room.getParticipants().toList()
|
||||
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
|
||||
members = members.take(10).toList();
|
||||
return Scaffold(
|
||||
appBar: room.isSpace
|
||||
? null
|
||||
: AppBar(
|
||||
leading: controller.widget.embeddedCloseButton ??
|
||||
const Center(child: BackButton()),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(
|
||||
vertical: isColumnMode ? 30.0 : 12.0,
|
||||
horizontal: isColumnMode ? 50.0 : 8.0,
|
||||
),
|
||||
child: MaxWidthBody(
|
||||
maxWidth: 900,
|
||||
showBorder: false,
|
||||
innerPadding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
withScrolling: !room.isSpace,
|
||||
child: room.isSpace
|
||||
? SpaceDetailsContent(controller, room)
|
||||
: ChatDetailsContent(controller, room),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
102
lib/pangea/chat_settings/pages/room_details_buttons.dart
Normal file
102
lib/pangea/chat_settings/pages/room_details_buttons.dart
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/space_details_content.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
|
||||
class ButtonDetails {
|
||||
final String title;
|
||||
final String? description;
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final bool visible;
|
||||
final bool enabled;
|
||||
final bool showInMainView;
|
||||
final bool desctructive;
|
||||
final SpaceSettingsTabs? tab;
|
||||
|
||||
const ButtonDetails({
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.icon,
|
||||
this.visible = true,
|
||||
this.enabled = true,
|
||||
this.onPressed,
|
||||
this.showInMainView = true,
|
||||
this.desctructive = false,
|
||||
this.tab,
|
||||
});
|
||||
}
|
||||
|
||||
class RoomDetailsButton extends StatelessWidget {
|
||||
final bool mini;
|
||||
final double height;
|
||||
final ButtonDetails buttonDetails;
|
||||
|
||||
final bool selected;
|
||||
|
||||
const RoomDetailsButton({
|
||||
super.key,
|
||||
required this.buttonDetails,
|
||||
required this.mini,
|
||||
required this.height,
|
||||
this.selected = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!buttonDetails.visible) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return TooltipVisibility(
|
||||
visible: mini,
|
||||
child: Tooltip(
|
||||
message: buttonDetails.title,
|
||||
child: AbsorbPointer(
|
||||
absorbing: !buttonDetails.enabled,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return GestureDetector(
|
||||
onTap: buttonDetails.onPressed,
|
||||
child: Opacity(
|
||||
opacity: buttonDetails.enabled ? 1.0 : 0.5,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: hovered || selected
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer
|
||||
.withAlpha(200)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: EdgeInsets.all(mini ? 6 : 12.0),
|
||||
child: mini
|
||||
? buttonDetails.icon
|
||||
: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
buttonDetails.icon,
|
||||
Text(
|
||||
buttonDetails.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
281
lib/pangea/chat_settings/pages/room_participants_widget.dart
Normal file
281
lib/pangea/chat_settings/pages/room_participants_widget.dart
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
|
||||
|
||||
class RoomParticipantsSection extends StatelessWidget {
|
||||
final Room room;
|
||||
|
||||
const RoomParticipantsSection({
|
||||
required this.room,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double _width = 100.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return LoadParticipantsUtil(
|
||||
space: room,
|
||||
builder: (participantsLoader) {
|
||||
final filteredParticipants = participantsLoader.sortedParticipants();
|
||||
|
||||
final originalLeaders = filteredParticipants.take(3).toList();
|
||||
filteredParticipants.sort((a, b) {
|
||||
// always sort bot to the end
|
||||
final aIsBot = a.id == BotName.byEnvironment;
|
||||
final bIsBot = b.id == BotName.byEnvironment;
|
||||
if (aIsBot && !bIsBot) {
|
||||
return 1;
|
||||
} else if (bIsBot && !aIsBot) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// put knocking users at the front
|
||||
if (a.membership == Membership.knock &&
|
||||
b.membership != Membership.knock) {
|
||||
return -1;
|
||||
} else if (b.membership == Membership.knock &&
|
||||
a.membership != Membership.knock) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// then invited users
|
||||
if (a.membership == Membership.invite &&
|
||||
b.membership != Membership.invite) {
|
||||
return -1;
|
||||
} else if (b.membership == Membership.invite &&
|
||||
a.membership != Membership.invite) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// then admins
|
||||
if (a.powerLevel == 100 && b.powerLevel != 100) {
|
||||
return -1;
|
||||
} else if (b.powerLevel == 100 && a.powerLevel != 100) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return Wrap(
|
||||
spacing: 8.0,
|
||||
alignment: WrapAlignment.center,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [...filteredParticipants, null].mapIndexed((index, user) {
|
||||
if (user == null) {
|
||||
return room.canInvite
|
||||
? MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => context.go(
|
||||
"/rooms/${room.id}/details/invite",
|
||||
),
|
||||
child: HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: hovered
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0),
|
||||
width: _width,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Icon(
|
||||
Icons.person_add_outlined,
|
||||
size: 50.0,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).invite,
|
||||
style: const TextStyle(fontSize: 16.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
final permissionBatch = user.powerLevel >= 100
|
||||
? L10n.of(context).admin
|
||||
: user.powerLevel >= 50
|
||||
? L10n.of(context).moderator
|
||||
: '';
|
||||
|
||||
final membershipBatch = switch (user.membership) {
|
||||
Membership.ban => null,
|
||||
Membership.invite => L10n.of(context).invited,
|
||||
Membership.join => null,
|
||||
Membership.knock => L10n.of(context).knocking,
|
||||
Membership.leave => null,
|
||||
};
|
||||
|
||||
final publicProfile = participantsLoader.getAnalyticsProfile(
|
||||
user.id,
|
||||
);
|
||||
|
||||
final leaderIndex = originalLeaders.indexOf(user);
|
||||
LinearGradient? gradient;
|
||||
if (leaderIndex != -1) {
|
||||
gradient = leaderIndex.leaderboardGradient;
|
||||
if (user.id == BotName.byEnvironment ||
|
||||
publicProfile == null ||
|
||||
publicProfile.level == null) {
|
||||
gradient = null;
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: SizedBox(
|
||||
width: _width,
|
||||
child: Opacity(
|
||||
opacity: user.membership == Membership.join ? 1.0 : 0.5,
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (gradient != null)
|
||||
CircleAvatar(
|
||||
radius: _width / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: gradient,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _width,
|
||||
width: _width,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => showMemberActionsPopupMenu(
|
||||
context: context,
|
||||
user: user,
|
||||
),
|
||||
child: Center(
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: _width - 6.0,
|
||||
presenceUserId: user.id,
|
||||
presenceOffset: const Offset(0, 0),
|
||||
presenceSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
user.calcDisplayname(),
|
||||
style: theme.textTheme.labelLarge?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Container(
|
||||
height: 20.0,
|
||||
alignment: Alignment.center,
|
||||
child: LevelDisplayName(
|
||||
userId: user.id,
|
||||
textStyle: theme.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 24.0,
|
||||
alignment: Alignment.center,
|
||||
child: membershipBatch != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
membershipBatch,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color:
|
||||
theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
)
|
||||
: permissionBatch.isNotEmpty
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.tertiary
|
||||
: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
permissionBatch,
|
||||
style:
|
||||
theme.textTheme.labelSmall?.copyWith(
|
||||
color: user.powerLevel >= 100
|
||||
? theme.colorScheme.onTertiary
|
||||
: theme.colorScheme
|
||||
.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
126
lib/pangea/chat_settings/pages/space_details_button_row.dart
Normal file
126
lib/pangea/chat_settings/pages/space_details_button_row.dart
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/space_details_content.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SpaceDetailsButtonRow extends StatefulWidget {
|
||||
final SpaceSettingsTabs? selectedTab;
|
||||
final Function(SpaceSettingsTabs) onTabSelected;
|
||||
final List<ButtonDetails> buttons;
|
||||
|
||||
final ChatDetailsController controller;
|
||||
final Room room;
|
||||
|
||||
const SpaceDetailsButtonRow({
|
||||
super.key,
|
||||
required this.selectedTab,
|
||||
required this.onTabSelected,
|
||||
required this.buttons,
|
||||
required this.controller,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SpaceDetailsButtonRow> createState() => SpaceDetailsButtonRowState();
|
||||
}
|
||||
|
||||
class SpaceDetailsButtonRowState extends State<SpaceDetailsButtonRow> {
|
||||
StreamSubscription? notificationChangeSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
notificationChangeSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where(
|
||||
(syncUpdate) =>
|
||||
syncUpdate.accountData?.any(
|
||||
(accountData) => accountData.type == 'm.push_rules',
|
||||
) ??
|
||||
false,
|
||||
)
|
||||
.listen(
|
||||
(u) => setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
notificationChangeSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final double _buttonHeight = 84.0;
|
||||
final double _miniButtonWidth = 50.0;
|
||||
|
||||
Room get room => widget.room;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttons = widget.buttons
|
||||
.where(
|
||||
(button) => button.visible,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final fullButtonCapacity = (availableWidth / 120.0).floor() - 1;
|
||||
|
||||
final mini = fullButtonCapacity < 4;
|
||||
|
||||
final List<ButtonDetails> mainViewButtons =
|
||||
buttons.where((button) => button.showInMainView).toList();
|
||||
final List<ButtonDetails> otherButtons =
|
||||
buttons.where((button) => !button.showInMainView).toList();
|
||||
|
||||
return Row(
|
||||
spacing: FluffyThemes.isColumnMode(context) ? 12.0 : 0.0,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(mainViewButtons.length + 1, (index) {
|
||||
if (index == mainViewButtons.length) {
|
||||
if (otherButtons.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: ButtonDetails(
|
||||
title: L10n.of(context).more,
|
||||
icon: const Icon(Icons.more_horiz_outlined),
|
||||
onPressed: () =>
|
||||
widget.onTabSelected(SpaceSettingsTabs.more),
|
||||
),
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
selected: widget.selectedTab == SpaceSettingsTabs.more,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final button = mainViewButtons[index];
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
buttonDetails: button,
|
||||
height: mini ? _miniButtonWidth : _buttonHeight,
|
||||
selected: widget.selectedTab == button.tab,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
429
lib/pangea/chat_settings/pages/space_details_content.dart
Normal file
429
lib/pangea/chat_settings/pages/space_details_content.dart
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_details/chat_details.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_details_buttons.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/room_participants_widget.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/space_details_button_row.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_settings/course_settings.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
enum SpaceSettingsTabs {
|
||||
chat,
|
||||
course,
|
||||
participants,
|
||||
analytics,
|
||||
more,
|
||||
}
|
||||
|
||||
class SpaceDetailsContent extends StatefulWidget {
|
||||
final ChatDetailsController controller;
|
||||
final Room room;
|
||||
|
||||
const SpaceDetailsContent(this.controller, this.room, {super.key});
|
||||
|
||||
@override
|
||||
State<SpaceDetailsContent> createState() => SpaceDetailsContentState();
|
||||
}
|
||||
|
||||
class SpaceDetailsContentState extends State<SpaceDetailsContent> {
|
||||
SpaceSettingsTabs? _selectedTab;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(
|
||||
() => _selectedTab = FluffyThemes.isColumnMode(context)
|
||||
? SpaceSettingsTabs.course
|
||||
: SpaceSettingsTabs.chat,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SpaceDetailsContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.room.id != widget.room.id) {
|
||||
setState(() {
|
||||
_selectedTab = FluffyThemes.isColumnMode(context)
|
||||
? SpaceSettingsTabs.course
|
||||
: SpaceSettingsTabs.chat;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setSelectedTab(SpaceSettingsTabs tab) {
|
||||
setState(() {
|
||||
_selectedTab = tab;
|
||||
});
|
||||
}
|
||||
|
||||
List<ButtonDetails> get _buttons {
|
||||
final L10n l10n = L10n.of(context);
|
||||
return [
|
||||
ButtonDetails(
|
||||
title: l10n.chats,
|
||||
icon: const Icon(Icons.chat_bubble_outline, size: 30.0),
|
||||
onPressed: () => setSelectedTab(SpaceSettingsTabs.chat),
|
||||
visible: !FluffyThemes.isColumnMode(context),
|
||||
tab: SpaceSettingsTabs.chat,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.coursePlan,
|
||||
icon: const Icon(Icons.map_outlined, size: 30.0),
|
||||
onPressed: () => setSelectedTab(SpaceSettingsTabs.course),
|
||||
tab: SpaceSettingsTabs.course,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.participants,
|
||||
icon: const Icon(Icons.group_outlined, size: 30.0),
|
||||
onPressed: () => setSelectedTab(SpaceSettingsTabs.participants),
|
||||
tab: SpaceSettingsTabs.participants,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.stats,
|
||||
icon: const Icon(Symbols.bar_chart_4_bars, size: 30.0),
|
||||
onPressed: () => setSelectedTab(SpaceSettingsTabs.analytics),
|
||||
enabled: widget.room.isRoomAdmin,
|
||||
tab: SpaceSettingsTabs.analytics,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.invite,
|
||||
description: l10n.inviteDesc,
|
||||
icon: const Icon(Icons.person_add_outlined, size: 30.0),
|
||||
onPressed: () {
|
||||
String filter = 'knocking';
|
||||
if (widget.room.getParticipants([Membership.knock]).isEmpty) {
|
||||
filter = widget.room.pangeaSpaceParents.isNotEmpty
|
||||
? 'space'
|
||||
: 'contacts';
|
||||
}
|
||||
context.go('/rooms/${widget.room.id}/details/invite?filter=$filter');
|
||||
},
|
||||
enabled: widget.room.canInvite && !widget.room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.editCourse,
|
||||
description: l10n.editCourseDesc,
|
||||
icon: const Icon(Icons.edit_outlined, size: 30.0),
|
||||
onPressed: () {},
|
||||
visible: false,
|
||||
enabled: widget.room.canChangeStateEvent(PangeaEventTypes.coursePlan),
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.permissions,
|
||||
description: l10n.permissionsDesc,
|
||||
icon: const Icon(Icons.edit_attributes_outlined, size: 30.0),
|
||||
onPressed: () =>
|
||||
context.go('/rooms/${widget.room.id}/details/permissions'),
|
||||
enabled: widget.room.isRoomAdmin && !widget.room.isDirectChat,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.access,
|
||||
description: l10n.accessDesc,
|
||||
icon: const Icon(Icons.shield_outlined, size: 30.0),
|
||||
onPressed: () => context.go('/rooms/${widget.room.id}/details/access'),
|
||||
enabled: widget.room.isRoomAdmin && widget.room.spaceParents.isEmpty,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.createGroupChat,
|
||||
description: l10n.createGroupChatDesc,
|
||||
icon: const Icon(Symbols.chat_add_on, size: 30.0),
|
||||
onPressed: widget.controller.addGroupChat,
|
||||
enabled: widget.room.isRoomAdmin &&
|
||||
widget.room.canChangeStateEvent(
|
||||
EventTypes.SpaceChild,
|
||||
),
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.leave,
|
||||
icon: const Icon(Icons.logout_outlined, size: 30.0),
|
||||
onPressed: () async {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).leave,
|
||||
cancelLabel: L10n.of(context).no,
|
||||
message: L10n.of(context).leaveSpaceDescription,
|
||||
isDestructive: true,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: widget.room.leaveSpace,
|
||||
);
|
||||
if (!resp.isError) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
},
|
||||
enabled: widget.room.membership == Membership.join,
|
||||
showInMainView: false,
|
||||
),
|
||||
ButtonDetails(
|
||||
title: l10n.delete,
|
||||
description: l10n.deleteDesc,
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
size: 30.0,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
onPressed: () async {
|
||||
final resp = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (_) => DeleteSpaceDialog(space: widget.room),
|
||||
);
|
||||
|
||||
if (resp == true) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
},
|
||||
enabled: widget.room.isRoomAdmin && !widget.room.isDirectChat,
|
||||
showInMainView: false,
|
||||
desctructive: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
final displayname = widget.room.getLocalizedDisplayname(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
);
|
||||
return CoursePlanBuilder(
|
||||
courseId: widget.room.coursePlan?.uuid,
|
||||
builder: (context, courseController) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: isColumnMode
|
||||
? CrossAxisAlignment.start
|
||||
: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (isColumnMode) ...[
|
||||
Avatar(
|
||||
mxContent: widget.room.avatar,
|
||||
name: displayname,
|
||||
userId: widget.room.directChatMatrixID,
|
||||
size: Avatar.defaultSize * 2.5,
|
||||
borderRadius: widget.room.isSpace
|
||||
? BorderRadius.circular(24.0)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16.0),
|
||||
],
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 32.0 : 12.0,
|
||||
),
|
||||
),
|
||||
if (isColumnMode && courseController.course != null)
|
||||
CourseInfoChips(
|
||||
courseController.course!,
|
||||
fontSize: 12.0,
|
||||
iconSize: 12.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.room.classCode != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0),
|
||||
child: PopupMenuButton(
|
||||
child: const Icon(Symbols.upload),
|
||||
onSelected: (value) async {
|
||||
final spaceCode = widget.room.classCode!;
|
||||
String toCopy = spaceCode;
|
||||
if (value == 0) {
|
||||
final String initialUrl = kIsWeb
|
||||
? html.window.origin!
|
||||
: Environment.frontendURL;
|
||||
toCopy =
|
||||
"$initialUrl/#/join_with_link?${SpaceConstants.classCode}=${widget.room.classCode}";
|
||||
}
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: toCopy));
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).copiedToClipboard,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<int>>[
|
||||
PopupMenuItem<int>(
|
||||
value: 0,
|
||||
child: ListTile(
|
||||
title: Text(L10n.of(context).shareSpaceLink),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<int>(
|
||||
value: 1,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)
|
||||
.shareInviteCode(widget.room.classCode!),
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: isColumnMode ? 24.0 : 12.0),
|
||||
SpaceDetailsButtonRow(
|
||||
controller: widget.controller,
|
||||
room: widget.room,
|
||||
selectedTab: _selectedTab,
|
||||
onTabSelected: setSelectedTab,
|
||||
buttons: _buttons,
|
||||
),
|
||||
SizedBox(height: isColumnMode ? 30.0 : 14.0),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_selectedTab) {
|
||||
case SpaceSettingsTabs.chat:
|
||||
return CourseChats(
|
||||
widget.room.id,
|
||||
activeChat: null,
|
||||
client: widget.room.client,
|
||||
);
|
||||
case SpaceSettingsTabs.course:
|
||||
return SingleChildScrollView(
|
||||
child: CourseSettings(
|
||||
courseController,
|
||||
room: widget.room,
|
||||
),
|
||||
);
|
||||
case SpaceSettingsTabs.participants:
|
||||
return SingleChildScrollView(
|
||||
child: RoomParticipantsSection(room: widget.room),
|
||||
);
|
||||
case SpaceSettingsTabs.analytics:
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: SpaceAnalytics(roomId: widget.room.id),
|
||||
),
|
||||
);
|
||||
case SpaceSettingsTabs.more:
|
||||
final buttons = _buttons
|
||||
.where(
|
||||
(b) => !b.showInMainView && b.visible,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (courseController.course != null) ...[
|
||||
Text(
|
||||
courseController.course!.description,
|
||||
style: TextStyle(
|
||||
fontSize: isColumnMode ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
SizedBox(height: isColumnMode ? 30.0 : 14.0),
|
||||
],
|
||||
Column(
|
||||
spacing: 10.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: buttons.map((b) {
|
||||
return Opacity(
|
||||
opacity: b.enabled ? 1.0 : 0.5,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
b.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: b.desctructive
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.error
|
||||
: null,
|
||||
),
|
||||
),
|
||||
subtitle: b.description != null
|
||||
? Text(
|
||||
b.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 8.0,
|
||||
color: b.desctructive
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.error
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
leading: b.icon,
|
||||
onTap: b.enabled
|
||||
? () {
|
||||
b.onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
case null:
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -166,6 +166,7 @@ class ModelKey {
|
|||
static const String activityPlanBookmarkId = "activity_id";
|
||||
static const String activityPlanEndAt = "end_at";
|
||||
static const String activityPlanDuration = "duration";
|
||||
static const String activityPlanTopicId = "topic_id";
|
||||
|
||||
static const String activityRequestTopic = "topic";
|
||||
static const String activityRequestMode = "mode";
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/analytics_page_constants.dart';
|
||||
import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
|
||||
class PangeaSideView extends StatelessWidget {
|
||||
final String? path;
|
||||
const PangeaSideView({
|
||||
super.key,
|
||||
required this.path,
|
||||
});
|
||||
|
||||
String get _asset {
|
||||
const defaultAsset = FindYourPeopleConstants.sideBearFileName;
|
||||
if (path == null || path!.isEmpty) return defaultAsset;
|
||||
|
||||
if (path!.contains('analytics')) {
|
||||
return AnalyticsPageConstants.dinoBotFileName;
|
||||
}
|
||||
|
||||
return defaultAsset;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
if (FluffyThemes.isColumnMode(context) ||
|
||||
AppConfig.displayNavigationRail) ...[
|
||||
SpacesNavigationRail(
|
||||
activeSpaceId: null,
|
||||
onGoToChats: () => context.go('/rooms'),
|
||||
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
|
||||
),
|
||||
Container(
|
||||
color: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 250.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: "${AppConfig.assetsBaseURL}/$_asset",
|
||||
errorWidget: (context, url, error) => const SizedBox(),
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
851
lib/pangea/course_chats/course_chats_page.dart
Normal file
851
lib/pangea/course_chats/course_chats_page.dart
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/utils/delete_room.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/widgets/delete_space_dialog.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/course_chats/course_chats_view.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class CourseChats extends StatefulWidget {
|
||||
final Client client;
|
||||
final String roomId;
|
||||
final String? activeChat;
|
||||
|
||||
const CourseChats(
|
||||
this.roomId, {
|
||||
super.key,
|
||||
required this.activeChat,
|
||||
required this.client,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CourseChats> createState() => CourseChatsController();
|
||||
}
|
||||
|
||||
class CourseChatsController extends State<CourseChats> {
|
||||
String get roomId => widget.roomId;
|
||||
Room? get room => widget.client.getRoomById(widget.roomId);
|
||||
|
||||
List<SpaceRoomsChunk>? discoveredChildren;
|
||||
StreamSubscription? _roomSubscription;
|
||||
String? _nextBatch;
|
||||
bool noMoreRooms = false;
|
||||
bool isLoading = false;
|
||||
|
||||
CoursePlanModel? course;
|
||||
String? selectedTopicId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// load full participant list into memory to ensure widgets
|
||||
// that rely on full participants list work as expected
|
||||
final room = widget.client.getRoomById(widget.roomId);
|
||||
room?.requestParticipants().then((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
loadHierarchy(reload: true);
|
||||
|
||||
// Listen for changes to the activeSpace's hierarchy,
|
||||
// and reload the hierarchy when they come through
|
||||
_roomSubscription ??= widget.client.onSync.stream
|
||||
.where(_hasHierarchyUpdate)
|
||||
.listen((update) => loadHierarchy(reload: true));
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CourseChats oldWidget) {
|
||||
// initState doesn't re-run when navigating between spaces
|
||||
// via the navigation rail, so this accounts for that
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.roomId != widget.roomId) {
|
||||
discoveredChildren = null;
|
||||
_nextBatch = null;
|
||||
noMoreRooms = false;
|
||||
|
||||
loadHierarchy(reload: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_roomSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void setCourse(CoursePlanModel? course) {
|
||||
setState(() {
|
||||
this.course = course;
|
||||
});
|
||||
}
|
||||
|
||||
void setSelectedTopicId(String topicID) {
|
||||
setState(() {
|
||||
selectedTopicId = topicID;
|
||||
});
|
||||
}
|
||||
|
||||
int get _selectedTopicIndex =>
|
||||
course?.topics.indexWhere((t) => t.uuid == selectedTopicId) ?? -1;
|
||||
|
||||
bool get canMoveLeft => _selectedTopicIndex > 0;
|
||||
bool get canMoveRight {
|
||||
if (course == null) return false;
|
||||
final endIndex =
|
||||
room?.ownCurrentTopicIndex(course!) ?? (course!.topics.length - 1);
|
||||
return _selectedTopicIndex < endIndex;
|
||||
}
|
||||
|
||||
void moveLeft() {
|
||||
if (canMoveLeft) {
|
||||
setSelectedTopicId(course!.topics[_selectedTopicIndex - 1].uuid);
|
||||
}
|
||||
}
|
||||
|
||||
void moveRight() {
|
||||
if (canMoveRight) {
|
||||
setSelectedTopicId(course!.topics[_selectedTopicIndex + 1].uuid);
|
||||
}
|
||||
}
|
||||
|
||||
Topic? get selectedTopic => course?.topics.firstWhereOrNull(
|
||||
(topic) => topic.uuid == selectedTopicId,
|
||||
);
|
||||
|
||||
Future<void> _joinDefaultChats() async {
|
||||
if (discoveredChildren == null) return;
|
||||
final found = List<SpaceRoomsChunk>.from(discoveredChildren!);
|
||||
|
||||
final List<Future> joinFutures = [];
|
||||
for (final chunk in found) {
|
||||
if (chunk.canonicalAlias == null) continue;
|
||||
final alias = chunk.canonicalAlias!;
|
||||
|
||||
final isDefaultChat = (alias.localpart ?? '')
|
||||
.startsWith(SpaceConstants.announcementsChatAlias) ||
|
||||
(alias.localpart ?? '')
|
||||
.startsWith(SpaceConstants.introductionChatAlias);
|
||||
|
||||
if (!isDefaultChat) continue;
|
||||
|
||||
joinFutures.add(
|
||||
widget.client.joinRoom(alias).then((_) {
|
||||
discoveredChildren?.remove(chunk);
|
||||
}).catchError((e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'alias': alias,
|
||||
'spaceId': widget.roomId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (joinFutures.isNotEmpty) {
|
||||
await Future.wait(joinFutures);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadHierarchy({reload = false}) async {
|
||||
final room = widget.client.getRoomById(widget.roomId);
|
||||
if (room == null) return;
|
||||
|
||||
if (mounted) setState(() => isLoading = true);
|
||||
|
||||
try {
|
||||
await _loadHierarchy(activeSpace: room, reload: reload);
|
||||
await _joinDefaultChats();
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to load hierarchy', e, s);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toLocalizedString(context))),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal logic of loadHierarchy. It will load the hierarchy of
|
||||
/// the active space id (or specified spaceId).
|
||||
/// If [reload] is true, it will reload the entire hierarchy (used when room
|
||||
/// is added/removed from the space)
|
||||
/// If [reload] is false, it will load the next set of rooms
|
||||
Future<void> _loadHierarchy({
|
||||
required Room activeSpace,
|
||||
bool reload = false,
|
||||
}) async {
|
||||
// Load all of the space's state events. Space Child events
|
||||
// are used to filtering out unsuggested, unjoined rooms.
|
||||
await activeSpace.postLoad();
|
||||
|
||||
// The current number of rooms loaded for this space that are visible in the UI
|
||||
final int prevLength = !reload ? (discoveredChildren?.length ?? 0) : 0;
|
||||
|
||||
// Failsafe to prevent too many calls to the server in a row
|
||||
int callsToServer = 0;
|
||||
|
||||
List<SpaceRoomsChunk>? currentHierarchy =
|
||||
discoveredChildren == null || reload
|
||||
? null
|
||||
: List.from(discoveredChildren!);
|
||||
String? currentNextBatch = reload ? null : _nextBatch;
|
||||
|
||||
// Makes repeated calls to the server until 10 new visible rooms have
|
||||
// been loaded, or there are no rooms left to load. Using a loop here,
|
||||
// rather than one single call to the endpoint, because some spaces have
|
||||
// so many invisible rooms (analytics rooms) that it might look like
|
||||
// pressing the 'load more' button does nothing (Because the only rooms
|
||||
// coming through from those calls are analytics rooms).
|
||||
while (callsToServer < 5) {
|
||||
// if this space has been loaded and there are no more rooms to load, break
|
||||
if (currentHierarchy != null && currentNextBatch == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// if this space has been loaded and 10 new rooms have been loaded, break
|
||||
final int currentLength = currentHierarchy?.length ?? 0;
|
||||
if (currentLength - prevLength >= 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
// make the call to the server
|
||||
final response = await widget.client.getSpaceHierarchy(
|
||||
widget.roomId,
|
||||
maxDepth: 1,
|
||||
from: currentNextBatch,
|
||||
limit: 100,
|
||||
);
|
||||
callsToServer++;
|
||||
|
||||
if (response.nextBatch == null) {
|
||||
noMoreRooms = true;
|
||||
}
|
||||
|
||||
// if rooms have earlier been loaded for this space, add those
|
||||
// previously loaded rooms to the front of the response list
|
||||
response.rooms.insertAll(
|
||||
0,
|
||||
currentHierarchy ?? [],
|
||||
);
|
||||
|
||||
// finally, set the response to the last response for this space
|
||||
// and set the current next batch token
|
||||
currentHierarchy = _filterHierarchyResponse(activeSpace, response.rooms);
|
||||
currentNextBatch = response.nextBatch;
|
||||
}
|
||||
|
||||
discoveredChildren = currentHierarchy;
|
||||
discoveredChildren?.sort(_sortSpaceChildren);
|
||||
_nextBatch = currentNextBatch;
|
||||
}
|
||||
|
||||
void onChatTap(Room room) async {
|
||||
if (room.membership == Membership.invite) {
|
||||
final theme = Theme.of(context);
|
||||
final inviteEvent = room.getState(
|
||||
EventTypes.RoomMember,
|
||||
room.client.userID!,
|
||||
);
|
||||
final matrixLocals = MatrixLocals(L10n.of(context));
|
||||
final action = await showAdaptiveDialog<InviteAction>(
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256),
|
||||
child: Center(
|
||||
child: Text(
|
||||
room.getLocalizedDisplayname(matrixLocals),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
content: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 256, maxHeight: 256),
|
||||
child: Text(
|
||||
inviteEvent == null
|
||||
? L10n.of(context).inviteForMe
|
||||
: inviteEvent.content.tryGet<String>('reason') ??
|
||||
L10n.of(context).youInvitedBy(
|
||||
room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
inviteEvent.senderId,
|
||||
)
|
||||
.calcDisplayname(i18n: matrixLocals),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.accept),
|
||||
bigButtons: true,
|
||||
child: Text(L10n.of(context).accept),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.decline),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).decline,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(InviteAction.block),
|
||||
bigButtons: true,
|
||||
child: Text(
|
||||
L10n.of(context).block,
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
switch (action) {
|
||||
case null:
|
||||
return;
|
||||
case InviteAction.accept:
|
||||
break;
|
||||
case InviteAction.decline:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.leave(),
|
||||
);
|
||||
return;
|
||||
case InviteAction.block:
|
||||
final userId = inviteEvent?.senderId;
|
||||
context.go('/rooms/settings/security/ignorelist', extra: userId);
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
final joinResult = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final waitForRoom = room.client.waitForRoomInSync(
|
||||
room.id,
|
||||
join: true,
|
||||
);
|
||||
await room.join();
|
||||
await waitForRoom;
|
||||
},
|
||||
exceptionContext: ExceptionContext.joinRoom,
|
||||
);
|
||||
if (joinResult.error != null) return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.ban) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context).youHaveBeenBannedFromThisChat),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.membership == Membership.leave) {
|
||||
context.go('/rooms/archive/${room.id}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (room.isSpace) {
|
||||
context.go("/rooms/spaces/${room.id}/details");
|
||||
return;
|
||||
}
|
||||
|
||||
context.go('/rooms/${room.id}');
|
||||
}
|
||||
|
||||
void joinChildRoom(SpaceRoomsChunk item) async {
|
||||
final space = widget.client.getRoomById(widget.roomId);
|
||||
final joined = await PublicRoomBottomSheet.show(
|
||||
context: context,
|
||||
chunk: item,
|
||||
via: space?.spaceChildren
|
||||
.firstWhereOrNull(
|
||||
(child) => child.roomId == item.roomId,
|
||||
)
|
||||
?.via,
|
||||
);
|
||||
if (mounted && joined == true) {
|
||||
setState(() {
|
||||
discoveredChildren?.remove(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void chatContextAction(
|
||||
Room room,
|
||||
BuildContext posContext, [
|
||||
Room? space,
|
||||
]) async {
|
||||
final overlay =
|
||||
Overlay.of(posContext).context.findRenderObject() as RenderBox;
|
||||
|
||||
final button = posContext.findRenderObject() as RenderBox;
|
||||
|
||||
final position = RelativeRect.fromRect(
|
||||
Rect.fromPoints(
|
||||
button.localToGlobal(const Offset(0, -65), ancestor: overlay),
|
||||
button.localToGlobal(
|
||||
button.size.bottomRight(Offset.zero) + const Offset(-50, 0),
|
||||
ancestor: overlay,
|
||||
),
|
||||
),
|
||||
Offset.zero & overlay.size,
|
||||
);
|
||||
|
||||
final displayname =
|
||||
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)));
|
||||
|
||||
final spacesWithPowerLevels = room.client.rooms
|
||||
.where(
|
||||
(space) =>
|
||||
space.isSpace &&
|
||||
space.canChangeStateEvent(EventTypes.SpaceChild) &&
|
||||
!space.spaceChildren.any((c) => c.roomId == room.id),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final action = await showMenu<ChatContextAction>(
|
||||
context: posContext,
|
||||
position: position,
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.open,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: room.avatar,
|
||||
name: displayname,
|
||||
userId: room.directChatMatrixID,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 128),
|
||||
child: Text(
|
||||
displayname,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
if (space != null)
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.goToSpace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Avatar(
|
||||
mxContent: space.avatar,
|
||||
size: Avatar.defaultSize / 2,
|
||||
name: space.getLocalizedDisplayname(),
|
||||
userId: space.directChatMatrixID,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
L10n.of(context).goToSpace(space.getLocalizedDisplayname()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (room.membership == Membership.join) ...[
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.mute,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? Icons.notifications_on_outlined
|
||||
: Icons.notifications_off_outlined,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? L10n.of(context).notificationsOn
|
||||
: L10n.of(context).notificationsOff,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.markUnread,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
room.markedUnread
|
||||
? Icons.mark_as_unread
|
||||
: Icons.mark_as_unread_outlined,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.markedUnread
|
||||
? L10n.of(context).markAsRead
|
||||
: L10n.of(context).markAsUnread,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.favorite,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.isFavourite
|
||||
? L10n.of(context).unpin
|
||||
: L10n.of(context).pin,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (spacesWithPowerLevels.isNotEmpty &&
|
||||
room.canChangeStateEvent(EventTypes.SpaceParent))
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.addToSpace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.groups_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).addToSpace),
|
||||
],
|
||||
),
|
||||
),
|
||||
// if the room has a parent for which the user has a high enough power level
|
||||
// to set parent's space child events, show option to remove the room from the space
|
||||
if (room.spaceParents.isNotEmpty &&
|
||||
room.canChangeStateEvent(EventTypes.SpaceParent) &&
|
||||
room.pangeaSpaceParents.any(
|
||||
(r) =>
|
||||
r.canChangeStateEvent(EventTypes.SpaceChild) &&
|
||||
r.id == widget.roomId,
|
||||
))
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.removeFromSpace,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.delete_sweep_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).removeFromSpace),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.leave,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.logout_outlined,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
room.membership == Membership.invite
|
||||
? L10n.of(context).delete
|
||||
: L10n.of(context).leave,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (room.isRoomAdmin && !room.isDirectChat)
|
||||
PopupMenuItem(
|
||||
value: ChatContextAction.delete,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.delete_outlined,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
L10n.of(context).delete,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (action == null) return;
|
||||
if (!mounted) return;
|
||||
|
||||
switch (action) {
|
||||
case ChatContextAction.open:
|
||||
onChatTap(room);
|
||||
return;
|
||||
case ChatContextAction.goToSpace:
|
||||
context.go("/rooms/spaces/${space!.id}/details");
|
||||
return;
|
||||
case ChatContextAction.favorite:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setFavourite(!room.isFavourite),
|
||||
);
|
||||
return;
|
||||
case ChatContextAction.markUnread:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.markUnread(!room.markedUnread),
|
||||
);
|
||||
return;
|
||||
case ChatContextAction.mute:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.setPushRuleState(
|
||||
room.pushRuleState == PushRuleState.notify
|
||||
? PushRuleState.mentionsOnly
|
||||
: PushRuleState.notify,
|
||||
),
|
||||
);
|
||||
return;
|
||||
case ChatContextAction.leave:
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).leaveSpaceDescription
|
||||
: L10n.of(context).leaveRoomDescription,
|
||||
okLabel: L10n.of(context).leave,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.isSpace ? room.leaveSpace : room.leave,
|
||||
);
|
||||
if (mounted && !resp.isError) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
|
||||
return;
|
||||
case ChatContextAction.addToSpace:
|
||||
final space = await showModalActionPopup(
|
||||
context: context,
|
||||
title: L10n.of(context).space,
|
||||
actions: spacesWithPowerLevels
|
||||
.map(
|
||||
(space) => AdaptiveModalAction(
|
||||
value: space,
|
||||
label: space
|
||||
.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
if (space == null) return;
|
||||
if (room.isSpace) {
|
||||
final resp = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).addSubspaceWarning,
|
||||
);
|
||||
if (resp == OkCancelResult.cancel) return;
|
||||
}
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => space.addToSpace(room.id),
|
||||
);
|
||||
try {
|
||||
await space.setSpaceChildAccess();
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context).accessSettingsWarning),
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
case ChatContextAction.removeFromSpace:
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final activeSpace = room.client.getRoomById(widget.roomId);
|
||||
if (activeSpace == null) return;
|
||||
await activeSpace.removeSpaceChild(room.id);
|
||||
},
|
||||
);
|
||||
try {
|
||||
await room.resetSpaceChildAccess();
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context).accessSettingsWarning),
|
||||
duration: const Duration(seconds: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
case ChatContextAction.delete:
|
||||
if (room.isSpace) {
|
||||
final resp = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (_) => DeleteSpaceDialog(space: room),
|
||||
);
|
||||
if (resp == true && mounted) {
|
||||
context.go("/rooms");
|
||||
}
|
||||
} else {
|
||||
final confirmed = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context).areYouSure,
|
||||
okLabel: L10n.of(context).delete,
|
||||
cancelLabel: L10n.of(context).cancel,
|
||||
isDestructive: true,
|
||||
message: room.isSpace
|
||||
? L10n.of(context).deleteSpaceDesc
|
||||
: L10n.of(context).deleteChatDesc,
|
||||
);
|
||||
if (confirmed != OkCancelResult.ok) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final resp = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: room.delete,
|
||||
);
|
||||
if (mounted && !resp.isError) {
|
||||
context.go("/rooms/spaces/${widget.roomId}/details");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool _includeSpaceChild(
|
||||
Room space,
|
||||
SpaceRoomsChunk hierarchyMember,
|
||||
) {
|
||||
if (!mounted) return false;
|
||||
final bool isAnalyticsRoom =
|
||||
hierarchyMember.roomType == PangeaRoomTypes.analytics;
|
||||
|
||||
final bool isMember = [Membership.join, Membership.invite].contains(
|
||||
widget.client.getRoomById(hierarchyMember.roomId)?.membership,
|
||||
);
|
||||
|
||||
final bool isSuggested =
|
||||
space.spaceChildSuggestionStatus[hierarchyMember.roomId] ?? true;
|
||||
|
||||
return !isAnalyticsRoom && (isMember || isSuggested);
|
||||
}
|
||||
|
||||
List<SpaceRoomsChunk> _filterHierarchyResponse(
|
||||
Room space,
|
||||
List<SpaceRoomsChunk> hierarchyResponse,
|
||||
) {
|
||||
final List<SpaceRoomsChunk> filteredChildren = [];
|
||||
for (final child in hierarchyResponse) {
|
||||
if (child.roomId == widget.roomId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final room = space.client.getRoomById(child.roomId);
|
||||
if (room != null && room.membership != Membership.leave) {
|
||||
// If the room is already joined or invited, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
final isDuplicate = filteredChildren.any(
|
||||
(filtered) => filtered.roomId == child.roomId,
|
||||
);
|
||||
if (isDuplicate) continue;
|
||||
|
||||
if (_includeSpaceChild(space, child)) {
|
||||
filteredChildren.add(child);
|
||||
}
|
||||
}
|
||||
return filteredChildren;
|
||||
}
|
||||
|
||||
/// Used to filter out sync updates with hierarchy updates for the active
|
||||
/// space so that the view can be auto-reloaded in the room subscription
|
||||
bool _hasHierarchyUpdate(SyncUpdate update) {
|
||||
final joinTimeline = update.rooms?.join?[widget.roomId]?.timeline;
|
||||
final leaveTimeline = update.rooms?.leave?[widget.roomId]?.timeline;
|
||||
if (joinTimeline == null && leaveTimeline == null) return false;
|
||||
final bool hasJoinUpdate = joinTimeline?.events?.any(
|
||||
(event) => event.type == EventTypes.SpaceChild,
|
||||
) ??
|
||||
false;
|
||||
final bool hasLeaveUpdate = leaveTimeline?.events?.any(
|
||||
(event) => event.type == EventTypes.SpaceChild,
|
||||
) ??
|
||||
false;
|
||||
return hasJoinUpdate || hasLeaveUpdate;
|
||||
}
|
||||
|
||||
int _sortSpaceChildren(
|
||||
SpaceRoomsChunk a,
|
||||
SpaceRoomsChunk b,
|
||||
) {
|
||||
final bool aIsSpace = a.roomType == 'm.space';
|
||||
final bool bIsSpace = b.roomType == 'm.space';
|
||||
|
||||
if (aIsSpace && !bIsSpace) {
|
||||
return -1;
|
||||
} else if (!aIsSpace && bIsSpace) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => CourseChatsView(this);
|
||||
}
|
||||
348
lib/pangea/course_chats/course_chats_view.dart
Normal file
348
lib/pangea/course_chats/course_chats_view.dart
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart' as sdk;
|
||||
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_list/chat_list_item.dart';
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/course_chats/course_chats_page.dart';
|
||||
import 'package:fluffychat/pangea/course_chats/unjoined_chat_list_item.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/utils/stream_extension.dart';
|
||||
|
||||
class CourseChatsView extends StatelessWidget {
|
||||
final CourseChatsController controller;
|
||||
const CourseChatsView(
|
||||
this.controller, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = controller.room;
|
||||
if (room == null) {
|
||||
return const Center(
|
||||
child: Icon(
|
||||
Icons.search_outlined,
|
||||
size: 80,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CoursePlanBuilder(
|
||||
courseId: room.coursePlan?.uuid,
|
||||
onFound: (course) {
|
||||
controller.setCourse(course);
|
||||
final topic = room.ownCurrentTopic(course);
|
||||
if (topic != null) controller.setSelectedTopicId(topic.uuid);
|
||||
},
|
||||
builder: (context, courseController) {
|
||||
final Topic? topic = controller.selectedTopic;
|
||||
final List<String> activityIds = topic?.activityIds ?? [];
|
||||
|
||||
final childrenIds =
|
||||
room.spaceChildren.map((c) => c.roomId).whereType<String>().toSet();
|
||||
|
||||
final joinedChats = [];
|
||||
final joinedSessions = [];
|
||||
final joinedRooms = room.client.rooms
|
||||
.where((room) => childrenIds.remove(room.id))
|
||||
.where((room) => !room.isHiddenRoom)
|
||||
.toList();
|
||||
|
||||
for (final joinedRoom in joinedRooms) {
|
||||
if (joinedRoom.isActivitySession) {
|
||||
if (activityIds.contains(joinedRoom.activityPlan?.bookmarkId)) {
|
||||
joinedSessions.add(joinedRoom);
|
||||
}
|
||||
} else {
|
||||
joinedChats.add(joinedRoom);
|
||||
}
|
||||
}
|
||||
|
||||
final discoveredGroupChats = [];
|
||||
final discoveredSessions = [];
|
||||
final discoveredChildren =
|
||||
controller.discoveredChildren ?? <SpaceRoomsChunk>[];
|
||||
|
||||
for (final child in discoveredChildren) {
|
||||
if (child.roomType?.startsWith(PangeaRoomTypes.activitySession) ==
|
||||
true) {
|
||||
if (activityIds.contains(child.roomType!.split(":").last)) {
|
||||
discoveredSessions.add(child);
|
||||
}
|
||||
} else {
|
||||
discoveredGroupChats.add(child);
|
||||
}
|
||||
}
|
||||
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
return StreamBuilder(
|
||||
stream: room.client.onSync.stream
|
||||
.where((s) => s.hasRoomUpdate)
|
||||
.rateLimit(const Duration(seconds: 1)),
|
||||
builder: (context, snapshot) {
|
||||
return Padding(
|
||||
padding: isColumnMode
|
||||
? const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 8.0,
|
||||
)
|
||||
: const EdgeInsets.all(0.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: joinedChats.length +
|
||||
joinedSessions.length +
|
||||
discoveredGroupChats.length +
|
||||
discoveredSessions.length +
|
||||
5,
|
||||
itemBuilder: (context, i) {
|
||||
// courses chats title
|
||||
if (i == 0) {
|
||||
if (isColumnMode) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 24.0),
|
||||
const Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 30.0,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).courseChats,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
const SizedBox(height: 14.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox();
|
||||
}
|
||||
i--;
|
||||
|
||||
// joined group chats
|
||||
if (i < joinedChats.length) {
|
||||
final joinedRoom = joinedChats[i];
|
||||
return ChatListItem(
|
||||
joinedRoom,
|
||||
onTap: () => controller.onChatTap(joinedRoom),
|
||||
onLongPress: (context) => controller.chatContextAction(
|
||||
joinedRoom,
|
||||
context,
|
||||
),
|
||||
activeChat: controller.widget.activeChat == joinedRoom.id,
|
||||
);
|
||||
}
|
||||
i -= joinedChats.length;
|
||||
|
||||
// unjoined group chats
|
||||
if (i < discoveredGroupChats.length) {
|
||||
return UnjoinedChatListItem(
|
||||
chunk: discoveredGroupChats[i],
|
||||
onTap: () =>
|
||||
controller.joinChildRoom(discoveredGroupChats[i]),
|
||||
);
|
||||
}
|
||||
i -= discoveredGroupChats.length;
|
||||
|
||||
if (i == 0) {
|
||||
if (room.coursePlan == null ||
|
||||
(courseController.course == null &&
|
||||
!courseController.loading)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
child: courseController.loading
|
||||
? LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
)
|
||||
: topic != null
|
||||
? Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MouseRegion(
|
||||
cursor: controller.canMoveLeft
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: controller.canMoveLeft
|
||||
? controller.moveLeft
|
||||
: null,
|
||||
child: Opacity(
|
||||
opacity: controller.canMoveLeft
|
||||
? 1.0
|
||||
: 0.3,
|
||||
child: const Icon(
|
||||
Icons.arrow_left,
|
||||
size: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: 6.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.location_on,
|
||||
size: 24.0,
|
||||
),
|
||||
Text(
|
||||
topic.location,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: controller.canMoveRight
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.basic,
|
||||
child: GestureDetector(
|
||||
onTap: controller.canMoveRight
|
||||
? controller.moveRight
|
||||
: null,
|
||||
child: Opacity(
|
||||
opacity: controller.canMoveRight
|
||||
? 1.0
|
||||
: 0.3,
|
||||
child: const Icon(
|
||||
Icons.arrow_right,
|
||||
size: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
i--;
|
||||
|
||||
if (i == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 4.0,
|
||||
left: 24.0,
|
||||
right: 24.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.event_note_outlined,
|
||||
size: 14.0,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).myActivitySessions,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
i--;
|
||||
|
||||
if (i == 0) {
|
||||
return joinedSessions.isEmpty && discoveredSessions.isEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).noSessionsFound,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.map_outlined, size: 24.0),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
i--;
|
||||
|
||||
// joined activity sessions
|
||||
if (i < joinedSessions.length) {
|
||||
final joinedRoom = joinedSessions[i];
|
||||
return ChatListItem(
|
||||
joinedRoom,
|
||||
onTap: () => controller.onChatTap(joinedRoom),
|
||||
onLongPress: (context) => controller.chatContextAction(
|
||||
joinedRoom,
|
||||
context,
|
||||
),
|
||||
activeChat: controller.widget.activeChat == joinedRoom.id,
|
||||
);
|
||||
}
|
||||
i -= joinedSessions.length;
|
||||
|
||||
// unjoined activity sessions
|
||||
if (i < discoveredSessions.length) {
|
||||
return UnjoinedChatListItem(
|
||||
chunk: discoveredSessions[i],
|
||||
onTap: () => controller.joinChildRoom(
|
||||
discoveredSessions[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
i -= discoveredSessions.length;
|
||||
|
||||
if (controller.noMoreRooms) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.loadHierarchy,
|
||||
child: controller.isLoading
|
||||
? LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
)
|
||||
: Text(L10n.of(context).loadMore),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
83
lib/pangea/course_chats/unjoined_chat_list_item.dart
Normal file
83
lib/pangea/course_chats/unjoined_chat_list_item.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class UnjoinedChatListItem extends StatelessWidget {
|
||||
final SpaceRoomsChunk chunk;
|
||||
final VoidCallback onTap;
|
||||
const UnjoinedChatListItem({
|
||||
super.key,
|
||||
required this.chunk,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayname =
|
||||
chunk.name ?? chunk.canonicalAlias ?? L10n.of(context).emptyChat;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -0.5),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
onTap: onTap,
|
||||
leading: Avatar(
|
||||
mxContent: chunk.avatarUrl,
|
||||
name: displayname,
|
||||
userId: Matrix.of(context)
|
||||
.client
|
||||
.getRoomById(chunk.roomId)
|
||||
?.directChatMatrixID,
|
||||
borderRadius: chunk.roomType == 'm.space'
|
||||
? BorderRadius.circular(
|
||||
AppConfig.borderRadius / 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
chunk.numJoinedMembers.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).textTheme.bodyMedium!.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.people_outlined,
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
chunk.topic ??
|
||||
L10n.of(context).countParticipants(
|
||||
chunk.numJoinedMembers,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
101
lib/pangea/course_creation/course_info_chip_widget.dart
Normal file
101
lib/pangea/course_creation/course_info_chip_widget.dart
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
|
||||
class CourseInfoChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
final double fontSize;
|
||||
final double iconSize;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const CourseInfoChip({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.fontSize,
|
||||
required this.iconSize,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CourseInfoChips extends StatelessWidget {
|
||||
final CoursePlanModel course;
|
||||
final double fontSize;
|
||||
final double iconSize;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const CourseInfoChips(
|
||||
this.course, {
|
||||
super.key,
|
||||
required this.fontSize,
|
||||
required this.iconSize,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
CourseInfoChip(
|
||||
icon: Icons.language,
|
||||
text:
|
||||
"${course.baseLanguageDisplay} → ${course.targetLanguageDisplay}",
|
||||
fontSize: fontSize,
|
||||
iconSize: iconSize,
|
||||
padding: padding,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.school,
|
||||
text: course.cefrLevel.string,
|
||||
fontSize: fontSize,
|
||||
iconSize: iconSize,
|
||||
padding: padding,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: L10n.of(context).numModules(course.topics.length),
|
||||
fontSize: fontSize,
|
||||
iconSize: iconSize,
|
||||
padding: padding,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.event_note_outlined,
|
||||
text: L10n.of(context).numActivityPlans(course.activities),
|
||||
fontSize: fontSize,
|
||||
iconSize: iconSize,
|
||||
padding: padding,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
127
lib/pangea/course_creation/course_plan_filter_widget.dart
Normal file
127
lib/pangea/course_creation/course_plan_filter_widget.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/widgets/dropdown_text_button.dart';
|
||||
|
||||
class CoursePlanFilter<T> extends StatefulWidget {
|
||||
final T? value;
|
||||
final List<T> items;
|
||||
|
||||
final void Function(T?) onChanged;
|
||||
final String Function(T?) displayname;
|
||||
|
||||
final bool enableSearch;
|
||||
|
||||
final double fontSize;
|
||||
final double iconSize;
|
||||
|
||||
const CoursePlanFilter({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
required this.displayname,
|
||||
required this.fontSize,
|
||||
required this.iconSize,
|
||||
this.enableSearch = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CoursePlanFilter<T>> createState() => CoursePlanFilterState<T>();
|
||||
}
|
||||
|
||||
class CoursePlanFilterState<T> extends State<CoursePlanFilter<T>> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<T>(
|
||||
customButton: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.displayname(widget.value),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontSize: widget.fontSize,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
size: widget.iconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
value: widget.value,
|
||||
items: [null, ...widget.items]
|
||||
.map(
|
||||
(item) => DropdownMenuItem(
|
||||
value: item,
|
||||
child: DropdownTextButton(
|
||||
text: item == null ? "" : widget.displayname(item),
|
||||
isSelected: item == widget.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: widget.onChanged,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(
|
||||
width: 250,
|
||||
),
|
||||
dropdownSearchData: widget.enableSearch
|
||||
? DropdownSearchData(
|
||||
searchController: _searchController,
|
||||
searchInnerWidgetHeight: 50,
|
||||
searchInnerWidget: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _searchController,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
searchMatchFn: (item, searchValue) {
|
||||
final displayName =
|
||||
widget.displayname(item.value).toLowerCase();
|
||||
|
||||
final search = searchValue.toLowerCase();
|
||||
return displayName.startsWith(search);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
onMenuStateChange: (isOpen) {
|
||||
if (!isOpen) _searchController.clear();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
108
lib/pangea/course_creation/course_plan_tile_widget.dart
Normal file
108
lib/pangea/course_creation/course_plan_tile_widget.dart
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/widgets/hover_builder.dart';
|
||||
|
||||
class CoursePlanTile extends StatelessWidget {
|
||||
final CoursePlanModel course;
|
||||
final VoidCallback onTap;
|
||||
|
||||
final double titleFontSize;
|
||||
final double chipFontSize;
|
||||
final double chipIconSize;
|
||||
|
||||
const CoursePlanTile({
|
||||
super.key,
|
||||
required this.course,
|
||||
required this.onTap,
|
||||
required this.titleFontSize,
|
||||
required this.chipFontSize,
|
||||
required this.chipIconSize,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return HoverBuilder(
|
||||
builder: (context, hovered) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
hovered ? theme.colorScheme.onSurface.withAlpha(10) : null,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: course.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: course.imageUrl!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
course.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
fontSize: chipFontSize,
|
||||
iconSize: chipIconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/pangea/course_creation/new_course_page.dart
Normal file
68
lib/pangea/course_creation/new_course_page.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/course_creation/new_course_view.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_repo.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
class NewCourse extends StatefulWidget {
|
||||
const NewCourse({super.key});
|
||||
|
||||
@override
|
||||
State<NewCourse> createState() => NewCourseController();
|
||||
}
|
||||
|
||||
class NewCourseController extends State<NewCourse> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
List<CoursePlanModel> courses = [];
|
||||
|
||||
LanguageLevelTypeEnum? languageLevelFilter;
|
||||
LanguageModel? instructionLanguageFilter;
|
||||
LanguageModel? targetLanguageFilter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
CourseFilter get _filter {
|
||||
return CourseFilter(
|
||||
targetLanguage: targetLanguageFilter,
|
||||
languageOfInstructions: instructionLanguageFilter,
|
||||
cefrLevel: languageLevelFilter,
|
||||
);
|
||||
}
|
||||
|
||||
void setLanguageLevelFilter(LanguageLevelTypeEnum? level) {
|
||||
languageLevelFilter = level;
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
void setInstructionLanguageFilter(LanguageModel? language) {
|
||||
instructionLanguageFilter = language;
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
void setTargetLanguageFilter(LanguageModel? language) {
|
||||
targetLanguageFilter = language;
|
||||
_loadCourses();
|
||||
}
|
||||
|
||||
Future<void> _loadCourses() async {
|
||||
try {
|
||||
setState(() => loading = true);
|
||||
courses = await CourseRepo.search(filter: _filter);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NewCourseView(this);
|
||||
}
|
||||
143
lib/pangea/course_creation/new_course_view.dart
Normal file
143
lib/pangea/course_creation/new_course_view.dart
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_plan_filter_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_plan_tile_widget.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/new_course_page.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class NewCourseView extends StatelessWidget {
|
||||
final NewCourseController controller;
|
||||
|
||||
const NewCourseView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double titleFontSize = 16.0;
|
||||
const double descFontSize = 12.0;
|
||||
|
||||
const double iconSize = 12.0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).newCourse),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: MaxWidthBody(
|
||||
showBorder: false,
|
||||
withScrolling: false,
|
||||
maxWidth: 500.0,
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).newCourseSubtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
alignment: WrapAlignment.start,
|
||||
children: [
|
||||
CoursePlanFilter<LanguageLevelTypeEnum>(
|
||||
value: controller.languageLevelFilter,
|
||||
onChanged: controller.setLanguageLevelFilter,
|
||||
items: LanguageLevelTypeEnum.values,
|
||||
displayname: (v) =>
|
||||
v?.string ?? L10n.of(context).cefrLevelLabel,
|
||||
fontSize: descFontSize,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
CoursePlanFilter<LanguageModel>(
|
||||
value: controller.instructionLanguageFilter,
|
||||
onChanged: controller.setInstructionLanguageFilter,
|
||||
items: MatrixState
|
||||
.pangeaController.pLanguageStore.baseOptions,
|
||||
displayname: (v) =>
|
||||
v?.getDisplayName(context) ??
|
||||
L10n.of(context).languageOfInstructionsLabel,
|
||||
enableSearch: true,
|
||||
fontSize: descFontSize,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
CoursePlanFilter<LanguageModel>(
|
||||
value: controller.targetLanguageFilter,
|
||||
onChanged: controller.setTargetLanguageFilter,
|
||||
items: MatrixState
|
||||
.pangeaController.pLanguageStore.targetOptions,
|
||||
displayname: (v) =>
|
||||
v?.getDisplayName(context) ??
|
||||
L10n.of(context).targetLanguageLabel,
|
||||
enableSearch: true,
|
||||
fontSize: descFontSize,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (controller.error != null) {
|
||||
return Center(
|
||||
child: ErrorIndicator(
|
||||
message: L10n.of(context).failedToLoadCourses,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.courses.length,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
padding: const EdgeInsetsGeometry.fromLTRB(
|
||||
4.0,
|
||||
4.0,
|
||||
4.0,
|
||||
16.0,
|
||||
),
|
||||
child: CoursePlanTile(
|
||||
course: controller.courses[index],
|
||||
onTap: () => context.go(
|
||||
"/rooms/communities/newcourse/${controller.courses[index].uuid}",
|
||||
),
|
||||
titleFontSize: titleFontSize,
|
||||
chipFontSize: descFontSize,
|
||||
chipIconSize: iconSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/pangea/course_creation/selected_course_page.dart
Normal file
70
lib/pangea/course_creation/selected_course_page.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.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' as sdk;
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/selected_course_view.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/client_spaces_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SelectedCourse extends StatefulWidget {
|
||||
final String courseId;
|
||||
const SelectedCourse(this.courseId, {super.key});
|
||||
|
||||
@override
|
||||
SelectedCourseController createState() => SelectedCourseController();
|
||||
}
|
||||
|
||||
class SelectedCourseController extends State<SelectedCourse> {
|
||||
Future<void> launchCourse(CoursePlanModel course) async {
|
||||
final client = Matrix.of(context).client;
|
||||
Uint8List? avatar;
|
||||
Uri? avatarUrl;
|
||||
if (course.imageUrl != null) {
|
||||
try {
|
||||
final Response response = await http.get(Uri.parse(course.imageUrl!));
|
||||
avatar = response.bodyBytes;
|
||||
avatarUrl = await client.uploadContent(avatar);
|
||||
} catch (e) {
|
||||
debugPrint("Error fetching course image: $e");
|
||||
}
|
||||
}
|
||||
|
||||
final roomId = await client.createPangeaSpace(
|
||||
name: course.title,
|
||||
introChatName: L10n.of(context).introductions,
|
||||
announcementsChatName: L10n.of(context).announcements,
|
||||
visibility: sdk.Visibility.private,
|
||||
joinRules: sdk.JoinRules.knock,
|
||||
initialState: [
|
||||
sdk.StateEvent(
|
||||
type: PangeaEventTypes.coursePlan,
|
||||
content: {
|
||||
"uuid": course.uuid,
|
||||
},
|
||||
),
|
||||
],
|
||||
avatar: avatar,
|
||||
avatarUrl: avatarUrl,
|
||||
spaceChild: 0,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room == null) return;
|
||||
context.go("/rooms/spaces/${room.id}/details");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SelectedCourseView(
|
||||
courseId: widget.courseId,
|
||||
launchCourse: launchCourse,
|
||||
);
|
||||
}
|
||||
320
lib/pangea/course_creation/selected_course_view.dart
Normal file
320
lib/pangea/course_creation/selected_course_view.dart
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class SelectedCourseView extends StatelessWidget {
|
||||
final String courseId;
|
||||
final Future<void> Function(CoursePlanModel course) launchCourse;
|
||||
const SelectedCourseView({
|
||||
super.key,
|
||||
required this.courseId,
|
||||
required this.launchCourse,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
const double titleFontSize = 16.0;
|
||||
const double descFontSize = 12.0;
|
||||
|
||||
const double largeIconSize = 24.0;
|
||||
const double mediumIconSize = 16.0;
|
||||
const double smallIconSize = 12.0;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).newCourse),
|
||||
),
|
||||
body: CoursePlanBuilder(
|
||||
courseId: courseId,
|
||||
onNotFound: () => context.go("/rooms/communities/newcourse"),
|
||||
builder: (context, controller) {
|
||||
final course = controller.course;
|
||||
return MaxWidthBody(
|
||||
showBorder: false,
|
||||
withScrolling: false,
|
||||
maxWidth: 500.0,
|
||||
child: course == null
|
||||
? const Center(child: CircularProgressIndicator.adaptive())
|
||||
: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: ListView.builder(
|
||||
itemCount: course.topics.length + 2,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
child: course.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: course.imageUrl!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
course.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
course.description,
|
||||
style:
|
||||
const TextStyle(fontSize: descFontSize),
|
||||
),
|
||||
CourseInfoChips(
|
||||
course,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.map,
|
||||
size: largeIconSize,
|
||||
),
|
||||
Text(
|
||||
L10n.of(context).coursePlan,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
index--;
|
||||
|
||||
if (index == course.topics.length) {
|
||||
return const SizedBox(height: 150.0);
|
||||
}
|
||||
|
||||
final topic = course.topics[index];
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: topic.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: topic.imageUrl!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 40.0,
|
||||
height: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.title,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
topic.description,
|
||||
style: const TextStyle(
|
||||
fontSize: descFontSize,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry
|
||||
.symmetric(
|
||||
vertical: 2.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: topic.location,
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.event_note_outlined,
|
||||
text: L10n.of(context)
|
||||
.numActivityPlans(
|
||||
topic.activities.length,
|
||||
),
|
||||
fontSize: descFontSize,
|
||||
iconSize: smallIconSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.dividerColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.edit,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).editCourseLater,
|
||||
style:
|
||||
const TextStyle(fontSize: descFontSize),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shield,
|
||||
size: mediumIconSize,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
L10n.of(context).newCourseAccess,
|
||||
style:
|
||||
const TextStyle(fontSize: descFontSize),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
),
|
||||
onPressed: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => launchCourse(course),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).createCourse,
|
||||
style: const TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
326
lib/pangea/course_settings/course_settings.dart
Normal file
326
lib/pangea/course_settings/course_settings.dart
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import 'dart:math';
|
||||
|
||||
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/themes.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.dart';
|
||||
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
import 'package:fluffychat/pangea/course_creation/course_info_chip_widget.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_builder.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
||||
class CourseSettings extends StatelessWidget {
|
||||
final Room room;
|
||||
final CoursePlanController controller;
|
||||
const CourseSettings(
|
||||
this.controller, {
|
||||
super.key,
|
||||
required this.room,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.error != null) {
|
||||
return Center(
|
||||
child: ErrorIndicator(message: L10n.of(context).failedToLoadCourseInfo),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.course == null) {
|
||||
return Center(child: Text(L10n.of(context).noCourseFound));
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
final double titleFontSize = isColumnMode ? 32.0 : 12.0;
|
||||
final double descFontSize = isColumnMode ? 12.0 : 8.0;
|
||||
final double iconSize = isColumnMode ? 16.0 : 12.0;
|
||||
|
||||
final course = controller.course!;
|
||||
final currentTopicIndex = room.currentTopicIndex(
|
||||
room.client.userID!,
|
||||
course,
|
||||
);
|
||||
final topicsToUsers = room.topicsToUsers(course);
|
||||
|
||||
return Column(
|
||||
spacing: isColumnMode ? 30.0 : 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: course.topics.mapIndexed((index, topic) {
|
||||
final unlocked = index <= currentTopicIndex;
|
||||
final usersInTopic = topicsToUsers[topic.uuid] ?? [];
|
||||
return AbsorbPointer(
|
||||
absorbing: !unlocked,
|
||||
child: Opacity(
|
||||
opacity: unlocked ? 1.0 : 0.5,
|
||||
child: Column(
|
||||
spacing: 12.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Row(
|
||||
spacing: 8.0,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
child: topic.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
width: 54.0,
|
||||
height: 54.0,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: topic.imageUrl!,
|
||||
placeholder: (context, url) {
|
||||
return const Center(
|
||||
child:
|
||||
CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
return Container(
|
||||
width: 54.0,
|
||||
height: 54.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme.secondary,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 54.0,
|
||||
height: 54.0,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!unlocked)
|
||||
const Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Icon(Icons.lock, size: 24.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
topic.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: titleFontSize,
|
||||
),
|
||||
),
|
||||
CourseInfoChip(
|
||||
icon: Icons.location_on,
|
||||
text: topic.location,
|
||||
fontSize: descFontSize,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
if (constraints.maxWidth < 700.0)
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsetsGeometry.symmetric(
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: TopicParticipantList(
|
||||
room: room,
|
||||
users: usersInTopic,
|
||||
avatarSize:
|
||||
isColumnMode ? 50.0 : 25.0,
|
||||
overlap: isColumnMode ? 20.0 : 8.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (constraints.maxWidth >= 700.0)
|
||||
TopicParticipantList(
|
||||
room: room,
|
||||
users: usersInTopic,
|
||||
avatarSize: isColumnMode ? 50.0 : 25.0,
|
||||
overlap: isColumnMode ? 20.0 : 8.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (unlocked)
|
||||
SizedBox(
|
||||
height: 210.0,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: topic.activities.length,
|
||||
itemBuilder: (context, index) {
|
||||
final activity = topic.activities[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 24.0),
|
||||
child: ActivityPlannerBuilder(
|
||||
initialActivity: activity,
|
||||
room: room,
|
||||
builder: (activityController) {
|
||||
return ActivitySuggestionCard(
|
||||
controller: activityController,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ActivitySuggestionDialog(
|
||||
controller: activityController,
|
||||
buttonText:
|
||||
L10n.of(context).launchToSpace,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
width: 120.0,
|
||||
height: 200.0,
|
||||
fontSize: 12.0,
|
||||
fontSizeSmall: 8.0,
|
||||
iconSize: 8.0,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TopicParticipantList extends StatelessWidget {
|
||||
final Room room;
|
||||
final List<User> users;
|
||||
final double avatarSize;
|
||||
final int maxVisible;
|
||||
final double overlap;
|
||||
|
||||
const TopicParticipantList({
|
||||
super.key,
|
||||
required this.room,
|
||||
required this.users,
|
||||
this.avatarSize = 50.0,
|
||||
this.maxVisible = 6,
|
||||
this.overlap = 20.0,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxWidth =
|
||||
(avatarSize - overlap) * min(users.length, maxVisible) + overlap;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: maxWidth,
|
||||
height: avatarSize,
|
||||
child: LoadParticipantsUtil(
|
||||
space: room,
|
||||
builder: (participantsLoader) {
|
||||
final publicProfiles = Map.fromEntries(
|
||||
users.map(
|
||||
(u) => MapEntry(
|
||||
u.id,
|
||||
participantsLoader.getAnalyticsProfile(u.id)?.level,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
users.sort((a, b) {
|
||||
final aLevel = publicProfiles[a.id];
|
||||
final bLevel = publicProfiles[b.id];
|
||||
if (aLevel != null && bLevel != null) {
|
||||
return bLevel.compareTo(aLevel);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return Stack(
|
||||
children: users.take(maxVisible).mapIndexed((index, user) {
|
||||
final level = publicProfiles[user.id];
|
||||
final LinearGradient? gradient =
|
||||
level != null ? index.leaderboardGradient : null;
|
||||
return Positioned(
|
||||
left: index * (avatarSize - overlap),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (gradient != null)
|
||||
CircleAvatar(
|
||||
radius: avatarSize / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: gradient,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
),
|
||||
Center(
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: avatarSize - 6.0,
|
||||
userId: user.id,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (users.length > maxVisible)
|
||||
Text(
|
||||
L10n.of(context).additionalParticipants(users.length - maxVisible),
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/pangea/courses/course_plan_builder.dart
Normal file
78
lib/pangea/courses/course_plan_builder.dart
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_repo.dart';
|
||||
|
||||
class CoursePlanBuilder extends StatefulWidget {
|
||||
final String? courseId;
|
||||
final VoidCallback? onNotFound;
|
||||
final Function(CoursePlanModel course)? onFound;
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
CoursePlanController controller,
|
||||
) builder;
|
||||
|
||||
const CoursePlanBuilder({
|
||||
super.key,
|
||||
required this.courseId,
|
||||
required this.builder,
|
||||
this.onNotFound,
|
||||
this.onFound,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CoursePlanBuilder> createState() => CoursePlanController();
|
||||
}
|
||||
|
||||
class CoursePlanController extends State<CoursePlanBuilder> {
|
||||
bool loading = true;
|
||||
Object? error;
|
||||
|
||||
CoursePlanModel? course;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCourse();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CoursePlanBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.courseId != widget.courseId) {
|
||||
_loadCourse();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCourse() async {
|
||||
if (widget.courseId == null) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
error = null;
|
||||
course = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = null;
|
||||
});
|
||||
|
||||
course = await CourseRepo.get(widget.courseId!);
|
||||
course == null
|
||||
? widget.onNotFound?.call()
|
||||
: widget.onFound?.call(course!);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
} finally {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder(context, this);
|
||||
}
|
||||
17
lib/pangea/courses/course_plan_event.dart
Normal file
17
lib/pangea/courses/course_plan_event.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class CoursePlanEvent {
|
||||
final String uuid;
|
||||
|
||||
CoursePlanEvent({required this.uuid});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
};
|
||||
}
|
||||
|
||||
factory CoursePlanEvent.fromJson(Map<String, dynamic> json) {
|
||||
return CoursePlanEvent(
|
||||
uuid: json['uuid'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/pangea/courses/course_plan_model.dart
Normal file
140
lib/pangea/courses/course_plan_model.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
|
||||
|
||||
/// Represents a topic in the course planner response.
|
||||
class Topic {
|
||||
final String title;
|
||||
final String description;
|
||||
final String location;
|
||||
final String uuid;
|
||||
final String? imageUrl;
|
||||
|
||||
final List<ActivityPlanModel> activities;
|
||||
|
||||
Topic({
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.location = "Unknown",
|
||||
required this.uuid,
|
||||
List<ActivityPlanModel>? activities,
|
||||
this.imageUrl,
|
||||
}) : activities = activities ?? [];
|
||||
|
||||
/// Deserialize from JSON
|
||||
factory Topic.fromJson(Map<String, dynamic> json) {
|
||||
return Topic(
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
location: json['location'] as String? ?? "Unknown",
|
||||
uuid: json['uuid'] as String,
|
||||
activities: (json['activities'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ActivityPlanModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
imageUrl: json['image_url'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'description': description,
|
||||
'location': location,
|
||||
'uuid': uuid,
|
||||
'activities': activities.map((e) => e.toJson()).toList(),
|
||||
'image_url': imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
List<String> get activityIds => activities.map((e) => e.bookmarkId).toList();
|
||||
}
|
||||
|
||||
/// Represents a course plan in the course planner response.
|
||||
class CoursePlanModel {
|
||||
final String targetLanguage;
|
||||
final String languageOfInstructions;
|
||||
final LanguageLevelTypeEnum cefrLevel;
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
final String uuid;
|
||||
|
||||
final List<Topic> topics;
|
||||
final String? imageUrl;
|
||||
|
||||
CoursePlanModel({
|
||||
required this.targetLanguage,
|
||||
required this.languageOfInstructions,
|
||||
required this.cefrLevel,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.uuid,
|
||||
List<Topic>? topics,
|
||||
this.imageUrl,
|
||||
}) : topics = topics ?? [];
|
||||
|
||||
int get activities =>
|
||||
topics.map((t) => t.activities.length).reduce((a, b) => a + b);
|
||||
|
||||
LanguageModel? get targetLanguageModel =>
|
||||
PLanguageStore.byLangCode(targetLanguage);
|
||||
|
||||
LanguageModel? get baseLanguageModel =>
|
||||
PLanguageStore.byLangCode(languageOfInstructions);
|
||||
|
||||
String get targetLanguageDisplay =>
|
||||
targetLanguageModel?.langCode.toUpperCase() ??
|
||||
targetLanguage.toUpperCase();
|
||||
|
||||
String get baseLanguageDisplay =>
|
||||
baseLanguageModel?.langCode.toUpperCase() ??
|
||||
languageOfInstructions.toUpperCase();
|
||||
|
||||
String? topicID(String activityID) {
|
||||
for (final topic in topics) {
|
||||
for (final activity in topic.activities) {
|
||||
if (activity.bookmarkId == activityID) {
|
||||
return topic.uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Deserialize from JSON
|
||||
factory CoursePlanModel.fromJson(Map<String, dynamic> json) {
|
||||
return CoursePlanModel(
|
||||
targetLanguage: json['target_language'] as String,
|
||||
languageOfInstructions: json['language_of_instructions'] as String,
|
||||
cefrLevel: LanguageLevelTypeEnumExtension.fromString(json['cefr_level']),
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
uuid: json['uuid'] as String,
|
||||
topics: (json['topics'] as List<dynamic>?)
|
||||
?.map((e) => Topic.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
imageUrl: json['image_url'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Serialize to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'target_language': targetLanguage,
|
||||
'language_of_instructions': languageOfInstructions,
|
||||
'cefr_level': cefrLevel.string,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'uuid': uuid,
|
||||
'topics': topics.map((e) => e.toJson()).toList(),
|
||||
'image_url': imageUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
115
lib/pangea/courses/course_plan_room_extension.dart
Normal file
115
lib/pangea/courses/course_plan_room_extension.dart
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_event.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/course_user_event.dart';
|
||||
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
|
||||
|
||||
extension CoursePlanRoomExtension on Room {
|
||||
CoursePlanEvent? get coursePlan {
|
||||
final event = getState(PangeaEventTypes.coursePlan);
|
||||
if (event == null) return null;
|
||||
return CoursePlanEvent.fromJson(event.content);
|
||||
}
|
||||
|
||||
CourseUserState? _courseUserState(String userID) {
|
||||
final event = getState(
|
||||
PangeaEventTypes.courseUser,
|
||||
userID,
|
||||
);
|
||||
if (event == null) return null;
|
||||
return CourseUserState.fromJson(event.content);
|
||||
}
|
||||
|
||||
CourseUserState? get _ownCourseState => _courseUserState(client.userID!);
|
||||
|
||||
bool _hasCompletedTopic(
|
||||
String userID,
|
||||
String topicID,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
final state = _courseUserState(userID);
|
||||
if (state == null) return false;
|
||||
|
||||
final topicIndex = course.topics.indexWhere(
|
||||
(t) => t.uuid == topicID,
|
||||
);
|
||||
|
||||
if (topicIndex == -1) {
|
||||
throw Exception('Topic not found');
|
||||
}
|
||||
|
||||
final activityIds =
|
||||
course.topics[topicIndex].activities.map((a) => a.bookmarkId).toList();
|
||||
return state.completedActivities(topicID).toSet().containsAll(activityIds);
|
||||
}
|
||||
|
||||
Topic? currentTopic(
|
||||
String userID,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
if (coursePlan == null) return null;
|
||||
final topicIDs = course.topics.map((t) => t.uuid).toList();
|
||||
if (topicIDs.isEmpty) return null;
|
||||
|
||||
final index = topicIDs.indexWhere(
|
||||
(t) => !_hasCompletedTopic(userID, t, course),
|
||||
);
|
||||
|
||||
return index == -1 ? null : course.topics[index];
|
||||
}
|
||||
|
||||
Topic? ownCurrentTopic(CoursePlanModel course) =>
|
||||
currentTopic(client.userID!, course);
|
||||
|
||||
int currentTopicIndex(
|
||||
String userID,
|
||||
CoursePlanModel course,
|
||||
) {
|
||||
if (coursePlan == null) return -1;
|
||||
final topicIDs = course.topics.map((t) => t.uuid).toList();
|
||||
if (topicIDs.isEmpty) return -1;
|
||||
|
||||
final index = topicIDs.indexWhere(
|
||||
(t) => !_hasCompletedTopic(userID, t, course),
|
||||
);
|
||||
|
||||
return index == -1 ? 0 : index;
|
||||
}
|
||||
|
||||
int ownCurrentTopicIndex(CoursePlanModel course) =>
|
||||
currentTopicIndex(client.userID!, course);
|
||||
|
||||
Map<String, List<User>> topicsToUsers(CoursePlanModel course) {
|
||||
final Map<String, List<User>> topicUserMap = {};
|
||||
final users = getParticipants();
|
||||
for (final user in users) {
|
||||
if (user.id == BotName.byEnvironment) continue;
|
||||
final topicIndex = currentTopicIndex(user.id, course);
|
||||
if (topicIndex != -1) {
|
||||
final topicID = course.topics[topicIndex].uuid;
|
||||
topicUserMap.putIfAbsent(topicID, () => []).add(user);
|
||||
}
|
||||
}
|
||||
return topicUserMap;
|
||||
}
|
||||
|
||||
Future<void> finishCourseActivity(
|
||||
String activityID,
|
||||
String topicID,
|
||||
) async {
|
||||
CourseUserState? state = _ownCourseState;
|
||||
state ??= CourseUserState(
|
||||
userID: client.userID!,
|
||||
completedActivities: {},
|
||||
);
|
||||
state.completeActivity(activityID, topicID);
|
||||
await client.setRoomStateWithKey(
|
||||
id,
|
||||
PangeaEventTypes.courseUser,
|
||||
client.userID!,
|
||||
state.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/pangea/courses/course_repo.dart
Normal file
97
lib/pangea/courses/course_repo.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/courses/course_plan_model.dart';
|
||||
import 'package:fluffychat/pangea/courses/test_courses_json.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
|
||||
class CourseFilter {
|
||||
final LanguageModel? targetLanguage;
|
||||
final LanguageModel? languageOfInstructions;
|
||||
final LanguageLevelTypeEnum? cefrLevel;
|
||||
|
||||
CourseFilter({
|
||||
this.targetLanguage,
|
||||
this.languageOfInstructions,
|
||||
this.cefrLevel,
|
||||
});
|
||||
}
|
||||
|
||||
class CourseRepo {
|
||||
static final GetStorage _courseStorage = GetStorage("course_storage");
|
||||
|
||||
static CoursePlanModel? _getCached(String id) {
|
||||
final json = _courseStorage.read(id);
|
||||
if (json != null) {
|
||||
try {
|
||||
return CoursePlanModel.fromJson(json);
|
||||
} catch (e) {
|
||||
_courseStorage.remove(id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CoursePlanModel> _getAllCached() {
|
||||
final keys = _courseStorage.getKeys();
|
||||
return keys
|
||||
.map((key) => _getCached(key))
|
||||
.whereType<CoursePlanModel>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static Future<void> set(CoursePlanModel coursePlan) async {
|
||||
await _courseStorage.write(coursePlan.uuid, coursePlan.toJson());
|
||||
}
|
||||
|
||||
static Future<CoursePlanModel?> get(String id) async {
|
||||
final cached = _getCached(id);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final resp = await search();
|
||||
return resp.firstWhereOrNull((course) => course.uuid == id);
|
||||
}
|
||||
|
||||
static Future<List<CoursePlanModel>> search({CourseFilter? filter}) async {
|
||||
final cached = _getAllCached();
|
||||
if (cached.isNotEmpty) {
|
||||
return cached.filtered(filter);
|
||||
}
|
||||
|
||||
final resp = (courseJson["courses"] as List<dynamic>)
|
||||
.map((json) => CoursePlanModel.fromJson(json))
|
||||
.whereType<CoursePlanModel>()
|
||||
.toList();
|
||||
|
||||
for (final plan in resp) {
|
||||
set(plan);
|
||||
}
|
||||
|
||||
return resp.filtered(filter);
|
||||
}
|
||||
}
|
||||
|
||||
extension on List<CoursePlanModel> {
|
||||
List<CoursePlanModel> filtered(CourseFilter? filter) {
|
||||
return where((course) {
|
||||
final matchesTargetLanguage = filter?.targetLanguage == null ||
|
||||
course.targetLanguage.split("-").first ==
|
||||
filter?.targetLanguage?.langCodeShort;
|
||||
|
||||
final matchesLanguageOfInstructions =
|
||||
filter?.languageOfInstructions == null ||
|
||||
course.languageOfInstructions.split("-").first ==
|
||||
filter?.languageOfInstructions?.langCodeShort;
|
||||
|
||||
final matchesCefrLevel =
|
||||
filter?.cefrLevel == null || course.cefrLevel == filter?.cefrLevel;
|
||||
|
||||
return matchesTargetLanguage &&
|
||||
matchesLanguageOfInstructions &&
|
||||
matchesCefrLevel;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
45
lib/pangea/courses/course_user_event.dart
Normal file
45
lib/pangea/courses/course_user_event.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
class CourseUserState {
|
||||
final String userID;
|
||||
final Map<String, List<String>> _completedActivities;
|
||||
|
||||
CourseUserState({
|
||||
required this.userID,
|
||||
required Map<String, List<String>> completedActivities,
|
||||
}) : _completedActivities = completedActivities;
|
||||
|
||||
void completeActivity(
|
||||
String activityID,
|
||||
String topicID,
|
||||
) {
|
||||
_completedActivities[topicID] ??= [];
|
||||
if (!_completedActivities[topicID]!.contains(activityID)) {
|
||||
_completedActivities[topicID]!.add(activityID);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> completedActivities(String topicID) {
|
||||
return _completedActivities[topicID] ?? [];
|
||||
}
|
||||
|
||||
factory CourseUserState.fromJson(Map<String, dynamic> json) {
|
||||
final Map<String, List<String>> activities = {};
|
||||
final activityEntry =
|
||||
(json['comp_act_by_topic'] as Map<String, dynamic>?) ?? {};
|
||||
|
||||
for (final entry in activityEntry.entries) {
|
||||
activities[entry.key] = List<String>.from(entry.value);
|
||||
}
|
||||
|
||||
return CourseUserState(
|
||||
userID: json['user_id'],
|
||||
completedActivities: activities,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_id': userID,
|
||||
'comp_act_by_topic': _completedActivities,
|
||||
};
|
||||
}
|
||||
}
|
||||
5762
lib/pangea/courses/test_courses_json.dart
Normal file
5762
lib/pangea/courses/test_courses_json.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -47,6 +47,9 @@ class PangeaEventTypes {
|
|||
/// Profile information related to a user's analytics
|
||||
static const profileAnalytics = "pangea.analytics_profile";
|
||||
static const profileActivities = "pangea.activities_profile";
|
||||
|
||||
static const activityRoomIds = "pangea.activity_room_ids";
|
||||
|
||||
/// Relates to course plans
|
||||
static const coursePlan = "pangea.course_plan";
|
||||
static const courseUser = "p.course_user";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
part of "pangea_room_extension.dart";
|
||||
|
||||
extension ChildrenAndParentsRoomExtension on Room {
|
||||
Room? get firstSpaceParent {
|
||||
for (final parent in spaceParents) {
|
||||
if (parent.roomId == null) continue;
|
||||
final room = client.getRoomById(parent.roomId!);
|
||||
if (room != null) return room;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Room> get pangeaSpaceParents => client.rooms
|
||||
.where(
|
||||
(r) => r.isSpace,
|
||||
|
|
@ -37,22 +46,10 @@ extension ChildrenAndParentsRoomExtension on Room {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await _trySetSpaceChild(
|
||||
roomId,
|
||||
suggested: suggested,
|
||||
);
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"roomID": roomId,
|
||||
"childID": child.id,
|
||||
"suggested": suggested,
|
||||
},
|
||||
);
|
||||
}
|
||||
await _trySetSpaceChild(
|
||||
roomId,
|
||||
suggested: suggested,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _trySetSpaceChild(
|
||||
|
|
|
|||
|
|
@ -64,16 +64,14 @@ class FindYourPeopleView extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: isColumnMode
|
||||
? null
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: () => context.push('/rooms/newspace'),
|
||||
icon: const Icon(Icons.add_box_outlined),
|
||||
label: Text(
|
||||
L10n.of(context).space,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => context.push('/rooms/communities/newcourse'),
|
||||
icon: const Icon(Icons.add_box_outlined),
|
||||
label: Text(
|
||||
L10n.of(context).newCourse,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: isColumnMode
|
||||
? const EdgeInsets.symmetric(
|
||||
|
|
@ -194,30 +192,6 @@ class FindYourPeopleView extends StatelessWidget {
|
|||
context,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_box_outlined,
|
||||
color: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
size: 24.0,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
L10n.of(context).createYourSpace,
|
||||
style: TextStyle(
|
||||
color: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
context.push('/rooms/newspace'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class PublicRoomBottomSheet extends StatefulWidget {
|
|||
.getRoomById(chunk!.roomId);
|
||||
|
||||
if (room != null && room.membership == Membership.join) {
|
||||
context.go("/rooms?spaceId=${room.id}");
|
||||
context.go("/rooms/spaces/${room.id}/details");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
|
|||
if (chunk?.roomType != 'm.space' && !client.getRoomById(roomID)!.isSpace) {
|
||||
outerContext.go("/rooms/$roomID");
|
||||
} else {
|
||||
context.go('/rooms?spaceId=$roomID');
|
||||
context.go('/rooms/spaces/$roomID/details');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -24,289 +23,272 @@ class SpaceAnalyticsView extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10n.of(context).spaceAnalytics),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final mini = constraints.maxWidth <= 550;
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(!mini ? 16.0 : 8.0),
|
||||
child: MaxWidthBody(
|
||||
maxWidth: 1000,
|
||||
showBorder: false,
|
||||
child: Column(
|
||||
spacing: !mini ? 24.0 : 12.0,
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final mini = constraints.maxWidth <= 550;
|
||||
return MaxWidthBody(
|
||||
maxWidth: 1000,
|
||||
showBorder: false,
|
||||
child: Column(
|
||||
spacing: !mini ? 24.0 : 12.0,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: !mini ? 12.0 : 4.0,
|
||||
children: [
|
||||
Row(
|
||||
spacing: !mini ? 12.0 : 4.0,
|
||||
children: [
|
||||
_MenuButton(
|
||||
text: L10n.of(context).requestAll,
|
||||
icon: Symbols.approval_delegation,
|
||||
onPressed: controller.requestAllAnalytics,
|
||||
mini: mini,
|
||||
hideLabel: false,
|
||||
),
|
||||
if (controller.room != null &&
|
||||
controller.availableAnalyticsRooms.isNotEmpty &&
|
||||
kIsWeb)
|
||||
_MenuButton(
|
||||
text: L10n.of(context).download,
|
||||
icon: Icons.download,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DownloadAnalyticsDialog(
|
||||
space: controller.room!,
|
||||
analyticsRooms:
|
||||
controller.availableAnalyticsRooms,
|
||||
),
|
||||
);
|
||||
},
|
||||
mini: mini,
|
||||
),
|
||||
],
|
||||
_MenuButton(
|
||||
text: L10n.of(context).requestAll,
|
||||
icon: Symbols.approval_delegation,
|
||||
onPressed: controller.requestAllAnalytics,
|
||||
mini: mini,
|
||||
hideLabel: false,
|
||||
),
|
||||
Row(
|
||||
spacing: !mini ? 12.0 : 4.0,
|
||||
children: [
|
||||
if (controller.lastUpdatedString != null)
|
||||
Text(
|
||||
L10n.of(context).lastUpdated(
|
||||
controller.lastUpdatedString!,
|
||||
if (controller.room != null &&
|
||||
controller.availableAnalyticsRooms.isNotEmpty)
|
||||
_MenuButton(
|
||||
text: L10n.of(context).download,
|
||||
icon: Icons.download,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DownloadAnalyticsDialog(
|
||||
space: controller.room!,
|
||||
analyticsRooms:
|
||||
controller.availableAnalyticsRooms,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: !mini ? 12.0 : 8.0,
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
),
|
||||
_MenuButton(
|
||||
text: L10n.of(context).refresh,
|
||||
icon: Symbols.refresh,
|
||||
onPressed: controller.refresh,
|
||||
mini: mini,
|
||||
),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<LanguageModel>(
|
||||
customButton: Container(
|
||||
height: !mini ? 36.0 : 26.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: !mini ? 8.0 : 4.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (controller.selectedLanguage != null)
|
||||
Text(
|
||||
controller.selectedLanguage!.langCode
|
||||
.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: theme
|
||||
.colorScheme.onPrimaryContainer,
|
||||
fontSize: !mini ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
size: !mini ? 24.0 : 14.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
value: controller.selectedLanguage,
|
||||
items: controller.availableLanguages
|
||||
.map(
|
||||
(item) => DropdownMenuItem(
|
||||
value: item,
|
||||
child: DropdownTextButton(
|
||||
text: item.getDisplayName(context) ??
|
||||
item.displayName,
|
||||
isSelected: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: controller.setSelectedLanguage,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
// This is necessary for the ink response to match our customButton radius.
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(
|
||||
offset: Offset(-160, 0),
|
||||
width: 250,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
mini: mini,
|
||||
),
|
||||
],
|
||||
),
|
||||
controller.initialized
|
||||
? Table(
|
||||
columnWidths: const {0: FlexColumnWidth(2.5)},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: theme.dividerColor),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
spacing: !mini ? 12.0 : 4.0,
|
||||
children: [
|
||||
if (controller.lastUpdatedString != null)
|
||||
Text(
|
||||
L10n.of(context).lastUpdated(
|
||||
controller.lastUpdatedString!,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: !mini ? 12.0 : 8.0,
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
),
|
||||
_MenuButton(
|
||||
text: L10n.of(context).refresh,
|
||||
icon: Symbols.refresh,
|
||||
onPressed: controller.refresh,
|
||||
mini: mini,
|
||||
),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<LanguageModel>(
|
||||
customButton: Container(
|
||||
height: !mini ? 36.0 : 26.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: !mini ? 8.0 : 4.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).viewingAnalytics(
|
||||
controller.completedDownloads,
|
||||
controller.downloads.length,
|
||||
if (controller.selectedLanguage != null)
|
||||
Text(
|
||||
controller.selectedLanguage!
|
||||
.getDisplayName(context) ??
|
||||
controller
|
||||
.selectedLanguage!.displayName,
|
||||
style: TextStyle(
|
||||
color:
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
fontSize: !mini ? 16.0 : 12.0,
|
||||
),
|
||||
),
|
||||
icon: Icons.group_outlined,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).level,
|
||||
icon: Icons.star,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).vocab,
|
||||
icon: Symbols.dictionary,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).grammar,
|
||||
icon: Symbols.toys_and_games,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).activities,
|
||||
icon: Icons.radar,
|
||||
mini: mini,
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
size: !mini ? 24.0 : 14.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
...controller.sortedDownloads.mapIndexed(
|
||||
(index, entry) {
|
||||
final download = entry.value;
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Opacity(
|
||||
opacity: download.requestStatus.opacity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: !mini ? 12.0 : 4.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: !mini ? 16.0 : 8.0,
|
||||
children: [
|
||||
Avatar(
|
||||
size: !mini ? 64.0 : 40.0,
|
||||
mxContent: entry.key.avatarUrl,
|
||||
name:
|
||||
entry.key.calcDisplayname(),
|
||||
userId: entry.key.id,
|
||||
presenceUserId: entry.key.id,
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: index == 0
|
||||
? 8.0
|
||||
: 0.0,
|
||||
),
|
||||
Text(
|
||||
entry.key.id,
|
||||
maxLines: 1,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
!mini ? 16.0 : 12.0,
|
||||
fontWeight:
|
||||
FontWeight.w500,
|
||||
),
|
||||
),
|
||||
_RequestButton(
|
||||
status: download
|
||||
.requestStatus,
|
||||
onPressed: () =>
|
||||
controller
|
||||
.requestAnalytics(
|
||||
entry.key,
|
||||
),
|
||||
mini: mini,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.level?.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.numLemmas
|
||||
.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.numMorphConstructs
|
||||
.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download
|
||||
.summary?.numCompletedActivities
|
||||
.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
value: controller.selectedLanguage,
|
||||
items: controller.availableLanguages
|
||||
.map(
|
||||
(item) => DropdownMenuItem(
|
||||
value: item,
|
||||
child: DropdownTextButton(
|
||||
text: item.getDisplayName(context) ??
|
||||
item.displayName,
|
||||
isSelected: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: controller.setSelectedLanguage,
|
||||
buttonStyleData: ButtonStyleData(
|
||||
// This is necessary for the ink response to match our customButton radius.
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
dropdownStyleData: const DropdownStyleData(
|
||||
offset: Offset(-160, 0),
|
||||
width: 250,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
controller.initialized
|
||||
? Table(
|
||||
columnWidths: const {0: FlexColumnWidth(2.5)},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: theme.dividerColor),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).viewingAnalytics(
|
||||
controller.completedDownloads,
|
||||
controller.downloads.length,
|
||||
),
|
||||
icon: Icons.group_outlined,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).level,
|
||||
icon: Icons.star,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).vocab,
|
||||
icon: Symbols.dictionary,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).grammar,
|
||||
icon: Symbols.toys_and_games,
|
||||
mini: mini,
|
||||
),
|
||||
_TableHeaderCell(
|
||||
text: L10n.of(context).activities,
|
||||
icon: Icons.radar,
|
||||
mini: mini,
|
||||
),
|
||||
],
|
||||
),
|
||||
...controller.sortedDownloads.mapIndexed(
|
||||
(index, entry) {
|
||||
final download = entry.value;
|
||||
return TableRow(
|
||||
children: [
|
||||
TableCell(
|
||||
child: Opacity(
|
||||
opacity: download.requestStatus.opacity,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: !mini ? 12.0 : 4.0,
|
||||
),
|
||||
child: Row(
|
||||
spacing: !mini ? 16.0 : 8.0,
|
||||
children: [
|
||||
Avatar(
|
||||
size: !mini ? 64.0 : 40.0,
|
||||
mxContent: entry.key.avatarUrl,
|
||||
name: entry.key.calcDisplayname(),
|
||||
userId: entry.key.id,
|
||||
presenceUserId: entry.key.id,
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
spacing: 4.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height:
|
||||
index == 0 ? 8.0 : 0.0,
|
||||
),
|
||||
Text(
|
||||
entry.key.id,
|
||||
maxLines: 1,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
!mini ? 16.0 : 12.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
_RequestButton(
|
||||
status:
|
||||
download.requestStatus,
|
||||
onPressed: () => controller
|
||||
.requestAnalytics(
|
||||
entry.key,
|
||||
),
|
||||
mini: mini,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.level?.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.numLemmas.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.numMorphConstructs
|
||||
.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
_TableContentCell(
|
||||
text: download.summary?.numCompletedActivities
|
||||
.toString(),
|
||||
downloadStatus: download.downloadStatus,
|
||||
requestStatus: download.requestStatus,
|
||||
mini: mini,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
: const CircularProgressIndicator.adaptive(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -506,34 +488,22 @@ class _RequestButton extends StatelessWidget {
|
|||
child: Opacity(
|
||||
opacity: status.enabled ? 0.9 : 0.3,
|
||||
child: Container(
|
||||
padding: (status != RequestStatus.unavailable)
|
||||
? const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
)
|
||||
: null,
|
||||
decoration: status != RequestStatus.unavailable
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: status.backgroundColor(context),
|
||||
)
|
||||
: null,
|
||||
child: Wrap(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
color: status.backgroundColor(context),
|
||||
),
|
||||
child: Row(
|
||||
spacing: 8.0,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (status.icon != null)
|
||||
Icon(
|
||||
status.icon,
|
||||
size: !mini ? 12.0 : 8.0,
|
||||
),
|
||||
Icon(
|
||||
status.icon,
|
||||
size: !mini ? 12.0 : 8.0,
|
||||
),
|
||||
Text(
|
||||
status.label(context),
|
||||
style: TextStyle(
|
||||
fontSize: !mini ? 12.0 : 8.0,
|
||||
fontStyle: status == RequestStatus.unavailable
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
style: TextStyle(fontSize: !mini ? 12.0 : 8.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class ClassController extends BaseController {
|
|||
Room? room = client.getRoomByAlias(alias) ?? client.getRoomById(alias);
|
||||
if (room != null) {
|
||||
room.isSpace
|
||||
? context.go("/rooms?spaceId=${room.id}")
|
||||
? context.go("/rooms/spaces/${room.id}/details")
|
||||
: context.go("/rooms/${room.id}");
|
||||
return;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ class ClassController extends BaseController {
|
|||
}
|
||||
|
||||
room.isSpace
|
||||
? context.go("/rooms?spaceId=${room.id}")
|
||||
? context.go("/rooms/spaces/${room.id}/details")
|
||||
: context.go("/rooms/${room.id}");
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ class ClassController extends BaseController {
|
|||
if (!(room?.isSpace ?? true)) {
|
||||
context.go("/rooms/${alreadyJoined.first}");
|
||||
} else {
|
||||
context.go("/rooms?spaceId=${alreadyJoined.first}");
|
||||
context.go("/rooms/spaces/${alreadyJoined.first}/details");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -212,7 +212,7 @@ class ClassController extends BaseController {
|
|||
}
|
||||
|
||||
if (room.isSpace) {
|
||||
context.go("/rooms?spaceId=${room.id}");
|
||||
context.go("/rooms/spaces/${room.id}/details");
|
||||
} else {
|
||||
context.go("/rooms/${room.id}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ extension SpacesClientExtension on Client {
|
|||
JoinRules joinRules = JoinRules.public,
|
||||
Uint8List? avatar,
|
||||
Uri? avatarUrl,
|
||||
List<StateEvent>? initialState,
|
||||
int spaceChild = 50,
|
||||
}) async {
|
||||
final roomId = await createRoom(
|
||||
creationContent: {'type': RoomCreationTypes.mSpace},
|
||||
|
|
@ -26,7 +28,10 @@ extension SpacesClientExtension on Client {
|
|||
name: name.trim(),
|
||||
powerLevelContentOverride: {'events_default': 100},
|
||||
initialState: [
|
||||
RoomDefaults.defaultSpacePowerLevels(userID!),
|
||||
RoomDefaults.defaultSpacePowerLevels(
|
||||
userID!,
|
||||
spaceChild: spaceChild,
|
||||
),
|
||||
await pangeaJoinRules(
|
||||
joinRules.toString().replaceAll('JoinRules.', ''),
|
||||
),
|
||||
|
|
@ -35,6 +40,7 @@ extension SpacesClientExtension on Client {
|
|||
type: EventTypes.RoomAvatar,
|
||||
content: {'url': avatarUrl.toString()},
|
||||
),
|
||||
if (initialState != null) ...initialState,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -69,15 +69,8 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
|
|||
}
|
||||
}
|
||||
|
||||
List<User> filteredParticipants(String filter) {
|
||||
final searchText = filter.toLowerCase();
|
||||
final filtered = participants.where((user) {
|
||||
final displayName = user.displayName?.toLowerCase() ?? '';
|
||||
return displayName.contains(searchText) ||
|
||||
user.id.toLowerCase().contains(searchText);
|
||||
}).toList();
|
||||
|
||||
filtered.sort((a, b) {
|
||||
List<User> sortedParticipants() {
|
||||
participants.sort((a, b) {
|
||||
if (a.id == BotName.byEnvironment) {
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -99,7 +92,7 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
|
|||
return (bProfile?.level ?? 0).compareTo(aProfile?.level ?? 0);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
return participants;
|
||||
}
|
||||
|
||||
Future<void> _cacheLevels() async {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class LeaderboardParticipantListState
|
|||
space: widget.space,
|
||||
builder: (participantsLoader) {
|
||||
final participants = participantsLoader
|
||||
.filteredParticipants("")
|
||||
.sortedParticipants()
|
||||
.where((p) => p.membership == Membership.join)
|
||||
.toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +140,8 @@ abstract class ClientManager {
|
|||
PangeaEventTypes.activitySummary,
|
||||
PangeaEventTypes.constructSummary,
|
||||
PangeaEventTypes.activityRoomIds,
|
||||
PangeaEventTypes.coursePlan,
|
||||
PangeaEventTypes.courseUser,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat_list/chat_list.dart';
|
||||
import 'package:fluffychat/pages/settings/settings.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/analytics_page_constants.dart';
|
||||
import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart';
|
||||
import 'package:fluffychat/widgets/navigation_rail.dart';
|
||||
|
||||
class TwoColumnLayout extends StatelessWidget {
|
||||
final Widget mainView;
|
||||
final Widget sideView;
|
||||
// #Pangea
|
||||
final Color? dividerColor;
|
||||
// final Widget mainView;
|
||||
final GoRouterState state;
|
||||
// Pangea#
|
||||
final Widget sideView;
|
||||
|
||||
const TwoColumnLayout({
|
||||
super.key,
|
||||
required this.mainView,
|
||||
required this.sideView,
|
||||
// #Pangea
|
||||
this.dividerColor,
|
||||
// required this.mainView,
|
||||
required this.state,
|
||||
// Pangea#
|
||||
required this.sideView,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -25,19 +34,37 @@ class TwoColumnLayout extends StatelessWidget {
|
|||
child: Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(),
|
||||
width: FluffyThemes.columnWidth + FluffyThemes.navRailWidth,
|
||||
child: mainView,
|
||||
),
|
||||
Container(
|
||||
width: 1.0,
|
||||
// #Pangea
|
||||
// color: theme.dividerColor,
|
||||
color: dividerColor ?? theme.dividerColor,
|
||||
// #Pangea
|
||||
if (FluffyThemes.isColumnMode(context) ||
|
||||
!(state.fullPath?.endsWith(":roomid") ?? false)) ...[
|
||||
SpacesNavigationRail(
|
||||
activeSpaceId: state.pathParameters['spaceid'],
|
||||
path: state.fullPath,
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
if (FluffyThemes.isColumnMode(context)) ...[
|
||||
// Pangea#
|
||||
),
|
||||
Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: const BoxDecoration(),
|
||||
// #Pangea
|
||||
// width: FluffyThemes.columnWidth + FluffyThemes.navRailWidth,
|
||||
// child: mainView,
|
||||
width: FluffyThemes.columnWidth,
|
||||
child: _MainView(state: state),
|
||||
// Pangea#
|
||||
),
|
||||
Container(
|
||||
width: 1.0,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
// Pangea#
|
||||
],
|
||||
// Pangea#
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
child: sideView,
|
||||
|
|
@ -49,3 +76,59 @@ class TwoColumnLayout extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
class _MainView extends StatelessWidget {
|
||||
final GoRouterState state;
|
||||
|
||||
const _MainView({
|
||||
required this.state,
|
||||
});
|
||||
|
||||
String get _asset {
|
||||
const defaultAsset = FindYourPeopleConstants.sideBearFileName;
|
||||
if (state.fullPath == null || state.fullPath!.isEmpty) return defaultAsset;
|
||||
|
||||
if (state.fullPath!.contains('analytics')) {
|
||||
return AnalyticsPageConstants.dinoBotFileName;
|
||||
}
|
||||
|
||||
return defaultAsset;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final path = state.fullPath;
|
||||
if (path == null) {
|
||||
return ChatList(
|
||||
activeChat: state.pathParameters['roomid'],
|
||||
activeSpaceId: state.pathParameters['spaceid'],
|
||||
);
|
||||
}
|
||||
|
||||
if (path.contains("settings")) {
|
||||
return Settings(key: state.pageKey);
|
||||
}
|
||||
|
||||
if (['communities', 'analytics'].any((p) => path.contains(p))) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 250.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: "${AppConfig.assetsBaseURL}/$_asset",
|
||||
errorWidget: (context, url, error) => const SizedBox(),
|
||||
placeholder: (context, url) => const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ChatList(
|
||||
activeChat: state.pathParameters['roomid'],
|
||||
activeSpaceId: state.pathParameters['spaceid'],
|
||||
);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -15,18 +15,18 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
|
||||
class SpacesNavigationRail extends StatelessWidget {
|
||||
final String? activeSpaceId;
|
||||
final void Function() onGoToChats;
|
||||
final void Function(String) onGoToSpaceId;
|
||||
// #Pangea
|
||||
final void Function()? clearActiveSpace;
|
||||
// final void Function() onGoToChats;
|
||||
// final void Function(String) onGoToSpaceId;
|
||||
final String? path;
|
||||
// Pangea#
|
||||
|
||||
const SpacesNavigationRail({
|
||||
required this.activeSpaceId,
|
||||
required this.onGoToChats,
|
||||
required this.onGoToSpaceId,
|
||||
// #Pangea
|
||||
this.clearActiveSpace,
|
||||
// required this.onGoToChats,
|
||||
// required this.onGoToSpaceId,
|
||||
required this.path,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -41,9 +41,8 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
.path
|
||||
.startsWith('/rooms/settings');
|
||||
// #Pangea
|
||||
final path = GoRouter.of(context).routeInformationProvider.value.uri.path;
|
||||
final isAnalytics = path.contains('analytics');
|
||||
final isCommunities = path.contains('communities');
|
||||
final isAnalytics = path?.contains('analytics') ?? false;
|
||||
final isCommunities = path?.contains('communities') ?? false;
|
||||
final isColumnMode = FluffyThemes.isColumnMode(context);
|
||||
|
||||
final width = isColumnMode
|
||||
|
|
@ -91,7 +90,6 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
return NaviRailItem(
|
||||
isSelected: isAnalytics,
|
||||
onTap: () {
|
||||
clearActiveSpace?.call();
|
||||
context.go("/rooms/analytics");
|
||||
},
|
||||
backgroundColor: Colors.transparent,
|
||||
|
|
@ -127,9 +125,7 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
!isSettings &&
|
||||
!isAnalytics &&
|
||||
!isCommunities,
|
||||
// Pangea#
|
||||
onTap: onGoToChats,
|
||||
// #Pangea
|
||||
// onTap: onGoToChats,
|
||||
// icon: const Padding(
|
||||
// padding: EdgeInsets.all(10.0),
|
||||
// child: Icon(Icons.forum_outlined),
|
||||
|
|
@ -140,6 +136,7 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
// ),
|
||||
icon: const Icon(Icons.forum_outlined),
|
||||
selectedIcon: const Icon(Icons.forum),
|
||||
onTap: () => context.go("/rooms"),
|
||||
// Pangea#
|
||||
toolTip: L10n.of(context).chats,
|
||||
unreadBadgeFilter: (room) => true,
|
||||
|
|
@ -158,7 +155,6 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
// toolTip: L10n.of(context).createNewSpace,
|
||||
isSelected: isCommunities,
|
||||
onTap: () {
|
||||
clearActiveSpace?.call();
|
||||
context.go('/rooms/communities');
|
||||
},
|
||||
icon: const Icon(Icons.groups),
|
||||
|
|
@ -186,7 +182,9 @@ class SpacesNavigationRail extends StatelessWidget {
|
|||
room,
|
||||
);
|
||||
} else {
|
||||
onGoToSpaceId(rootSpaces[i].id);
|
||||
context.go(
|
||||
"/rooms/spaces/${rootSpaces[i].id}/details",
|
||||
);
|
||||
}
|
||||
},
|
||||
// Pangea#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue