fix: bring back lazy loading of members, call requestParticipants where full participant list is needed (#3815)

This commit is contained in:
ggurdin 2025-08-26 15:37:17 -04:00 committed by GitHub
parent f632f1fbae
commit 4b3d58e30b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 606 additions and 1038 deletions

View file

@ -645,12 +645,6 @@ class ChatController extends State<ChatPageWithRoom>
onInsert: onInsert,
);
// #Pangea
// Users with assosiated events in the timeline will be loaded into the state
// in-memory cache by getTimeline, but to ensure all users (even those without
// event in the timeline) are loaded, we will request all users. This is important
// for widgets in the chat like the activity participants displays
room.requestParticipants();
if (visibleEvents.length < 10 && timeline != null) {
var prevNumEvents = timeline!.events.length;
await requestHistory();

View file

@ -53,18 +53,6 @@ class ChatDetailsController extends State<ChatDetails> {
String? get roomId => widget.roomId;
// #Pangea
@override
void initState() {
super.initState();
// Widgets within the chat details page rely on a fully-loaded in-memory
// participants list, so we need to ensure that the participants are loaded
final room = Matrix.of(context).client.getRoomById(widget.roomId);
room?.requestParticipants().then((_) {
if (mounted) setState(() {});
});
}
final GlobalKey<ChatDetailsController> addConversationBotKey =
GlobalKey<ChatDetailsController>();

View file

@ -16,6 +16,7 @@ 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/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -80,192 +81,197 @@ class ActivityFinishedStatusMessage extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (!controller.room.showActivityChatUI ||
!controller.room.activityIsFinished ||
controller.room.ownRole == null) {
return const SizedBox.shrink();
}
return LoadParticipantsBuilder(
room: controller.room,
builder: (context, participants) {
if (!controller.room.showActivityChatUI ||
!controller.room.activityIsFinished ||
controller.room.ownRole == null) {
return const SizedBox.shrink();
}
final summary = controller.room.activitySummary;
final summary = controller.room.activitySummary;
final theme = Theme.of(context);
final theme = Theme.of(context);
final user = controller.room.getParticipants().firstWhereOrNull(
final user = participants.participants.firstWhereOrNull(
(u) => u.id == _highlightedRole?.userId,
);
final userSummary =
controller.room.activitySummary?.summary?.participants.firstWhereOrNull(
(p) => p.participantId == _highlightedRole?.userId,
);
final userSummary = controller
.room.activitySummary?.summary?.participants
.firstWhereOrNull(
(p) => p.participantId == _highlightedRole?.userId,
);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Center(
child: Container(
padding: const EdgeInsets.all(16.0),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (summary?.summary != null) ...[
Text(
L10n.of(context).activityFinishedMessage,
style: const TextStyle(fontSize: 18.0),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
summary!.summary!.summary,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14.0,
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: Center(
child: Container(
padding: const EdgeInsets.all(16.0),
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (summary?.summary != null) ...[
Text(
L10n.of(context).activityFinishedMessage,
style: const TextStyle(fontSize: 18.0),
),
),
),
if (summary.analytics != null)
Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ActivityAnalyticsChip(
ConstructTypeEnum.vocab.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}",
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
summary!.summary!.summary,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14.0,
),
),
ActivityAnalyticsChip(
ConstructTypeEnum.morph.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}",
),
],
),
const SizedBox(height: 16.0),
if (_highlightedRole != null && userSummary != null)
ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
),
child: Column(
),
if (summary.analytics != null)
Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ActivityResultsCarousel(
userId: _highlightedRole!.userId,
selectedRole: _highlightedRole!,
user: user,
summary: userSummary,
analytics: summary.analytics,
ActivityAnalyticsChip(
ConstructTypeEnum.vocab.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}",
),
Wrap(
alignment: WrapAlignment.center,
children: _rolesWithSummaries.map(
(role) {
final user = controller.room
.getParticipants()
.firstWhereOrNull(
ActivityAnalyticsChip(
ConstructTypeEnum.morph.indicator.icon,
"${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}",
),
],
),
const SizedBox(height: 16.0),
if (_highlightedRole != null && userSummary != null)
ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
),
child: Column(
children: [
ActivityResultsCarousel(
userId: _highlightedRole!.userId,
selectedRole: _highlightedRole!,
user: user,
summary: userSummary,
analytics: summary.analytics,
),
Wrap(
alignment: WrapAlignment.center,
children: _rolesWithSummaries.map(
(role) {
final user = participants.participants
.firstWhereOrNull(
(u) => u.id == role.userId,
);
return IntrinsicWidth(
child: ActivityParticipantIndicator(
availableRole: _roles[role.id]!,
avatarUrl: _roles[role.id]?.avatarUrl ??
user?.avatarUrl?.toString(),
onTap: _highlightedRole == role
? null
: () => controller.highlightRole(role),
assignedRole: role,
selected: _highlightedRole == role,
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 24.0,
),
borderRadius: BorderRadius.zero,
),
);
},
).toList(),
return IntrinsicWidth(
child: ActivityParticipantIndicator(
availableRole: _roles[role.id]!,
avatarUrl: _roles[role.id]?.avatarUrl ??
user?.avatarUrl?.toString(),
onTap: _highlightedRole == role
? null
: () =>
controller.highlightRole(role),
assignedRole: role,
selected: _highlightedRole == role,
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 24.0,
),
borderRadius: BorderRadius.zero,
),
);
},
).toList(),
),
],
),
],
),
),
),
),
const SizedBox(height: 20.0),
] else if (summary?.isLoading ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
const CircularProgressIndicator.adaptive(),
Text(L10n.of(context).loadingActivitySummary),
],
),
)
else if (summary?.hasError ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
Row(
mainAxisSize: MainAxisSize.min,
const SizedBox(height: 20.0),
] else if (summary?.isLoading ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
const Icon(
Icons.school_outlined,
size: 24.0,
const CircularProgressIndicator.adaptive(),
Text(L10n.of(context).loadingActivitySummary),
],
),
)
else if (summary?.hasError ?? false)
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 8.0,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.school_outlined,
size: 24.0,
),
const SizedBox(width: 8),
Flexible(
child: Text(
L10n.of(context).activitySummaryError,
textAlign: TextAlign.center,
),
),
],
),
const SizedBox(width: 8),
Flexible(
child: Text(
L10n.of(context).activitySummaryError,
textAlign: TextAlign.center,
),
TextButton(
onPressed: () => controller.room.fetchSummaries(),
child: Text(L10n.of(context).requestSummaries),
),
],
),
TextButton(
onPressed: () => controller.room.fetchSummaries(),
child: Text(L10n.of(context).requestSummaries),
),
],
),
),
if (!controller.room.isHiddenActivityRoom)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor: theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: () async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _archiveToAnalytics(context),
);
if (!controller.room.isHiddenActivityRoom)
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 8.0,
),
foregroundColor: theme.colorScheme.onPrimaryContainer,
backgroundColor: theme.colorScheme.primaryContainer,
),
onPressed: () async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _archiveToAnalytics(context),
);
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).archiveToAnalytics),
],
),
),
],
if (!resp.isError) {
context.go(
"/rooms/analytics?mode=activities",
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).archiveToAnalytics),
],
),
),
],
),
),
),
),
),
);
},
);
}
}

