feat: initial work for participant list leaderboard (#2579)

* feat: initial work for participant list leaderboard

* chore: normalize leaderboard header

* chore: fix some errors after fluffychat merge updates
This commit is contained in:
ggurdin 2025-05-12 14:28:33 -04:00 committed by GitHub
parent 5c8b25a005
commit 542afa0506
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 934 additions and 15 deletions

View file

@ -4921,5 +4921,6 @@
"type": "String"
}
}
}
},
"leaderboard": "Leaderboard"
}

View file

@ -245,13 +245,11 @@ abstract class AppRoutes {
!state.uri.queryParameters.containsKey('spaceId') ||
spaceId == 'clear' ||
!FluffyThemes.isColumnMode(context) ||
(state.fullPath?.contains('details') ?? true)) {
state.path == ':roomid') {
return resp;
}
return !FluffyThemes.isColumnMode(context)
? resp
: '/rooms/$spaceId/details?spaceId=${spaceId ?? 'clear'}';
return '/rooms/$spaceId?spaceId=${spaceId ?? 'clear'}';
},
// Pangea#
pageBuilder: (context, state) => defaultPageBuilder(

View file

@ -50,6 +50,7 @@ import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dialog.dart';
import 'package:fluffychat/pangea/spaces/pages/pangea_space_page.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/error_reporter.dart';
@ -101,6 +102,12 @@ class ChatPage extends StatelessWidget {
);
}
// #Pangea
if (room.isSpace) {
return PangeaSpacePage(space: room);
}
// Pangea#
return ChatPageWithRoom(
key: Key('chat_page_${roomId}_$eventId'),
room: room,

View file

@ -120,7 +120,7 @@ class ChatListController extends State<ChatList>
// #Pangea
if (FluffyThemes.isColumnMode(context)) {
context.push("/rooms/$spaceId/details");
context.push("/rooms/$spaceId");
}
// Pangea#

View file

@ -17,6 +17,7 @@ import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/spaces/widgets/knocking_users_indicator.dart';
import 'package:fluffychat/pangea/spaces/widgets/space_view_leaderboard.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/public_room_dialog.dart';
@ -631,6 +632,16 @@ class _SpaceViewState extends State<SpaceView> {
final filter = _filterController.text.trim().toLowerCase();
return CustomScrollView(
slivers: [
// #Pangea
SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return SpaceViewLeaderboard(
space: room,
);
},
),
// Pangea#
SliverAppBar(
floating: true,
toolbarHeight: 72,

View file

@ -288,9 +288,7 @@ class InvitationSelectionView extends StatelessWidget {
),
],
),
onPressed: () => room.isSpace
? context.push("/rooms/${room.id}/details")
: context.go("/rooms/${room.id}"),
onPressed: () => context.go("/rooms/${room.id}"),
),
),
],

View file

