refactor: invite page redesign (#2186)

This commit is contained in:
ggurdin 2025-03-20 13:40:19 -04:00 committed by GitHub
parent af923d67bf
commit fae97c0ab2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 487 additions and 140 deletions

View file

@ -4813,5 +4813,9 @@
"pleaseEnterInt": "Please enter a number",
"home": "Home",
"join": "Join",
"learnByTexting": "Learn by texting"
"learnByTexting": "Learn by texting",
"startChatting": "Start chatting",
"referFriends": "Refer friends",
"referFriendDialogTitle": "Invite a friend to your conversation",
"referFriendDialogDesc": "Do you have a friend who is excited to learn a new language with you? Then copy and send this invitation link to join and start chatting with you today."
}

View file

@ -34,13 +34,82 @@ class InvitationSelectionController extends State<InvitationSelection> {
String? get roomId => widget.roomId;
// #Pangea
List<User>? get participants {
final room = Matrix.of(context).client.getRoomById(roomId!);
return room?.getParticipants();
}
List<Membership> get _membershipOrder => [
Membership.join,
Membership.invite,
Membership.knock,
Membership.leave,
Membership.ban,
];
String? membershipCopy(Membership? membership) => switch (membership) {
Membership.ban => L10n.of(context).banned,
Membership.invite => L10n.of(context).invited,
Membership.join => null,
Membership.knock => L10n.of(context).knocking,
Membership.leave => L10n.of(context).leftTheChat,
null => null,
};
int _sortUsers(User a, User b) {
// sort yourself to the top
final client = Matrix.of(context).client;
if (a.id == client.userID) return -1;
if (b.id == client.userID) return 1;
// sort the bot to the bottom
if (a.id == BotName.byEnvironment) return 1;
if (b.id == BotName.byEnvironment) return -1;
if (participants != null) {
final participantA = participants!.firstWhereOrNull((u) => u.id == a.id);
final participantB = participants!.firstWhereOrNull((u) => u.id == b.id);
// sort all participants first, with admins first, then moderators, then the rest
if (participantA?.membership == null &&
participantB?.membership != null) {
return 1;
}
if (participantA?.membership != null &&
participantB?.membership == null) {
return -1;
}
if (participantA?.membership != null &&
participantB?.membership != null) {
final aIndex = _membershipOrder.indexOf(participantA!.membership);
final bIndex = _membershipOrder.indexOf(participantB!.membership);
if (aIndex != bIndex) {
return aIndex.compareTo(bIndex);
}
}
}
// finally, sort by displayname
final aName = a.calcDisplayname().toLowerCase();
final bName = b.calcDisplayname().toLowerCase();
return aName.compareTo(bName);
}
// Pangea#
Future<List<User>> getContacts(BuildContext context) async {
final client = Matrix.of(context).client;
final room = client.getRoomById(roomId!)!;
final participants = (room.summary.mJoinedMemberCount ?? 0) > 100
? room.getParticipants()
: await room.requestParticipants();
// #Pangea
// : await room.requestParticipants();
: await room.requestParticipants(
[Membership.join, Membership.invite, Membership.knock],
false,
true,
);
// Pangea#
participants.removeWhere(
(u) => ![Membership.join, Membership.invite].contains(u.membership),
);
@ -53,16 +122,26 @@ class InvitationSelectionController extends State<InvitationSelection> {
.getParticipants()
.firstWhereOrNull((u) => u.id != client.userID),
)
.where((u) => u != null)
.cast<User>()
// Pangea#
.toList();
// #Pangea
contacts.removeWhere((u) => u == null || u.id != BotName.byEnvironment);
contacts.sort(
(a, b) => a!.calcDisplayname().toLowerCase().compareTo(
b!.calcDisplayname().toLowerCase(),
),
);
return contacts.cast<User>();
final mutuals = client.rooms
.where((r) => r.isSpace)
.map((r) => r.getParticipants())
.expand((element) => element)
.toList();
for (final user in mutuals) {
final index = contacts.indexWhere((u) => u.id == user.id);
if (index == -1) {
contacts.add(user);
}
}
contacts.sort(_sortUsers);
return contacts;
// contacts.sort(
// (a, b) => a.calcDisplayname().toLowerCase().compareTo(
// b.calcDisplayname().toLowerCase(),
@ -72,72 +151,6 @@ class InvitationSelectionController extends State<InvitationSelection> {
//Pangea#
}
//#Pangea
// // add all students (already local) from spaceParents who aren't already in room to eligibleStudents
// // use room.members to get all users in room
// bool _initialized = false;
// Future<List<User>> eligibleStudents(
// BuildContext context,
// String text,
// ) async {
// if (!_initialized) {
// _initialized = true;
// await requestParentSpaceParticipants();
// }
// final eligibleStudents = <User>[];
// final spaceParents = room?.pangeaSpaceParents;
// if (spaceParents == null) return eligibleStudents;
// final userId = Matrix.of(context).client.userID;
// for (final Room space in spaceParents) {
// eligibleStudents.addAll(
// space.getParticipants().where(
// (spaceUser) =>
// spaceUser.id != BotName.byEnvironment &&
// spaceUser.id != "@support:staging.pangea.chat" &&
// spaceUser.id != userId &&
// (text.isEmpty ||
// (spaceUser.displayName
// ?.toLowerCase()
// .contains(text.toLowerCase()) ??
// false) ||
// spaceUser.id.toLowerCase().contains(text.toLowerCase())),
// ),
// );
// }
// return eligibleStudents;
// }
// Future<SearchUserDirectoryResponse>
// eligibleStudentsAsSearchUserDirectoryResponse(
// BuildContext context,
// String text,
// ) async {
// return SearchUserDirectoryResponse(
// results: (await eligibleStudents(context, text))
// .map(
// (e) => Profile(
// userId: e.id,
// avatarUrl: e.avatarUrl,
// displayName: e.displayName,
// ),
// )
// .toList(),
// limited: false,
// );
// }
// List<User?> studentsInRoom(BuildContext context) =>
// room
// ?.getParticipants()
// .where(
// (u) => [Membership.join, Membership.invite].contains(u.membership),
// )
// .toList() ??
// <User>[];
//Pangea#
void inviteAction(BuildContext context, String id, String displayname) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!;

View file

@ -1,10 +1,19 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/analytics_misc/level_display_name.dart';
import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/refer_friends_dialog.dart';
import 'package:fluffychat/pangea/common/widgets/full_width_dialog.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@ -42,10 +51,20 @@ class InvitationSelectionView extends StatelessWidget {
),
body: MaxWidthBody(
innerPadding: const EdgeInsets.symmetric(vertical: 8),
// #Pangea
withScrolling: false,
// Pangea#
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
// #Pangea
// padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.only(
bottom: 16.0,
left: 16.0,
right: 16.0,
),
// Pangea#
child: TextField(
textInputAction: TextInputAction.search,
decoration: InputDecoration(
@ -81,70 +100,200 @@ class InvitationSelectionView extends StatelessWidget {
onChanged: controller.searchUserWithCoolDown,
),
),
StreamBuilder<Object>(
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id),
builder: (context, snapshot) {
final participants =
room.getParticipants().map((user) => user.id).toSet();
return controller.foundProfiles.isNotEmpty
? ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
profile: controller.foundProfiles[i],
isMember: participants
.contains(controller.foundProfiles[i].userId),
onTap: () => controller.inviteAction(
context,
controller.foundProfiles[i].userId,
controller.foundProfiles[i].displayName ??
controller.foundProfiles[i].userId.localpart ??
L10n.of(context).user,
// #Pangea
// StreamBuilder<Object>(
Expanded(
child: StreamBuilder<Object>(
// Pangea#
stream: room.client.onRoomState.stream
.where((update) => update.roomId == room.id),
builder: (context, snapshot) {
final participants =
room.getParticipants().map((user) => user.id).toSet();
return controller.foundProfiles.isNotEmpty
? ListView.builder(
// #Pangea
// physics: const NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// Pangea#
itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
profile: controller.foundProfiles[i],
isMember: participants
.contains(controller.foundProfiles[i].userId),
onTap: () => controller.inviteAction(
context,
controller.foundProfiles[i].userId,
controller.foundProfiles[i].displayName ??
controller
.foundProfiles[i].userId.localpart ??
L10n.of(context).user,
),
),
)
: FutureBuilder<List<User>>(
future: controller.getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
final contacts = snapshot.data!;
return ListView.builder(
// #Pangea
// physics: const NeverScrollableScrollPhysics(),
// shrinkWrap: true,
// itemCount: contacts.length,
// itemBuilder: (BuildContext context, int i) =>
// _InviteContactListTile(
itemCount: contacts.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i == contacts.length) {
return room.isSpace
? const SizedBox()
: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: 450,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendAsset}",
errorWidget:
(context, url, error) =>
const SizedBox(),
placeholder: (context, url) =>
const Center(
child:
CircularProgressIndicator
.adaptive(),
),
),
),
),
);
}
return _InviteContactListTile(
// Pangea#
user: contacts[i],
profile: Profile(
avatarUrl: contacts[i].avatarUrl,
displayName: contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
userId: contacts[i].id,
),
isMember:
participants.contains(contacts[i].id),
onTap: () => controller.inviteAction(
context,
contacts[i].id,
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
),
// #Pangea
roomPowerLevel: controller.participants
?.firstWhereOrNull(
(element) =>
element.id == contacts[i].id,
)
?.powerLevel,
membership: controller.participants
?.firstWhereOrNull(
(element) =>
element.id == contacts[i].id,
)
?.membership,
// Pangea#
);
},
);
},
);
},
),
),
// #Pangea
if (!room.isSpace)
Padding(
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
top: 16.0,
bottom: FluffyThemes.isColumnMode(context) ? 0 : 16.0,
),
child: Row(
spacing: 8.0,
children: [
Expanded(
child: ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (context) => FullWidthDialog(
dialogContent: ReferFriendsDialog(room: room),
maxWidth: 600.0,
maxHeight: 800.0,
),
),
)
: FutureBuilder<List<User>>(
future: controller.getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
);
}
final contacts = snapshot.data!;
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: contacts.length,
itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile(
user: contacts[i],
profile: Profile(
avatarUrl: contacts[i].avatarUrl,
displayName: contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
userId: contacts[i].id,
),
isMember: participants.contains(contacts[i].id),
onTap: () => controller.inviteAction(
context,
contacts[i].id,
contacts[i].displayName ??
contacts[i].id.localpart ??
L10n.of(context).user,
style: ElevatedButton.styleFrom(
backgroundColor: AppConfig.gold,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
Icon(
Icons.redeem_outlined,
color: Theme.of(context).brightness ==
Brightness.light
? DefaultTextStyle.of(context).style.color
: Theme.of(context).colorScheme.surface,
),
Text(
L10n.of(context).referFriends,
style: TextStyle(
color: Theme.of(context).brightness ==
Brightness.light
? null
: Theme.of(context).colorScheme.surface,
fontWeight: FontWeight.bold,
),
),
);
},
);
},
),
],
),
),
),
Expanded(
child: ElevatedButton(
onPressed: () => context.go("/rooms/${room.id}"),
style: ElevatedButton.styleFrom(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 12.0,
children: [
Icon(
Icons.chat_outlined,
color: DefaultTextStyle.of(context).style.color,
),
Text(
L10n.of(context).startChatting,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
// Pangea#
],
),
),
@ -157,16 +306,47 @@ class _InviteContactListTile extends StatelessWidget {
final User? user;
final bool isMember;
final void Function() onTap;
// #Pangea
final int? roomPowerLevel;
final Membership? membership;
// Pangea#
const _InviteContactListTile({
required this.profile,
this.user,
required this.isMember,
required this.onTap,
// #Pangea
this.roomPowerLevel,
this.membership,
// Pangea#
});
@override
Widget build(BuildContext context) {
// #Pangea
String? permissionCopy() {
if (roomPowerLevel == null) {
return null;
}
return roomPowerLevel! >= 100
? L10n.of(context).admin
: roomPowerLevel! >= 50
? L10n.of(context).moderator
: null;
}
String? membershipCopy() => switch (membership) {
Membership.ban => L10n.of(context).banned,
Membership.invite => L10n.of(context).invited,
Membership.join => null,
Membership.knock => L10n.of(context).knocking,
Membership.leave => L10n.of(context).leftTheChat,
null => null,
};
// Pangea#
final theme = Theme.of(context);
final l10n = L10n.of(context);
@ -197,11 +377,62 @@ class _InviteContactListTile extends StatelessWidget {
color: theme.colorScheme.secondary,
),
),
trailing: TextButton.icon(
onPressed: isMember ? null : onTap,
label: Text(isMember ? l10n.participant : l10n.invite),
icon: Icon(isMember ? Icons.check : Icons.add),
// #Pangea
// trailing: TextButton.icon(
// onPressed: isMember ? null : onTap,
// label: Text(isMember ? l10n.participant : l10n.invite),
// icon: Icon(isMember ? Icons.check : Icons.add),
// ),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
LevelDisplayName(userId: profile.userId),
if (membershipCopy() != null)
Container(
padding: const EdgeInsets.all(4),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: theme.secondaryHeaderColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(
membershipCopy()!,
style: theme.textTheme.labelSmall,
),
)
else if (permissionCopy() != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: roomPowerLevel! >= 100
? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: Text(
permissionCopy()!,
style: theme.textTheme.labelSmall?.copyWith(
color: roomPowerLevel! >= 100
? theme.colorScheme.onTertiary
: theme.colorScheme.onTertiaryContainer,
),
),
)
else if (!isMember || roomPowerLevel == null || roomPowerLevel! < 50)
TextButton.icon(
onPressed: isMember ? null : onTap,
label: Text(isMember ? l10n.participant : l10n.invite),
icon: Icon(isMember ? Icons.check : Icons.add),
),
],
),
// Pangea#
);
}
}