View file

@ -6,6 +6,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/widgets/avatar.dart';
class ActivityParticipantList extends StatelessWidget {
@ -27,89 +28,94 @@ class ActivityParticipantList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return LoadParticipantsBuilder(
room: room,
builder: (context, participants) {
final theme = Theme.of(context);
final availableRoles = room.activityPlan!.roles;
final assignedRoles = room.assignedRoles ?? {};
final availableRoles = room.activityPlan!.roles;
final assignedRoles = room.assignedRoles ?? {};
final remainingMembers = room.getParticipants().where(
final remainingMembers = participants.participants.where(
(p) => !assignedRoles.values.any((r) => r.userId == p.id),
);
return Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
return Column(
spacing: 12.0,
runSpacing: 12.0,
children: availableRoles.values.map((availableRole) {
final assignedRole = assignedRoles[availableRole.id];
final user = room.getParticipants().firstWhereOrNull(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
spacing: 12.0,
runSpacing: 12.0,
children: availableRoles.values.map((availableRole) {
final assignedRole = assignedRoles[availableRole.id];
final user = participants.participants.firstWhereOrNull(
(u) => u.id == assignedRole?.userId,
);
final selectable =
canSelect != null ? canSelect!(availableRole.id) : true;
final selectable =
canSelect != null ? canSelect!(availableRole.id) : true;
return ActivityParticipantIndicator(
availableRole: availableRole,
assignedRole: assignedRole,
opacity: getOpacity != null ? getOpacity!(assignedRole) : 1.0,
avatarUrl: availableRole.avatarUrl ?? user?.avatarUrl?.toString(),
onTap: onTap != null && selectable
? () => onTap!(availableRole.id)
: null,
selected:
isSelected != null ? isSelected!(availableRole.id) : false,
);
}).toList(),
),
Wrap(
alignment: WrapAlignment.center,
spacing: 12.0,
runSpacing: 12.0,
children: remainingMembers.map((member) {
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(18.0),
),
padding: const EdgeInsets.all(4.0),
child: Opacity(
opacity: 0.5,
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
size: 18.0,
mxContent: member.avatarUrl,
name: member.calcDisplayname(),
userId: member.id,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 80.0,
),
child: Text(
member.calcDisplayname(),
style: TextStyle(
fontSize: 12.0,
color: theme.colorScheme.onPrimaryContainer,
return ActivityParticipantIndicator(
availableRole: availableRole,
assignedRole: assignedRole,
opacity: getOpacity != null ? getOpacity!(assignedRole) : 1.0,
avatarUrl:
availableRole.avatarUrl ?? user?.avatarUrl?.toString(),
onTap: onTap != null && selectable
? () => onTap!(availableRole.id)
: null,
selected: isSelected != null
? isSelected!(availableRole.id)
: false,
);
}).toList(),
),
Wrap(
alignment: WrapAlignment.center,
spacing: 12.0,
runSpacing: 12.0,
children: remainingMembers.map((member) {
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(18.0),
),
padding: const EdgeInsets.all(4.0),
child: Opacity(
opacity: 0.5,
child: Row(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: [
Avatar(
size: 18.0,
mxContent: member.avatarUrl,
name: member.calcDisplayname(),
userId: member.id,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 80.0,
),
child: Text(
member.calcDisplayname(),
style: TextStyle(
fontSize: 12.0,
color: theme.colorScheme.onPrimaryContainer,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
],
),
),
);
}).toList(),
),
],
),
);
}).toList(),
),
],
);
},
);
}
}