@ -21,7 +21,7 @@ void chatListHandleSpaceTap(
controller.setActiveSpace(space.id);
if (FluffyThemes.isColumnMode(context)) {
context.go('/rooms/${space.id}/details');
context.go('/rooms/${space.id}');
} else if (controller.activeChat != null &&
!space.isFirstOrSecondChild(controller.activeChat!)) {
context.go("/rooms");

View file

@ -6,7 +6,6 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart';
@ -96,9 +95,7 @@ class PangeaChatDetailsView extends StatelessWidget {
return Scaffold(
appBar: AppBar(
leading: controller.widget.embeddedCloseButton ??
(room.isSpace && FluffyThemes.isColumnMode(context)
? const SizedBox()
: const Center(child: BackButton())),
const Center(child: BackButton()),
elevation: theme.appBarTheme.elevation,
title: ClassNameHeader(
controller: controller,

View file

@ -155,6 +155,7 @@ class MessageTextUtil {
}
_tokenPositionsCache[pangeaMessageEvent.eventId] = tokenPositions;
return tokenPositions;
} catch (err, s) {
ErrorHandler.logError(

View file

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/spaces/pages/pangea_space_page_view.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
class PangeaSpacePage extends StatefulWidget {
final Room space;
const PangeaSpacePage({
required this.space,
super.key,
});
@override
State<PangeaSpacePage> createState() => PangeaSpacePageState();
}
class PangeaSpacePageState extends State<PangeaSpacePage> {
bool expanded = true;
final TextEditingController searchController = TextEditingController();
final FocusNode searchFocusNode = FocusNode();
@override
void initState() {
super.initState();
searchController.addListener(() {
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
searchController.dispose();
searchFocusNode.dispose();
super.dispose();
}
void startSearch() {
setState(() {});
searchFocusNode.requestFocus();
}
void cancelSearch({bool unfocus = true}) {
setState(() {
searchController.clear();
});
if (unfocus) searchFocusNode.unfocus();
}
void toggleExpanded() {
setState(() {
expanded = !expanded;
});
}
@override
Widget build(BuildContext context) {
return LoadParticipantsUtil(
space: widget.space,
builder: (util) => PangeaSpacePageView(
this,
participantsLoader: util,
),
);
}
}

View file

@ -0,0 +1,622 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_details/participant_list_item.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/spaces/pages/pangea_space_page.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/user_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
class PangeaSpacePageView extends StatelessWidget {
final PangeaSpacePageState controller;
final LoadParticipantsUtilState participantsLoader;
const PangeaSpacePageView(
this.controller, {
required this.participantsLoader,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final room = controller.widget.space;
final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
);
final filteredParticipants = participantsLoader
.filteredParticipants("")
.where((u) => u.id != BotName.byEnvironment)
.toList();
final bool showMedals = !participantsLoader.loading &&
controller.searchController.text.isEmpty &&
filteredParticipants.isNotEmpty;
final Widget leaderboardHeader = ListTile(
tileColor: Color.lerp(AppConfig.gold, Colors.black, 0.3),
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
visualDensity: const VisualDensity(vertical: -4.0),
title: Text(
L10n.of(context).leaderboard,
style: Theme.of(context).textTheme.headlineSmall,
),
trailing: Icon(
controller.expanded
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined,
),
onTap: controller.toggleExpanded,
);
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
elevation: theme.appBarTheme.elevation,
backgroundColor: theme.appBarTheme.backgroundColor,
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () => context.go(
'/rooms/${room.id}/details',
),
),
],
shape: Border(
bottom: BorderSide(
color: theme.dividerColor,
),
),
),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: MaxWidthBody(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
children: [
Padding(
padding: const EdgeInsets.all(32.0),
child: Avatar(
mxContent: room.avatar,
name: displayname,
size: Avatar.defaultSize * 2.5,
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
displayname,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
theme.colorScheme.onSurface,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 18),
),
),
Row(
spacing: 8.0,
children: [
TextButton.icon(
onPressed: () => context.push(
'/rooms/${room.id}/details/members',
),
icon: const Icon(
Icons.group_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
theme.colorScheme.secondary,
),
label: Text(
L10n.of(context).countParticipants(
(room.summary.mInvitedMemberCount ??
0) +
(room.summary
.mJoinedMemberCount ??
0),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
TextButton.icon(
onPressed: () => context.push(
'/rooms/${room.id}/details/invite',
),
icon: const Icon(
Icons.group_add_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
theme.colorScheme.secondary,
),
label: Text(
L10n.of(context).invite,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
Divider(color: theme.dividerColor, height: 1),
Padding(
padding: const EdgeInsets.only(
left: 24.0,
right: 24.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(),
),
),
if (constraints.maxWidth <= 800) leaderboardHeader,
if (constraints.maxWidth <= 800 && controller.expanded)
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
spacing: 16.0,
children: [
SizedBox(
width: 200.0,
child: LeaderboardMedals(
isVisible: showMedals,
participants: filteredParticipants,
smallRadius: Avatar.defaultSize * 0.7,
largeRadius: Avatar.defaultSize,
),
),
if (filteredParticipants.isNotEmpty)
Expanded(
child: Column(
children: filteredParticipants
.take(3)
.mapIndexed((i, user) {
return TrophyParticipantListItem(
index: i,
user: user,
);
}).toList(),
),
),
],
),
),
],
),
),
),
if (constraints.maxWidth > 800)
Container(
decoration: BoxDecoration(
border: Border(
left: BorderSide(
color: theme.dividerColor,
width: 1.0,
),
),
),
width: 350.0,
child: Column(
spacing: 16.0,
children: [
leaderboardHeader,
if (controller.expanded)
Expanded(
child: Column(
children: [
LeaderboardMedals(
isVisible: showMedals,
participants: filteredParticipants,
padding: EdgeInsets.only(
top: showMedals ? 16.0 : 0,
left: showMedals ? 42.0 : 0,
right: showMedals ? 42.0 : 0,
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: participantsLoader.loading
? const CircularProgressIndicator
.adaptive()
: Text(
L10n.of(context)
.countParticipants(
participantsLoader
.participants.length,
),
),
),
IconButton(
icon: const Icon(
Icons.group_add_outlined,
),
iconSize: 20.0,
onPressed: () => context.push(
'/rooms/${room.id}/details/members',
),
),
],
),
TextField(
controller: controller.searchController,
focusNode: controller.searchFocusNode,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: true,
fillColor: theme
.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius:
BorderRadius.circular(99),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context).search,
hintStyle: TextStyle(
color: theme
.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
),
prefixIcon: controller.searchController
.text.isNotEmpty
? 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,
),
),
),
),
],
),
),
Expanded(
child: Builder(
builder: (context) {
if (participantsLoader.loading) {
return const Column(
children: [
CircularProgressIndicator.adaptive(),
],
);
}
if (participantsLoader.error != null) {
return Text(
L10n.of(context).oopsSomethingWentWrong,
style: TextStyle(
color: theme.colorScheme.error,
),
);
}
return ListView.builder(
itemCount: filteredParticipants.length,
itemBuilder: (context, index) {
return TrophyParticipantListItem(
index: index,
user: filteredParticipants[index],
);
},
);
},
),
),
],
),
),
],
),
),
],
),
);
},
);
}
}
class LeaderboardMedal extends StatelessWidget {
final User user;
final Color color;
final double radius;
final double iconSize;
final double iconRadius;
final double? top;
final double? left;
final double? right;
final double? bottom;
const LeaderboardMedal(
this.user, {
required this.color,
required this.radius,
required this.iconSize,
required this.iconRadius,
this.top,
this.left,
this.right,
this.bottom,
super.key,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
top: top,
left: left,
right: right,
bottom: bottom != null ? bottom! + 10.0 : null,
child: CircleAvatar(
radius: radius + 3.0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: const Alignment(0.5, -0.5),
end: const Alignment(-0.5, 0.5),
colors: <Color>[
color,
Colors.white,
color,
],
),
),
),
),
),
Positioned(
top: top != null ? 3.0 : null,
left: left != null ? 3.0 : null,
right: right != null ? 3.0 : null,
bottom: bottom != null ? bottom! + 10.0 + 3.0 : null,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => UserDialog.show(
context: context,
profile: Profile(
userId: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
),
),
child: Center(
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
size: radius * 2,
presenceUserId: user.id,
showPresence: false,
),
),
),
),
),
Positioned(
top: top != null ? ((radius + 3.0) * 2) - iconRadius : null,
left: left != null ? radius + 3.0 - iconRadius : null,
right: right != null ? radius + 3.0 - iconRadius : null,
bottom: bottom,
child: CircleAvatar(
backgroundColor: color,
radius: iconRadius,
child: Icon(
Symbols.trophy,
color: Colors.white,
size: iconSize,
),
),
),
],
);
}
}
class LeaderboardMedals extends StatelessWidget {
final bool isVisible;
final List<User> participants;
final EdgeInsets? padding;
final double? largeRadius;
final double? smallRadius;
const LeaderboardMedals({
super.key,
required this.isVisible,
required this.participants,
this.largeRadius,
this.smallRadius,
this.padding,
});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: isVisible ? Avatar.defaultSize * 3.5 : 0.0,
// padding: EdgeInsets.only(
// top: isVisible ? 16.0 : 0,
// left: isVisible ? 42.0 : 0,
// right: isVisible ? 42.0 : 0,
// ),
padding: padding,
child: !isVisible
? const SizedBox.shrink()
: Stack(
children: [
if (participants.length > 1)
LeaderboardMedal(
participants[1],
color: Colors.grey[400]!,
radius: smallRadius ?? Avatar.defaultSize * 0.75,
iconSize: 16.0,
iconRadius: 10.0,
bottom: 0.0,
left: 0.0,
),
if (participants.isNotEmpty)
LeaderboardMedal(
participants[0],
color: AppConfig.gold,
radius: largeRadius ?? Avatar.defaultSize * 1.25,
iconSize: 20.0,
iconRadius: 16.0,
top: 0.0,
right: 0.0,
left: 0.0,
),
if (participants.length > 2)
LeaderboardMedal(
participants[2],
color: Colors.brown[400]!,
radius: smallRadius ?? Avatar.defaultSize * 0.75,
bottom: 0.0,
right: 0.0,
iconSize: 16.0,
iconRadius: 10.0,
),
],
),
);
}
}
class TrophyParticipantListItem extends StatelessWidget {
final int index;
final User user;
const TrophyParticipantListItem({
required this.index,
required this.user,
super.key,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => UserDialog.show(
context: context,
profile: Profile(
userId: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
),
),
child: Row(
children: [
Container(
alignment: Alignment.centerRight,
width: 32.0,
child: (index < 3)
? Icon(
Symbols.trophy,
color: index == 0
? AppConfig.gold
: index == 1
? Colors.grey[400]
: index == 2
? Colors.brown[400]
: null,
)
: null,
),
Expanded(
child: AbsorbPointer(
child: ParticipantListItem(user),
),
),
],
),
);
}
}