View file

@ -0,0 +1,4 @@
class RoomSettingsConstants {
static const String referFriendAsset = "Refer+a+friend.png";
static const String referFriendDialogAsset = "Refer+a+friend_2.png";
}

View file

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/chat_settings/constants/room_settings_constants.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
class ReferFriendsDialog extends StatelessWidget {
final Room room;
const ReferFriendsDialog({
required this.room,
super.key,
});
@override
Widget build(BuildContext context) {
final inviteLink =
"${Environment.frontendURL}/#/join_with_alias?alias=${Uri.encodeComponent(room.id)}";
return Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(20.0),
border: Border.all(
color: Theme.of(context).colorScheme.primary.withAlpha(50),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16.0,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: 450,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${RoomSettingsConstants.referFriendDialogAsset}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
),
),
Text(
L10n.of(context).referFriendDialogTitle,
style: Theme.of(context).textTheme.displaySmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
Text(
L10n.of(context).referFriendDialogDesc,
style: Theme.of(context).textTheme.titleMedium,
),
Material(
color: Colors.transparent, // Keeps the `Container`'s background
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.0),
side: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
title: Text(
inviteLink,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Icons.copy_outlined),
onTap: () async {
Clipboard.setData(
ClipboardData(text: inviteLink),
);
},
),
),
],
),
);
}
}