View file

@ -131,7 +131,8 @@ class ChatDetailsContent extends StatelessWidget {
),
label: Text(
L10n.of(context).countParticipants(
room.getParticipants().length,
(room.summary.mJoinedMemberCount ?? 0) +
(room.summary.mInvitedMemberCount ?? 0),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,

View file

@ -90,6 +90,14 @@ class PangeaInvitationSelectionController
void initState() {
super.initState();
_room?.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
).then((_) {
if (mounted) setState(() {});
});
if (widget.initialFilter != null &&
availableFilters.contains(widget.initialFilter)) {
filter = widget.initialFilter!;

View file

@ -34,9 +34,6 @@ class PangeaRoomDetailsView extends StatelessWidget {
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

View file

@ -26,11 +26,11 @@ class RoomParticipantsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return LoadParticipantsUtil(
space: room,
builder: (participantsLoader) {
final filteredParticipants = participantsLoader.sortedParticipants();
return LoadParticipantsBuilder(
room: room,
loadProfiles: true,
builder: (context, participantsLoader) {
final filteredParticipants = participantsLoader.sortedParticipants;
final originalLeaders = filteredParticipants.take(3).toList();
filteredParticipants.sort((a, b) {
// always sort bot to the end

View file

@ -120,7 +120,9 @@ class SpaceDetailsContentState extends State<SpaceDetailsContent> {
? 'space'
: 'contacts';
}
context.go('/rooms/${widget.room.id}/details/invite?filter=$filter');
context.go(
'/rooms/spaces/${widget.room.id}/details/invite?filter=$filter',
);
},
enabled: widget.room.canInvite && !widget.room.isDirectChat,
showInMainView: false,
@ -228,38 +230,45 @@ class SpaceDetailsContentState extends State<SpaceDetailsContent> {
: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (isColumnMode) ...[
Avatar(
mxContent: widget.room.avatar,
name: displayname,
userId: widget.room.directChatMatrixID,
size: 80.0,
borderRadius: widget.room.isSpace
? BorderRadius.circular(24.0)
: null,
),
const SizedBox(width: 16.0),
],
Flexible(
child: Column(
spacing: 12.0,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: isColumnMode ? 32.0 : 12.0,
if (isColumnMode) ...[
Avatar(
mxContent: widget.room.avatar,
name: displayname,
userId: widget.room.directChatMatrixID,
size: 80.0,
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 (isColumnMode && courseController.course != null)
CourseInfoChips(
courseController.course!,
fontSize: 12.0,
iconSize: 12.0,
),
],
),
),

View file

@ -59,13 +59,6 @@ class CourseChatsController extends State<CourseChats> {
@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,

View file

@ -15,6 +15,8 @@ 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/pangea/space_analytics/analytics_request_indicator.dart';
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
import 'package:fluffychat/utils/stream_extension.dart';
class CourseChatsView extends StatelessWidget {
@ -59,7 +61,8 @@ class CourseChatsView extends StatelessWidget {
for (final joinedRoom in joinedRooms) {
if (joinedRoom.isActivitySession) {
if (activityIds.contains(joinedRoom.activityPlan?.bookmarkId)) {
if (topic == null ||
activityIds.contains(joinedRoom.activityPlan?.bookmarkId)) {
joinedSessions.add(joinedRoom);
}
} else {
@ -103,7 +106,7 @@ class CourseChatsView extends StatelessWidget {
joinedSessions.length +
discoveredGroupChats.length +
discoveredSessions.length +
5,
7,
itemBuilder: (context, i) {
// courses chats title
if (i == 0) {
@ -133,6 +136,16 @@ class CourseChatsView extends StatelessWidget {
}
i--;
if (i == 0) {
return KnockingUsersIndicator(room: room);
}
i--;
if (i == 0) {
return AnalyticsRequestIndicator(room: room);
}
i--;
// joined group chats
if (i < joinedChats.length) {
final joinedRoom = joinedChats[i];

View file

@ -52,173 +52,181 @@ class CourseSettings extends StatelessWidget {
room.client.userID!,
course,
);
final topicsToUsers = room.topicsToUsers(course);
return Column(
spacing: isColumnMode ? 40.0 : 36.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(
return FutureBuilder(
future: room.topicsToUsers(course),
builder: (context, snapshot) {
final topicsToUsers = snapshot.data ?? {};
return Column(
spacing: isColumnMode ? 40.0 : 36.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: [
ClipPath(
clipper: PinClipper(),
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(
Stack(
children: [
ClipPath(
clipper: PinClipper(),
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,
),
);
},
)
: 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 (!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 (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,
);
},
),
],
);
},
),
),
],
),
),
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(),
);
}).toList(),
},
);
}
}

View file

@ -35,9 +35,10 @@ class TopicParticipantList extends StatelessWidget {
SizedBox(
width: maxWidth,
height: avatarSize,
child: LoadParticipantsUtil(
space: room,
builder: (participantsLoader) {
child: LoadParticipantsBuilder(
room: room,
loadProfiles: true,
builder: (context, participantsLoader) {
final publicProfiles = Map.fromEntries(
users.map(
(u) => MapEntry(

View file

@ -81,9 +81,14 @@ extension CoursePlanRoomExtension on Room {
int ownCurrentTopicIndex(CoursePlanModel course) =>
currentTopicIndex(client.userID!, course);
Map<String, List<User>> topicsToUsers(CoursePlanModel course) {
Future<Map<String, List<User>>> topicsToUsers(CoursePlanModel course) async {
final Map<String, List<User>> topicUserMap = {};
final users = getParticipants();
final users = await requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
for (final user in users) {
if (user.id == BotName.byEnvironment) continue;
final topicIndex = currentTopicIndex(user.id, course);

View file

@ -45,8 +45,12 @@ class AnalyticsRequestIndicatorState extends State<AnalyticsRequestIndicator> {
Future<void> _fetchKnockingAdmins() async {
setState(() => _knockingAdmins.clear());
final admins =
widget.room.getParticipants().where((u) => u.powerLevel >= 100);
final admins = (await widget.room.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
))
.where((u) => u.powerLevel >= 100);
for (final analyticsRoom in widget.room.client.allMyAnalyticsRooms) {
final knocking =
@ -96,51 +100,46 @@ class AnalyticsRequestIndicatorState extends State<AnalyticsRequestIndicator> {
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _knockingAdmins.isEmpty
? const SizedBox()
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _knockingAdmins.isEmpty
? const SizedBox()
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
trailing: Icon(
Icons.arrow_right,
size: 20,
color: Theme.of(context).colorScheme.error,
),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
trailing: Icon(
Icons.arrow_right,
size: 20,
title: Row(
spacing: 8.0,
children: [
Icon(
Icons.notifications_active_outlined,
color: Theme.of(context).colorScheme.error,
),
title: Row(
spacing: 8.0,
children: [
Icon(
Icons.notifications_active_outlined,
color: Theme.of(context).colorScheme.error,
),
Expanded(
child: Text(
L10n.of(context).adminRequestedAccess,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
Expanded(
child: Text(
L10n.of(context).adminRequestedAccess,
style: Theme.of(context).textTheme.bodyMedium,
),
),
onTap: () => _onTap(context),
),
],
),
onTap: () => _onTap(context),
),
);
},
),
),
);
}
}

View file

@ -216,7 +216,12 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
}
Future<void> _initialize() async {
await room?.requestParticipants();
await room?.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
final List<Future> futures = [
GetStorage.init('analytics_request_storage'),
_loadProfiles(),

View file

@ -8,27 +8,33 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/user/models/analytics_profile_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LoadParticipantsUtil extends StatefulWidget {
final Room space;
final Widget Function(LoadParticipantsUtilState) builder;
class LoadParticipantsBuilder extends StatefulWidget {
final Room room;
final bool loadProfiles;
final Widget Function(
BuildContext context,
LoadParticipantsBuilderState,
) builder;
const LoadParticipantsUtil({
required this.space,
const LoadParticipantsBuilder({
required this.room,
required this.builder,
this.loadProfiles = false,
super.key,
});
@override
State<LoadParticipantsUtil> createState() => LoadParticipantsUtilState();
State<LoadParticipantsBuilder> createState() =>
LoadParticipantsBuilderState();
}
class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
class LoadParticipantsBuilderState extends State<LoadParticipantsBuilder> {
bool loading = true;
String? error;
final Map<String, AnalyticsProfileModel> _levelsCache = {};
List<User> get participants => widget.space.getParticipants();
List<User> get participants => widget.room.getParticipants();
@override
void initState() {
@ -37,9 +43,9 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
}
@override
void didUpdateWidget(LoadParticipantsUtil oldWidget) {
void didUpdateWidget(LoadParticipantsBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.space != widget.space) {
if (oldWidget.room.id != widget.room.id) {
_loadParticipants();
}
}
@ -51,15 +57,20 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
error = null;
});
await widget.space.requestParticipants();
await _cacheLevels();
await widget.room.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
if (widget.loadProfiles) await _cacheLevels();
} catch (err, s) {
error = err.toString();
ErrorHandler.logError(
e: err,
s: s,
data: {
'spaceId': widget.space.id,
'roomId': widget.room.id,
},
);
} finally {
@ -69,7 +80,7 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
}
}
List<User> sortedParticipants() {
List<User> get sortedParticipants {
participants.sort((a, b) {
if (a.id == BotName.byEnvironment) {
return 1;
@ -111,7 +122,7 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
@override
Widget build(BuildContext context) {
return widget.builder(this);
return widget.builder(context, this);
}
}

View file

@ -429,480 +429,3 @@ class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
);
}
}
// import 'package:flutter/material.dart';
// import 'package:csv/csv.dart';
// import 'package:excel/excel.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/pangea/analytics_downloads/space_analytics_summary_enum.dart';
// import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_model.dart';
// import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
// import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
// import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
// import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
// import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart';
// import 'package:fluffychat/pangea/common/utils/error_handler.dart';
// import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
// import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// class DownloadAnalyticsDialog extends StatefulWidget {
// final Room space;
// const DownloadAnalyticsDialog({
// required this.space,
// super.key,
// });
// @override
// DownloadAnalyticsDialogState createState() => DownloadAnalyticsDialogState();
// }
// class DownloadAnalyticsDialogState extends State<DownloadAnalyticsDialog> {
// bool _initialized = false;
// bool _downloaded = false;
// bool _downloading = false;
// bool get _loading => _downloading || !_initialized;
// Object? _error;
// Map<String, int> _downloadStatuses = {};
// @override
// void initState() {
// super.initState();
// _initialize();
// }
// Future<void> _initialize() async {
// try {
// await widget.space.requestParticipants(
// [Membership.join],
// false,
// true,
// );
// } catch (e, s) {
// ErrorHandler.logError(
// e: e,
// s: s,
// data: {
// "spaceID": widget.space.id,
// },
// );
// } finally {
// _downloadStatuses = Map.fromEntries(
// _usersToDownload.map((user) => MapEntry(user.id, 0)),
// );
// if (mounted) setState(() => _initialized = true);
// }
// }
// DownloadType _downloadType = DownloadType.csv;
// void _setDownloadType(DownloadType type) {
// _clean();
// if (mounted) setState(() => _downloadType = type);
// }
// void _clean() {
// _error = null;
// _downloading = false;
// _downloaded = false;
// _downloadStatuses = Map.fromEntries(
// _usersToDownload.map((user) => MapEntry(user.id, 0)),
// );
// }
// List<User> get _usersToDownload => widget.space
// .getParticipants()
// .where(
// (member) =>
// member.id != BotName.byEnvironment &&
// member.membership == Membership.join,
// )
// .toList();
// Color _downloadStatusColor(String userID) {
// final status = _downloadStatuses[userID];
// if (status == 1) return Colors.yellow;
// if (status == 2) return Colors.green;
// if ((status ?? 0) < 0) return Colors.red;
// return Colors.grey;
// }
// String? get _statusText {
// if (_downloading) return L10n.of(context).downloading;
// if (_downloaded) return L10n.of(context).downloadComplete;
// return null;
// }
// String? get userL2 =>
// MatrixState.pangeaController.languageController.userL2?.langCode;
// Future<void> _runDownload() async {
// try {
// if (!mounted || userL2 == null) return;
// setState(() {
// _error = null;
// _downloading = true;
// });
// final List<SpaceAnalyticsSummaryModel> summaries = [];
// await for (final batch
// in widget.space.getNextAnalyticsRoomBatch(userL2!)) {
// if (batch.isEmpty) continue;
// final List<SpaceAnalyticsSummaryModel?> batchSummaries =
// await Future.wait(
// batch.map((r) => _getAnalyticsModel(r)),
// );
// summaries
// .addAll(batchSummaries.whereType<SpaceAnalyticsSummaryModel>());
// }
// for (final userID in _downloadStatuses.keys) {
// if (_downloadStatuses[userID] == 0) {
// _downloadStatuses[userID] = -1;
// summaries.add(SpaceAnalyticsSummaryModel.emptyModel(userID));
// }
// }
// await _downloadSpaceAnalytics(summaries);
// if (mounted) {
// setState(() {
// _downloading = false;
// _downloaded = true;
// });
// }
// } catch (e, s) {
// ErrorHandler.logError(
// e: e,
// s: s,
// data: {
// "spaceID": widget.space.id,
// },
// );
// _clean();
// _error = e;
// if (mounted) setState(() {});
// }
// }
// Future<void> _downloadSpaceAnalytics(
// List<SpaceAnalyticsSummaryModel> summaries,
// ) async {
// final content = _downloadType == DownloadType.xlsx
// ? _getExcelFileContent(summaries)
// : _getCSVFileContent(summaries);
// final fileName =
// "analytics_${widget.space.name}_${DateTime.now().toIso8601String()}.${_downloadType == DownloadType.xlsx ? 'xlsx' : 'csv'}";
// await downloadFile(
// content,
// fileName,
// DownloadType.csv,
// );
// }
// Future<SpaceAnalyticsSummaryModel?> _getAnalyticsModel(
// Room analyticsRoom,
// ) async {
// final String? userID = analyticsRoom.creatorId;
// if (userID == null) return null;
// SpaceAnalyticsSummaryModel? summary;
// try {
// _downloadStatuses[userID] = 1;
// if (mounted) setState(() {});
// final constructEvents = await analyticsRoom.getAnalyticsEvents(
// userId: userID,
// );
// if (constructEvents == null) {
// if (mounted) setState(() => _downloadStatuses[userID] = -1);
// return SpaceAnalyticsSummaryModel.emptyModel(userID);
// }
// final List<OneConstructUse> uses = [];
// for (final event in constructEvents) {
// uses.addAll(event.content.uses);
// }
// final constructs = ConstructListModel(uses: uses);
// summary = SpaceAnalyticsSummaryModel.fromConstructListModel(
// userID,
// constructs,
// 0,
// getCopy,
// context,
// );
// if (mounted) setState(() => _downloadStatuses[userID] = 2);
// } catch (e, s) {
// ErrorHandler.logError(
// e: e,
// s: s,
// data: {
// "spaceID": widget.space.id,
// "userID": userID,
// },
// );
// if (mounted) setState(() => _downloadStatuses[userID] = -2);
// } finally {
// if (userID != widget.space.client.userID) {
// try {
// await analyticsRoom.leave();
// } catch (e, s) {
// ErrorHandler.logError(
// e: e,
// s: s,
// data: {
// "spaceID": widget.space.id,
// "userID": userID,
// },
// );
// }
// }
// }
// return summary;
// }
// List<CellValue> _formatExcelRow(
// SpaceAnalyticsSummaryModel summary,
// ) {
// final List<CellValue> row = [];
// for (int i = 0; i < SpaceAnalyticsSummaryEnum.values.length; i++) {
// final key = SpaceAnalyticsSummaryEnum.values[i];
// final value = summary.getValue(key, context);
// if (value is int) {
// row.add(IntCellValue(value));
// } else if (value is String) {
// row.add(TextCellValue(value));
// } else if (value is List<String>) {
// row.add(TextCellValue(value.join(", ")));
// }
// }
// return row;
// }
// List<int> _getExcelFileContent(
// List<SpaceAnalyticsSummaryModel> summaries,
// ) {
// final excel = Excel.createExcel();
// final sheet = excel['Sheet1'];
// for (final key in SpaceAnalyticsSummaryEnum.values) {
// sheet
// .cell(
// CellIndex.indexByColumnRow(
// rowIndex: 0,
// columnIndex: key.index,
// ),
// )
// .value = TextCellValue(key.header(L10n.of(context)));
// }
// final rows = summaries.map((summary) => _formatExcelRow(summary)).toList();
// for (int i = 0; i < rows.length; i++) {
// final row = rows[i];
// for (int j = 0; j < row.length; j++) {
// final cell = row[j];
// sheet
// .cell(CellIndex.indexByColumnRow(rowIndex: i + 2, columnIndex: j))
// .value = cell;
// }
// }
// return excel.encode() ?? [];
// }
// String _getCSVFileContent(
// List<SpaceAnalyticsSummaryModel> summaries,
// ) {
// final List<List<dynamic>> rows = [];
// final headerRow = [];
// for (final key in SpaceAnalyticsSummaryEnum.values) {
// headerRow.add(key.header(L10n.of(context)));
// }
// rows.add(headerRow);
// for (final summary in summaries) {
// final row = [];
// for (int i = 0; i < SpaceAnalyticsSummaryEnum.values.length; i++) {
// final key = SpaceAnalyticsSummaryEnum.values[i];
// final value = summary.getValue(key, context);
// if (value == null) continue;
// value is List<String> ? row.add(value.join(", ")) : row.add(value);
// }
// rows.add(row);
// }
// final String fileString = const ListToCsvConverter().convert(rows);
// return fileString;
// }
// String getCopy(ConstructUses use) {
// return getGrammarCopy(
// category: use.category,
// lemma: use.lemma,
// context: context,
// ) ??
// use.lemma;
// }
// @override
// Widget build(BuildContext context) {
// return Dialog(
// child: Container(
// constraints: const BoxConstraints(
// maxWidth: 400,
// ),
// padding: const EdgeInsets.symmetric(vertical: 20),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(
// L10n.of(context).fileType,
// style: TextStyle(
// fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize,
// ),
// ),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: SegmentedButton<DownloadType>(
// selected: {_downloadType},
// onSelectionChanged: (c) => _setDownloadType(c.first),
// segments: [
// ButtonSegment(
// value: DownloadType.csv,
// label: Text(L10n.of(context).commaSeparatedFile),
// ),
// ButtonSegment(
// value: DownloadType.xlsx,
// label: Text(L10n.of(context).excelFile),
// ),
// ],
// ),
// ),
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: ConstrainedBox(
// constraints: const BoxConstraints(
// maxHeight: 300,
// minHeight: 0,
// ),
// child: ListView.builder(
// shrinkWrap: true,
// itemCount: _usersToDownload.length,
// itemBuilder: (context, index) {
// final user = _usersToDownload[index];
// String tooltip = "";
// if (_downloadStatuses[user.id] == -1) {
// tooltip = L10n.of(context).analyticsNotAvailable;
// } else if (_downloadStatuses[user.id] == -2) {
// tooltip = L10n.of(context).failedFetchUserAnalytics;
// }
// return Padding(
// padding: const EdgeInsets.all(4.0),
// child: AnimatedOpacity(
// duration: FluffyThemes.animationDuration,
// opacity:
// (_downloadStatuses[user.id] ?? 0) > 0 ? 1 : 0.5,
// child: Row(
// children: [
// SizedBox(
// width: 40,
// height: 30,
// child: (_downloadStatuses[user.id] ?? 0) < 0
// ? const Icon(
// Icons.error_outline,
// size: 16,
// )
// : Center(
// child: AnimatedContainer(
// duration:
// FluffyThemes.animationDuration,
// height: 12,
// width: 12,
// decoration: BoxDecoration(
// color: _downloadStatusColor(user.id),
// borderRadius:
// BorderRadius.circular(100),
// ),
// ),
// ),
// ),
// Flexible(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(user.displayName ?? user.id),
// if (tooltip.isNotEmpty)
// Text(
// tooltip,
// style: const TextStyle(fontSize: 10),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// );
// },
// ),
// ),
// ),
// Padding(
// padding: const EdgeInsets.fromLTRB(8.0, 16.0, 8.0, 8.0),
// child: OutlinedButton(
// onPressed: _loading || !_initialized ? null : _runDownload,
// child: _initialized && !_loading
// ? Text(
// _loading
// ? L10n.of(context).downloading
// : L10n.of(context).download,
// )
// : const SizedBox(
// height: 10,
// width: 100,
// child: LinearProgressIndicator(),
// ),
// ),
// ),
// AnimatedSize(
// duration: FluffyThemes.animationDuration,
// child: _statusText != null
// ? Padding(
// padding: const EdgeInsets.all(8.0),
// child: Text(_statusText!),
// )
// : const SizedBox(),
// ),
// AnimatedSize(
// duration: FluffyThemes.animationDuration,
// child: _error != null
// ? Padding(
// padding: const EdgeInsets.all(8.0),
// child: ErrorIndicator(
// message: L10n.of(context).errorDownloading,
// ),
// )
// : const SizedBox(),
// ),
// ],
// ),
// ),
// );
// }
// }

View file

@ -35,7 +35,12 @@ class KnockingUsersIndicatorState extends State<KnockingUsersIndicator> {
.where(_isMemberUpdate)
.rateLimit(const Duration(seconds: 1))
.listen((_) => _setKnockingUsers());
_setKnockingUsers();
widget.room.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
).then((_) => _setKnockingUsers());
}
bool _isMemberUpdate(({String roomId, StrippedStateEvent state}) event) =>
@ -58,55 +63,50 @@ class KnockingUsersIndicatorState extends State<KnockingUsersIndicator> {
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _knockingUsers.isEmpty || !widget.room.isRoomAdmin
? const SizedBox()
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: _knockingUsers.isEmpty || !widget.room.isRoomAdmin
? const SizedBox()
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
trailing: Icon(
Icons.adaptive.arrow_forward_outlined,
size: 16,
),
child: Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge,
child: ListTile(
minVerticalPadding: 0,
trailing: Icon(
Icons.adaptive.arrow_forward_outlined,
size: 16,
title: Row(
spacing: 8.0,
children: [
Icon(
Icons.notifications_outlined,
color: Theme.of(context).colorScheme.error,
),
title: Row(
spacing: 8.0,
children: [
Icon(
Icons.notifications_outlined,
color: Theme.of(context).colorScheme.error,
),
Expanded(
child: Text(
_knockingUsers.length == 1
? L10n.of(context).aUserIsKnocking
: L10n.of(context)
.usersAreKnocking(_knockingUsers.length),
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
Expanded(
child: Text(
_knockingUsers.length == 1
? L10n.of(context).aUserIsKnocking
: L10n.of(context)
.usersAreKnocking(_knockingUsers.length),
style: Theme.of(context).textTheme.bodyMedium,
),
),
onTap: () => context.push(
"/rooms/${widget.room.id}/details/members?filter=knock",
),
),
],
),
onTap: () => context.push(
"/rooms/spaces/${widget.room.id}/details/members?filter=knock",
),
),
);
},
),
),
);
}
}

View file

@ -44,11 +44,11 @@ class LeaderboardParticipantListState
return StreamBuilder(
stream: client.onSync.stream.rateLimit(const Duration(seconds: 3)),
builder: (context, snapshot) {
return LoadParticipantsUtil(
space: widget.space,
builder: (participantsLoader) {
final participants = participantsLoader
.sortedParticipants()
return LoadParticipantsBuilder(
room: widget.space,
loadProfiles: true,
builder: (context, participantsLoader) {
final participants = participantsLoader.sortedParticipants
.where((p) => p.membership == Membership.join)
.toList();

View file

@ -163,6 +163,7 @@ abstract class ClientManager {
// #Pangea
syncFilter: Filter(
room: RoomFilter(
state: StateFilter(lazyLoadMembers: true),
timeline: StateFilter(
notTypes: [
PangeaEventTypes.construct,