View file

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/user/models/profile_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LoadParticipantsUtil extends StatefulWidget {
final Room space;
final Widget Function(LoadParticipantsUtilState) builder;
const LoadParticipantsUtil({
required this.space,
required this.builder,
super.key,
});
@override
State<LoadParticipantsUtil> createState() => LoadParticipantsUtilState();
}
class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
bool loading = true;
String? error;
final Map<String, PublicProfileModel> _levelsCache = {};
List<User> get participants => widget.space.getParticipants();
@override
void initState() {
super.initState();
_loadParticipants();
}
@override
void didUpdateWidget(LoadParticipantsUtil oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.space != widget.space) {
_loadParticipants();
}
}
Future<void> _loadParticipants() async {
try {
setState(() {
loading = true;
error = null;
});
await widget.space.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
await _cacheLevels();
} catch (err, s) {
error = err.toString();
ErrorHandler.logError(
e: err,
s: s,
data: {
'spaceId': widget.space.id,
},
);
} finally {
if (mounted) {
setState(() => loading = false);
}
}
}
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) {
if (a.id == BotName.byEnvironment) {
return 1;
}
if (b.id == BotName.byEnvironment) {
return -1;
}
final PublicProfileModel? aProfile = _levelsCache[a.id];
final PublicProfileModel? bProfile = _levelsCache[b.id];
return (bProfile?.level ?? 0).compareTo(aProfile?.level ?? 0);
});
return filtered;
}
Future<void> _cacheLevels() async {
for (final user in participants) {
if (_levelsCache[user.id] == null) {
_levelsCache[user.id] = await MatrixState
.pangeaController.userController
.getPublicProfile(user.id);
}
}
}
@override
Widget build(BuildContext context) {
return widget.builder(this);
}
}

