merge prod into main

This commit is contained in:
ggurdin 2025-12-02 10:22:56 -05:00
commit 22417c6c4a
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
10 changed files with 263 additions and 48 deletions

View file

@ -4961,5 +4961,32 @@
"introChatTitle": "Create Introductions Chat",
"introChatDesc": "Anyone in the space can post.",
"announcementsChatTitle": "Announcements Chat",
"announcementsChatDesc": "Only space admin can post."
"announcementsChatDesc": "Only space admin can post.",
"notStartedActivitiesTitle": "Open sessions ({num})",
"@notStartedActivitiesTitle": {
"type": "String",
"placeholders": {
"num": {
"type": "int"
}
}
},
"inProgressActivitiesTitle": "Happening now ({num})",
"@inProgressActivitiesTitle": {
"type": "String",
"placeholders": {
"num": {
"type": "int"
}
}
},
"completedActivitiesTitle": "Done ({num})",
"@completedActivitiesTitle": {
"type": "String",
"placeholders": {
"num": {
"type": "int"
}
}
}
}

View file

@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/bot_join_error_dialog.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_translation_request.dart';
@ -227,6 +228,9 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
);
}
Map<ActivitySummaryStatus, Map<String, RoomSummaryResponse>>
get activityStatuses => activitySessionStatuses(widget.activityId);
void toggleInstructions() {
setState(() {
showInstructions = !showInstructions;
@ -436,6 +440,33 @@ class ActivitySessionStartController extends State<ActivitySessionStartPage>
return joinedSessionId;
}
Future<void> joinActivityByRoomId(String roomId) async {
final resp = await showFutureLoadingDialog(
context: context,
future: () async {
await courseParent!.client.joinRoom(
roomId,
via: courseParent?.spaceChildren
.firstWhereOrNull(
(child) => child.roomId == roomId,
)
?.via,
);
final room = courseParent!.client.getRoomById(roomId);
if (room == null || room.membership != Membership.join) {
await courseParent!.client.waitForRoomInSync(roomId, join: true);
}
},
);
if (!resp.isError) {
widget.parentId != null
? context.go("/rooms/spaces/${widget.parentId}/$roomId")
: context.go("/rooms/$roomId");
}
}
Future<void> pingCourse() async {
if (activityRoom?.courseParent == null) {
throw Exception("Activity is not part of a course");

View file

@ -1,6 +1,7 @@
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';
@ -9,13 +10,17 @@ import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.da
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/common/widgets/feedback_dialog.dart';
import 'package:fluffychat/pangea/course_chats/open_roles_indicator.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/activity_summaries_provider.dart';
import 'package:fluffychat/pangea/course_plans/course_activities/course_activity_repo.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/member_actions_popup_menu_button.dart';
class ActivitySessionStartView extends StatelessWidget {
final ActivitySessionStartController controller;
@ -148,7 +153,6 @@ class ActivitySessionStartView extends StatelessWidget {
),
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12.0,
children: [
ActivitySummary(
activity: controller.activity!,
@ -167,6 +171,12 @@ class ActivitySessionStartView extends StatelessWidget {
controller.canSelectParticipant,
assignedRoles: controller.assignedRoles,
),
if (controller.courseParent != null)
_ActivityStatuses(
statuses: controller.activityStatuses,
space: controller.courseParent!,
onTap: controller.joinActivityByRoomId,
),
],
),
),
@ -403,3 +413,80 @@ class _ActivityStartButtons extends StatelessWidget {
);
}
}
class _ActivityStatuses extends StatelessWidget {
final Map<ActivitySummaryStatus, Map<String, RoomSummaryResponse>> statuses;
final Room space;
final Function(String) onTap;
const _ActivityStatuses({
required this.statuses,
required this.space,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Column(
children: [
...ActivitySummaryStatus.values.map(
(status) {
final entry = statuses[status];
if (entry!.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsetsGeometry.symmetric(
horizontal: 20.0,
vertical: 16.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
status.label(L10n.of(context), entry.length),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
...entry.entries.map((e) {
final summary = e.value;
final roomId = e.key;
return ListTile(
title: OpenRolesIndicator(
roles: summary.activityPlan.roles.values.toList(),
assignedRoles:
summary.activityRoles.roles.values.toList(),
size: 40.0,
spacing: 8.0,
space: space,
onUserTap: (user, context) {
showMemberActionsPopupMenu(
context: context,
user: user,
);
},
),
trailing: const Icon(Icons.arrow_forward),
onTap: space.isRoomAdmin ? () => onTap(roomId) : null,
);
}),
],
),
);
},
),
],
),
);
}
}

View file

@ -54,9 +54,8 @@ class ChatListItemSubtitle extends StatelessWidget {
);
} else if (!room.isActivityStarted) {
return OpenRolesIndicator(
totalSlots: room.activityPlan!.req.numberOfParticipants,
userIds:
room.assignedRoles?.values.map((r) => r.userId).toList() ?? [],
roles: room.activityPlan!.roles.values.toList(),
assignedRoles: room.assignedRoles?.values.toList() ?? [],
room: room,
space: room.courseParent,
);

View file

@ -82,8 +82,8 @@ class ActivityTemplateChatListItem extends StatelessWidget {
children: [
Expanded(
child: OpenRolesIndicator(
totalSlots: activity.req.numberOfParticipants,
userIds: e.userIds,
roles: activity.roles.values.toList(),
assignedRoles: e.assignedRoles,
space: space,
),
),

View file

@ -132,8 +132,7 @@ class CourseChatsController extends State<CourseChats>
}
final activity = summary.activityPlan;
final users =
summary.activityRoles.roles.values.map((r) => r.userId).toList();
final users = summary.activityRoles.roles.values.toList();
if (users.isEmpty || !validIDs.contains(activity.activityId)) {
continue;
@ -156,7 +155,7 @@ class CourseChatsController extends State<CourseChats>
sessionsMap[activity]!.add(
ExtendedSpaceRoomsChunk(
chunk: chunk,
userIds: users,
assignedRoles: users,
),
);
}
@ -455,7 +454,9 @@ class CourseChatsController extends State<CourseChats>
String activityId,
ExtendedSpaceRoomsChunk chunk,
) async {
final hasRole = chunk.userIds.contains(widget.client.userID);
final hasRole = chunk.assignedRoles.any(
(role) => role.userId == widget.client.userID,
);
final roomId = chunk.chunk.roomId;
if (!hasRole) {
context.go(

View file

@ -1,11 +1,13 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
class ExtendedSpaceRoomsChunk {
final SpaceRoomsChunk chunk;
final List<String> userIds;
final List<ActivityRoleModel> assignedRoles;
ExtendedSpaceRoomsChunk({
required this.chunk,
required this.userIds,
required this.assignedRoles,
});
}

View file

@ -1,63 +1,75 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/widgets/avatar.dart';
class OpenRolesIndicator extends StatelessWidget {
final int totalSlots;
final List<String> userIds;
final List<ActivityRole> roles;
final List<ActivityRoleModel> assignedRoles;
final Room? room;
final Room? space;
final double? spacing;
final double? size;
final Function(User, BuildContext)? onUserTap;
const OpenRolesIndicator({
super.key,
required this.totalSlots,
required this.userIds,
required this.roles,
required this.assignedRoles,
this.room,
this.space,
this.spacing,
this.size,
this.onUserTap,
});
@override
Widget build(BuildContext context) {
final remainingSlots = max(
0,
totalSlots - userIds.length,
);
final roomParticipants = room?.getParticipants() ?? [];
final spaceParticipants = space?.getParticipants() ?? [];
return Row(
spacing: 2.0,
spacing: spacing ?? 2.0,
children: [
...userIds.map((u) {
final user = roomParticipants.firstWhereOrNull((p) => p.id == u) ??
spaceParticipants.firstWhereOrNull((p) => p.id == u);
...roles.map((role) {
final assigned =
assignedRoles.firstWhereOrNull((r) => r.id == role.id);
return Avatar(
mxContent: user?.avatarUrl,
name: user?.calcDisplayname() ?? u.localpart ?? u,
size: 16,
userId: u,
);
}),
...List.generate(remainingSlots, (_) {
return Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 8,
backgroundColor: Theme.of(context).colorScheme.primary,
final user = assigned != null
? roomParticipants
.firstWhereOrNull((p) => p.id == assigned.userId) ??
spaceParticipants
.firstWhereOrNull((p) => p.id == assigned.userId)
: null;
if (assigned != null) {
return Builder(
builder: (context) => Avatar(
mxContent: user?.avatarUrl,
name: user?.calcDisplayname() ??
assigned.userId.localpart ??
assigned.userId,
size: size ?? 16,
userId: assigned.userId,
onTap: onUserTap != null && user != null
? () => onUserTap!(user, context)
: null,
),
CircleAvatar(
radius: 7,
backgroundColor: Theme.of(context).colorScheme.surface,
),
],
);
}
return CircleAvatar(
radius: size != null ? size! / 2 : 8,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Icon(
Icons.question_mark,
size: size != null ? (size! / 2) : 8,
),
);
}),
],

View file

@ -2,12 +2,30 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat_settings/utils/room_summary_extension.dart';
import 'package:fluffychat/pangea/course_plans/course_topics/course_topic_model.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum ActivitySummaryStatus {
notStarted,
inProgress,
completed;
String label(L10n l10n, int count) {
switch (this) {
case ActivitySummaryStatus.notStarted:
return l10n.notStartedActivitiesTitle(count);
case ActivitySummaryStatus.inProgress:
return l10n.inProgressActivitiesTitle(count);
case ActivitySummaryStatus.completed:
return l10n.completedActivitiesTitle(count);
}
}
}
mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
Map<String, RoomSummaryResponse>? roomSummaries;
@ -23,6 +41,44 @@ mixin ActivitySummariesProvider<T extends StatefulWidget> on State<T> {
}
}
Map<String, RoomSummaryResponse> activitySessions(String activityId) =>
Map.fromEntries(
roomSummaries?.entries
.where((v) => v.value.activityPlan.activityId == activityId) ??
[],
);
Map<ActivitySummaryStatus, Map<String, RoomSummaryResponse>>
activitySessionStatuses(
String activityId,
) {
final statuses = <ActivitySummaryStatus, Map<String, RoomSummaryResponse>>{
ActivitySummaryStatus.notStarted: {},
ActivitySummaryStatus.inProgress: {},
ActivitySummaryStatus.completed: {},
};
final sessions = activitySessions(activityId);
for (final entry in sessions.entries) {
final session = entry.value;
final roomId = entry.key;
final roles = session.activityRoles.roles.values;
if (roles.isNotEmpty &&
(roles.any((r) => r.isArchived) ||
roles.every((r) => r.isFinished))) {
statuses[ActivitySummaryStatus.completed]![roomId] = session;
} else if (session.activityRoles.roles.length <
session.activityPlan.req.numberOfParticipants) {
statuses[ActivitySummaryStatus.notStarted]![roomId] = session;
} else {
statuses[ActivitySummaryStatus.inProgress]![roomId] = session;
}
}
return statuses;
}
Set<String> openSessions(String activityId) {
if (roomSummaries == null || roomSummaries!.isEmpty) return {};
final Set<String> sessions = {};

View file

@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
# Pangea#
publish_to: none
# On version bump also increase the build number for F-Droid
version: 4.1.15+7
version: 4.1.15+8
environment:
sdk: ">=3.0.0 <4.0.0"