3263 duplicate participant list causes confusion (#3390)

* chore: remove participant list from space view

* chore: don't change participant list rendering based on screen sizwe
This commit is contained in:
ggurdin 2025-07-10 11:28:28 -04:00 committed by GitHub
parent 4006e3207c
commit ad1a84bc5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 177 additions and 347 deletions

View file

@ -21,7 +21,6 @@ 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/pangea/spaces/widgets/knocking_users_indicator.dart';
import 'package:fluffychat/pangea/spaces/widgets/leaderboard_participant_list.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -855,14 +854,6 @@ class _SpaceViewState extends State<SpaceView> {
// },
// ),
KnockingUsersIndicator(room: room),
SliverList.builder(
itemCount: 1,
itemBuilder: (context, i) {
return LeaderboardParticipantList(
space: room,
);
},
),
// Pangea#
SliverList.builder(
itemCount: joinedRooms.length,

View file

@ -11,7 +11,6 @@ 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/pages/chat_details/participant_list_item.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';
@ -671,117 +670,112 @@ class RoomParticipantsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final List<User> members = room.getParticipants().toList()
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
return LoadParticipantsUtil(
space: room,
builder: (participantsLoader) {
final filteredParticipants =
participantsLoader.filteredParticipants("");
final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) +
(room.summary.mJoinedMemberCount ?? 0);
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;
}
return LayoutBuilder(
builder: (context, constraints) {
final availableWidth = constraints.maxWidth;
final capacity = (availableWidth / (_width + _spacing)).floor();
return LoadParticipantsUtil(
space: room,
builder: (participantsLoader) {
final filteredParticipants =
participantsLoader.filteredParticipants("");
// 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;
}
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;
}
// 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;
}
// 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 admins
if (a.powerLevel == 100 && b.powerLevel != 100) {
return -1;
} else if (b.powerLevel == 100 && a.powerLevel != 100) {
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;
}
return 0;
});
// then admins
if (a.powerLevel == 100 && b.powerLevel != 100) {
return -1;
} else if (b.powerLevel == 100 && a.powerLevel != 100) {
return 1;
}
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
: '';
return 0;
});
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,
};
if (capacity < 4) {
return Column(
children: [
...filteredParticipants.map(
(member) => ParticipantListItem(member),
),
if (actualMembersCount - members.length > 0)
ListTile(
title: Text(
L10n.of(context).loadCountMoreParticipants(
(actualMembersCount - members.length),
),
),
leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor,
child: const Icon(
Icons.group_outlined,
color: Colors.grey,
),
),
onTap: () => context.push(
'/rooms/${room.id}/details/members',
),
trailing: const Icon(Icons.chevron_right_outlined),
),
],
final publicProfile = participantsLoader.getPublicProfile(
user.id,
);
}
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 leaderIndex = originalLeaders.indexOf(user);
LinearGradient? gradient;
if (leaderIndex != -1) {
gradient = leaderIndex.leaderboardGradient;
if (user.id == BotName.byEnvironment ||
publicProfile == null ||
publicProfile.level == null) {
gradient = null;
}
}
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,
};
return SizedBox(
width: _width,
child: Opacity(
opacity: user.membership == Membership.join ? 1.0 : 0.5,
child: Column(
spacing: 4.0,
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(
@ -795,7 +789,7 @@ class RoomParticipantsSection extends StatelessWidget {
child: Avatar(
mxContent: user.avatarUrl,
name: user.calcDisplayname(),
size: _width,
size: _width - 6.0,
presenceUserId: user.id,
presenceOffset: const Offset(0, 0),
presenceSize: 18.0,
@ -805,83 +799,80 @@ class RoomParticipantsSection extends StatelessWidget {
);
},
),
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
],
),
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:
theme.colorScheme.secondaryContainer,
color: user.powerLevel >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: Text(
membershipBatch,
permissionBatch,
style:
theme.textTheme.labelSmall?.copyWith(
color: theme
.colorScheme.onSecondaryContainer,
color: user.powerLevel >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme
.onTertiaryContainer,
),
),
)
: 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,
),
],
: null,
),
),
);
}),
],
);
},
],
),
),
);
}),
],
);
},
);

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.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';
@ -125,3 +126,27 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
return widget.builder(this);
}
}
extension LeaderboardGradient on int {
LinearGradient? get leaderboardGradient {
final Color? color = this == 0
? AppConfig.gold
: this == 1
? Colors.grey[400]!
: this == 2
? Colors.brown[400]!
: null;
if (color == null) return null;
return LinearGradient(
colors: [
color,
Colors.white,
color,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}

View file

@ -1,177 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/status_msg_list.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/spaces/utils/load_participants_util.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/presence_builder.dart';
class LeaderboardParticipantList extends StatefulWidget {
final Room space;
const LeaderboardParticipantList({
required this.space,
super.key,
});
static const double height = 116;
@override
State<LeaderboardParticipantList> createState() =>
LeaderboardParticipantListState();
}
class LeaderboardParticipantListState
extends State<LeaderboardParticipantList> {
final _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final client = Matrix.of(context).client;
final theme = Theme.of(context);
return StreamBuilder(
stream: client.onSync.stream.rateLimit(const Duration(seconds: 3)),
builder: (context, snapshot) {
return LoadParticipantsUtil(
space: widget.space,
builder: (participantsLoader) {
final participants = participantsLoader
.filteredParticipants("")
.where((p) => p.membership == Membership.join)
.toList();
return AnimatedSize(
duration: FluffyThemes.animationDuration,
curve: Curves.easeInOut,
child: SizedBox(
height: 130.0,
child: Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(
8.0,
8.0,
8.0,
16.0,
),
scrollDirection: Axis.horizontal,
itemCount: participants.length,
itemBuilder: (context, i) {
final user = participants[i];
final publicProfile = participantsLoader.getPublicProfile(
user.id,
);
LinearGradient? gradient = i.leaderboardGradient;
if (user.id == BotName.byEnvironment ||
publicProfile == null ||
publicProfile.level == null) {
gradient = null;
}
return PresenceBuilder(
userId: user.id,
builder: (context, presence) {
Color? dotColor;
if (presence != null) {
dotColor = presence.presence.isOnline
? Colors.green
: presence.presence.isUnavailable
? Colors.orange
: Colors.grey;
}
return PresenceAvatar(
presence: presence ??
CachedPresence(
PresenceType.unavailable,
null,
null,
null,
user.id,
),
height: StatusMessageList.height,
onTap: (profile) => UserDialog.show(
context: context,
profile: profile,
),
gradient: gradient,
showPresence: false,
floatingIndicator: Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(32),
),
alignment: Alignment.center,
child: Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: dotColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
width: 1,
color: theme.colorScheme.surface,
),
),
),
),
),
);
},
);
},
),
),
),
);
},
);
},
);
}
}
extension LeaderboardGradient on int {
LinearGradient? get leaderboardGradient {
final Color? color = this == 0
? AppConfig.gold
: this == 1
? Colors.grey[400]!
: this == 2
? Colors.brown[400]!
: null;
if (color == null) return null;
return LinearGradient(
colors: [
color,
Colors.white,
color,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}