View file

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/spaces/pages/pangea_space_page_view.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/widgets/avatar.dart';
class SpaceViewLeaderboard extends StatefulWidget {
final Room space;
const SpaceViewLeaderboard({
required this.space,
super.key,
});
@override
State<SpaceViewLeaderboard> createState() => SpaceViewLeaderboardState();
}
class SpaceViewLeaderboardState extends State<SpaceViewLeaderboard> {
bool _expanded = true;
void _toggleExpanded() {
setState(() => _expanded = !_expanded);
}
@override
Widget build(BuildContext context) {
if (FluffyThemes.isColumnMode(context)) {
return const SizedBox.shrink();
}
return LoadParticipantsUtil(
space: widget.space,
builder: (participantsLoader) {
final filteredParticipants = participantsLoader
.filteredParticipants("")
.where((u) => u.id != BotName.byEnvironment)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16.0,
children: [
ListTile(
tileColor: Color.lerp(AppConfig.gold, Colors.black, 0.3),
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0),
visualDensity: const VisualDensity(vertical: -4.0),
title: Text(
L10n.of(context).leaderboard,
style: Theme.of(context).textTheme.headlineSmall,
),
trailing: Icon(
_expanded
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined,
),
onTap: _toggleExpanded,
),
if (_expanded)
Column(
children: [
SizedBox(
width: 225.0,
child: LeaderboardMedals(
isVisible: !participantsLoader.loading &&
filteredParticipants.isNotEmpty,
participants: filteredParticipants,
smallRadius: Avatar.defaultSize * 0.7,
largeRadius: Avatar.defaultSize,
),
),
Column(
children: filteredParticipants.take(3).mapIndexed(
(index, user) {
return TrophyParticipantListItem(
index: index,
user: filteredParticipants[index],
);
},
).toList(),
),
],
),
],
);
},
);
}
}

View file

@ -22,6 +22,7 @@ class Avatar extends StatelessWidget {
final BorderSide? border;
// #Pangea
final bool useRive;
final bool showPresence;
final String? userId;
// Pangea#
@ -38,6 +39,7 @@ class Avatar extends StatelessWidget {
this.icon,
// #Pangea
this.useRive = false,
this.showPresence = true,
this.userId,
// Pangea#
super.key,
@ -117,7 +119,7 @@ class Avatar extends StatelessWidget {
),
// #Pangea
// if (presenceUserId != null)
if (presenceUserId != null && size >= 32.0)
if (presenceUserId != null && size >= 32.0 && showPresence)
// Pangea#
PresenceBuilder(
client: client,