feat: find your people page

This commit is contained in:
ggurdin 2025-06-03 14:34:13 -04:00 committed by GitHub
parent c8fdcda3fb
commit 01d797e53f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 600 additions and 839 deletions

View file

@ -4975,5 +4975,6 @@
"canBeFoundViaInvitation": "\u2022 invitation",
"canBeFoundViaCodeOrLink": "\u2022 code or link",
"canBeFoundViaKnock": "\u2022 request to join and admin approval",
"anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!"
"anyoneCanJoin": "Anyone can join! However, admin can kick and ban whoever misbehaves. Those who are banned may not return!",
"createYourSpace": "Create your space"
}

View file

@ -32,6 +32,8 @@ import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pangea/activity_generator/activity_generator.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_suggestions/suggestions_page.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_side_view.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
import 'package:fluffychat/pangea/login/pages/login_or_signup_view.dart';
@ -42,7 +44,6 @@ import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_alias.dart';
import 'package:fluffychat/pangea/spaces/utils/join_with_link.dart';
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
import 'package:fluffychat/pangea/user/pages/find_partner.dart';
import 'package:fluffychat/widgets/config_viewer.dart';
import 'package:fluffychat/widgets/layouts/empty_page.dart';
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
@ -153,17 +154,11 @@ abstract class AppRoutes {
),
GoRoute(
path: '/join_with_alias',
pageBuilder: (context, state) => Matrix.of(context).client.isLogged()
? chatListShellRouteBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
)
: defaultPageBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
JoinWithAlias(alias: state.uri.queryParameters['alias']),
),
),
GoRoute(
path: '/user_age',
@ -195,8 +190,13 @@ abstract class AppRoutes {
pageBuilder: (context, state, child) => noTransitionPageBuilder(
context,
state,
// #Pangea
// FluffyThemes.isColumnMode(context) &&
// state.fullPath?.startsWith('/rooms/settings') == false
FluffyThemes.isColumnMode(context) &&
state.fullPath?.startsWith('/rooms/settings') == false
state.fullPath?.startsWith('/rooms/settings') == false &&
state.fullPath?.startsWith('/rooms/communities') == false
// Pangea#
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
@ -302,16 +302,30 @@ abstract class AppRoutes {
redirect: loggedOutRedirect,
),
// #Pangea
GoRoute(
path: 'partner',
pageBuilder: (context, state) => defaultPageBuilder(
ShellRoute(
pageBuilder: (context, state, child) => defaultPageBuilder(
context,
state,
const FindPartner(),
FluffyThemes.isColumnMode(context)
? TwoColumnLayout(
mainView: const FindYourPeopleSideView(),
sideView: child,
dividerColor: Colors.transparent,
)
: child,
),
redirect: loggedOutRedirect,
routes: [
GoRoute(
path: 'communities',
redirect: loggedOutRedirect,
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const FindYourPeople(),
),
),
],
),
// #Pangea
GoRoute(
path: 'homepage',
redirect: loggedOutRedirect,
@ -748,27 +762,5 @@ abstract class AppRoutes {
redirect: loggedOutRedirect,
),
];
static Page chatListShellRouteBuilder(
context,
state,
child,
) =>
noTransitionPageBuilder(
context,
state,
FluffyThemes.isColumnMode(context) &&
state.fullPath?.startsWith('/rooms/settings') == false
? TwoColumnLayout(
mainView: ChatList(
activeChat: state.pathParameters['roomid'],
activeSpaceId: state.uri.queryParameters['spaceId'],
displayNavigationRail:
state.path?.startsWith('/rooms/settings') != true,
),
sideView: child,
)
: child,
);
// Pangea#
}

View file

@ -41,6 +41,9 @@ class ChatListView extends StatelessWidget {
activeSpaceId: controller.activeSpaceId,
onGoToChats: controller.clearActiveSpace,
onGoToSpaceId: controller.setActiveSpace,
// #Pangea
clearActiveSpace: controller.clearActiveSpace,
// Pangea#
),
Container(
color: Theme.of(context).dividerColor,

View file

@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
class LearningProgressIndicatorButton extends StatelessWidget {
class HoverButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget child;
final BorderRadius? borderRadius;
final double hoverOpacity;
const LearningProgressIndicatorButton({
const HoverButton({
super.key,
required this.onPressed,
required this.child,
this.borderRadius,
this.hoverOpacity = 0.2,
});
@override
@ -23,9 +27,12 @@ class LearningProgressIndicatorButton extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
color: hovered
? Theme.of(context).colorScheme.primary.withAlpha(50)
? Theme.of(context)
.colorScheme
.primary
.withAlpha((hoverOpacity * 255).round())
: Colors.transparent,
borderRadius: BorderRadius.circular(36.0),
borderRadius: borderRadius ?? BorderRadius.circular(36.0),
),
padding: const EdgeInsets.symmetric(
vertical: 2.0,

View file

@ -105,7 +105,7 @@ class LearningProgressIndicatorsState
spacing: 16.0,
children: ConstructTypeEnum.values
.map(
(c) => LearningProgressIndicatorButton(
(c) => HoverButton(
onPressed: () {
showDialog<AnalyticsPopupWrapper>(
context: context,
@ -124,7 +124,7 @@ class LearningProgressIndicatorsState
.toList(),
),
),
LearningProgressIndicatorButton(
HoverButton(
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),

View file

@ -0,0 +1,102 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_view.dart';
import 'package:fluffychat/widgets/matrix.dart';
class FindYourPeople extends StatefulWidget {
const FindYourPeople({super.key});
@override
State<FindYourPeople> createState() => FindYourPeopleState();
}
class FindYourPeopleState extends State<FindYourPeople> {
final TextEditingController searchController = TextEditingController();
String? error;
bool loading = true;
Timer? _coolDown;
final List<PublicRoomsChunk> spaceItems = [];
@override
void initState() {
super.initState();
setSpaceItems();
}
@override
void dispose() {
searchController.dispose();
_coolDown?.cancel();
super.dispose();
}
void onSearchEnter(String text, {bool globalSearch = true}) {
if (text.isEmpty) {
setSpaceItems();
return;
}
_coolDown?.cancel();
_coolDown = Timer(const Duration(milliseconds: 500), setSpaceItems);
}
Future<void> setSpaceItems() async {
setState(() {
loading = true;
error = null;
spaceItems.clear();
});
try {
final resp = await Matrix.of(context).client.queryPublicRooms(
filter: PublicRoomQueryFilter(
roomTypes: ['m.space'],
genericSearchTerm: searchController.text,
),
limit: 100,
);
spaceItems.addAll(resp.chunk);
spaceItems.sort((a, b) {
int getPriority(item) {
final bool hasTopic = item.topic != null && item.topic!.isNotEmpty;
final bool hasAvatar = item.avatarUrl != null;
if (hasTopic && hasAvatar) return 0; // Highest priority
if (hasAvatar) return 1; // Second priority
if (hasTopic) return 2; // Third priority
return 3; // Lowest priority
}
return getPriority(a).compareTo(getPriority(b));
});
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'searchText': searchController.text,
},
);
error = e.toString();
} finally {
if (mounted) {
setState(() {
loading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return FindYourPeopleView(controller: this);
}
}

View file

@ -0,0 +1,3 @@
class FindYourPeopleConstants {
static const String sideBearFileName = "Bear_Find_your_people.png";
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people_constants.dart';
import 'package:fluffychat/widgets/navigation_rail.dart';
class FindYourPeopleSideView extends StatelessWidget {
const FindYourPeopleSideView({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
if (FluffyThemes.isColumnMode(context) ||
AppConfig.displayNavigationRail) ...[
SpacesNavigationRail(
activeSpaceId: null,
onGoToChats: () => context.go('/rooms'),
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
),
Container(
color: Colors.transparent,
width: 1,
),
],
Expanded(
child: Center(
child: SizedBox(
width: 250.0,
child: CachedNetworkImage(
imageUrl:
"${AppConfig.assetsBaseURL}/${FindYourPeopleConstants.sideBearFileName}",
errorWidget: (context, url, error) => const SizedBox(),
placeholder: (context, url) => const Center(
child: CircularProgressIndicator.adaptive(),
),
),
),
),
),
],
);
}
}

View file

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/find_your_people/find_your_people.dart';
import 'package:fluffychat/pangea/find_your_people/public_space_tile.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
class FindYourPeopleView extends StatelessWidget {
final FindYourPeopleState controller;
const FindYourPeopleView({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isColumnMode = FluffyThemes.isColumnMode(context);
return Scaffold(
appBar: isColumnMode
? null
: AppBar(
leading: IconButton(
icon: Icon(
Icons.chevron_left,
color: theme.colorScheme.primary,
),
onPressed: () => Navigator.of(context).pop(),
),
title: Icon(
Icons.groups_outlined,
size: 20.0,
color: theme.colorScheme.primary,
),
centerTitle: false,
leadingWidth: 48.0,
actions: [
TextButton(
child: Row(
children: [
Icon(
Icons.join_full,
color: theme.colorScheme.primary,
size: 20.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).joinWithCode,
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 10.0,
),
),
],
),
onPressed: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
),
],
),
floatingActionButton: isColumnMode
? null
: FloatingActionButton.extended(
onPressed: () => context.push('/rooms/newspace'),
icon: const Icon(Icons.add_box_outlined),
label: Text(
L10n.of(context).space,
overflow: TextOverflow.fade,
),
),
body: Padding(
padding: isColumnMode
? const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 20.0,
)
: const EdgeInsets.all(12.0),
child: Column(
spacing: 16.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isColumnMode)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 12.0,
),
child: Text(
L10n.of(context).findYourPeople,
style: const TextStyle(fontSize: 32.0),
),
),
Expanded(
child: Column(
spacing: isColumnMode ? 32.0 : 16.0,
children: [
Container(
height: 48.0,
padding: isColumnMode
? const EdgeInsets.symmetric(horizontal: 12)
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 10,
children: [
Expanded(
child: SizedBox(
height: 40.0,
child: TextField(
controller: controller.searchController,
onChanged: controller.onSearchEnter,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
filled: !isColumnMode,
fillColor: isColumnMode
? null
: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder(
borderSide: isColumnMode
? const BorderSide()
: BorderSide.none,
borderRadius: BorderRadius.circular(100),
),
contentPadding: EdgeInsets.zero,
hintText: L10n.of(context).findYourPeople,
hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal,
fontSize: 16.0,
),
floatingLabelBehavior:
FloatingLabelBehavior.never,
prefixIcon: IconButton(
onPressed: () {},
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
),
),
),
),
),
if (isColumnMode)
TextButton(
child: Row(
children: [
Icon(
Icons.join_full,
color: theme.colorScheme.onPrimaryContainer,
size: 24.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).joinWithCode,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 14.0,
),
),
],
),
onPressed: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
),
if (isColumnMode)
TextButton(
child: Row(
children: [
Icon(
Icons.add_box_outlined,
color: theme.colorScheme.onPrimaryContainer,
size: 24.0,
),
const SizedBox(width: 8.0),
Text(
L10n.of(context).createYourSpace,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
fontSize: 14.0,
),
),
],
),
onPressed: () => context.push('/rooms/newspace'),
),
],
),
),
controller.error != null
? Row(
spacing: 8.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
L10n.of(context).oopsSomethingWentWrong,
),
IconButton(
onPressed: controller.setSpaceItems,
icon: const Icon(Icons.refresh),
),
],
)
: controller.loading
? const CircularProgressIndicator.adaptive()
: controller.spaceItems.isEmpty
? Text(
L10n.of(context).nothingFound,
)
: Expanded(
child: ListView.builder(
itemCount: controller.spaceItems.length,
itemBuilder: (context, index) {
final space =
controller.spaceItems[index];
return Padding(
padding: isColumnMode
? const EdgeInsets.only(
bottom: 32.0,
)
: const EdgeInsets.only(
bottom: 16.0,
),
child: PublicSpaceTile(space: space),
);
},
),
),
],
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicator_button.dart';
import 'package:fluffychat/pangea/public_spaces/public_room_bottom_sheet.dart';
import 'package:fluffychat/widgets/avatar.dart';
class PublicSpaceTile extends StatelessWidget {
final PublicRoomsChunk space;
const PublicSpaceTile({super.key, required this.space});
@override
Widget build(BuildContext context) {
final bool isColumnMode = FluffyThemes.isColumnMode(context);
return HoverButton(
onPressed: () => PublicRoomBottomSheet.show(
context: context,
chunk: space,
),
borderRadius: BorderRadius.circular(10.0),
hoverOpacity: 0.1,
child: Padding(
padding: isColumnMode
? const EdgeInsets.all(12.0)
: const EdgeInsets.all(0.0),
child: Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: isColumnMode ? 80.0 : 58.0,
child: Row(
children: [
Avatar(
mxContent: space.avatarUrl,
name: space.name,
size: isColumnMode ? 80.0 : 58.0,
borderRadius: BorderRadius.circular(
10,
),
),
Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
space.name ?? '',
style: TextStyle(
fontSize: isColumnMode ? 20.0 : 14.0,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.group,
size: isColumnMode ? 30.0 : 16.0,
),
Text(
L10n.of(context).countParticipants(
space.numJoinedMembers,
),
style: TextStyle(
fontSize: isColumnMode ? 20.0 : 12.0,
height: 1.2,
),
),
],
),
],
),
),
),
],
),
),
if (isColumnMode && space.topic != null && space.topic!.isNotEmpty)
Text(
space.topic!,
style: const TextStyle(
fontSize: 20.0,
height: 1.2,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
}

View file

@ -1,4 +0,0 @@
class AgeLimits {
static const int toAccessFeatures = 18;
static const int toUseTheApp = 13;
}

View file

@ -1,27 +0,0 @@
import 'user_model.dart';
class UserProfileSearchResponse {
int count;
String? next;
String? previous;
List<PangeaProfile> results;
UserProfileSearchResponse({
required this.count,
required this.next,
required this.previous,
required this.results,
});
factory UserProfileSearchResponse.fromJson(Map<String, dynamic> json) {
return UserProfileSearchResponse(
count: json["count"],
next: json["next"],
previous: json["previous"],
results: json["results"]
.map((p) => PangeaProfile.fromJson(p))
.toList()
.cast<PangeaProfile>(),
);
}
}

View file

@ -1,179 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:country_picker/country_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/user/models/user_model.dart';
import '../../../widgets/matrix.dart';
import '../../common/controllers/pangea_controller.dart';
import '../models/user_profile_search_model.dart';
import '../repo/user_repo.dart';
import 'find_partner_view.dart';
class FindPartner extends StatefulWidget {
const FindPartner({super.key});
@override
State<FindPartner> createState() => FindPartnerController();
}
class FindPartnerController extends State<FindPartner> {
PangeaController pangeaController = MatrixState.pangeaController;
bool initialLoad = true;
bool loading = false;
String currentSearchTerm = "";
late LanguageModel targetLanguageSearch;
late LanguageModel sourceLanguageSearch;
String? countrySearch;
String? flagEmoji;
//PTODO - implement pagination
String? nextUrl = "";
int nextPage = 1;
Timer? coolDown;
final List<PangeaProfile> _userProfilesCache = [];
final scrollController = ScrollController();
String? error;
@override
void initState() {
targetLanguageSearch = pangeaController.languageController.userL1 ??
pangeaController.pLanguageStore.targetOptions[1];
sourceLanguageSearch = pangeaController.languageController.userL2 ??
pangeaController.pLanguageStore.targetOptions[0];
scrollController.addListener(() {
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
searchUserProfiles();
}
});
searchUserProfiles().then((_) => setState(() => initialLoad = false));
super.initState();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (error != null && error!.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).oopsSomethingWentWrong),
Text(L10n.of(context).errorPleaseRefresh),
],
),
);
}
return FindPartnerView(this);
}
List<PangeaProfile> get userProfiles => _userProfilesCache.where((p) {
return (p.targetLanguage != null &&
targetLanguageSearch.langCode == p.targetLanguage) &&
(p.sourceLanguage != null &&
sourceLanguageSearch.langCode == p.sourceLanguage) &&
(countrySearch == null ||
(p.country != null && countrySearch == p.country));
}).toList();
void searchUserProfilesWithCoolDown(String text) {
coolDown?.cancel();
coolDown = Timer(
const Duration(milliseconds: 0),
() => searchUserProfiles(),
);
}
Future<void> searchUserProfiles() async {
coolDown?.cancel();
if (loading || nextUrl == null) return;
setState(() => loading = true);
UserProfileSearchResponse response;
try {
final String accessToken = pangeaController.userController.accessToken;
response = await PUserRepo.searchUserProfiles(
accessToken: accessToken,
targetLanguage: targetLanguageSearch.langCode,
sourceLanguage: sourceLanguageSearch.langCode,
country: countrySearch,
limit: 15,
pageNumber: nextPage.toString(),
);
} catch (err, s) {
error = err.toString();
setState(() => loading = false);
ErrorHandler.logError(
e: err,
s: s,
data: {
"accessToken": pangeaController.userController.accessToken,
"targetLanguage": targetLanguageSearch.langCode,
"sourceLanguage": sourceLanguageSearch.langCode,
"country": countrySearch,
"pageNumber": nextPage.toString(),
},
);
return;
}
nextUrl = response.next;
nextPage++;
final String? currentUserId = pangeaController.matrixState.client.userID;
_userProfilesCache.addAll(
response.results.where(
(p) =>
!_userProfilesCache.any(
(element) => p.pangeaUserId == element.pangeaUserId,
) &&
p.pangeaUserId != currentUserId,
),
);
setState(() => loading = false);
}
Future<void> filterUserProfiles({
LanguageModel? targetLanguage,
LanguageModel? sourceLanguage,
Country? country,
}) async {
if (country != null) {
if (country.name != "World Wide") {
countrySearch = country.displayNameNoCountryCode;
flagEmoji = country.flagEmoji;
} else {
countrySearch = null;
flagEmoji = null;
}
}
if (targetLanguage != null) {
targetLanguageSearch = targetLanguage;
}
if (sourceLanguage != null) {
sourceLanguageSearch = sourceLanguage;
}
nextPage = 1;
nextUrl = "";
await searchUserProfiles();
setState(() {});
}
}

View file

@ -1,315 +0,0 @@
import 'package:flutter/material.dart';
import 'package:country_picker/country_picker.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/common/widgets/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/learning_settings/utils/country_display.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart';
import 'package:fluffychat/pangea/user/models/user_model.dart';
import 'package:fluffychat/pangea/user/widgets/list_placeholder.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../../widgets/profile_bottom_sheet.dart';
import 'find_partner.dart';
class FindPartnerView extends StatelessWidget {
final FindPartnerController controller;
const FindPartnerView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () => context.pop(),
),
centerTitle: true,
title: const PageTitleText(),
),
body: Center(
child: Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2,
minWidth: FluffyThemes.columnWidth * 2,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
LanguageSelectionRow(
controller: controller,
isSource: true,
),
LanguageSelectionRow(
controller: controller,
isSource: false,
),
Padding(
padding: const EdgeInsets.all(18),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context).iWantALanguagePartnerFrom,
style: const TextStyle(fontSize: 16),
),
Row(
children: [
Text(
controller.countrySearch ??
L10n.of(context).worldWide,
),
Padding(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
child: controller.flagEmoji != null
? RichText(
text: TextSpan(
text: controller.flagEmoji,
style: const TextStyle(fontSize: 30),
),
)
: const PangeaLogoSvg(width: 30),
),
IconButton(
icon: const Icon(Icons.expand_more),
onPressed: () => showCountryPicker(
showWorldWide: true,
context: context,
showPhoneCode: false,
onSelect: (Country country) {
controller.filterUserProfiles(
country: country,
);
},
),
),
],
),
],
),
),
controller.initialLoad
? const ExpandedContainer(body: ListPlaceholder())
: controller.userProfiles.isNotEmpty
? ExpandedContainer(
body: ListView.builder(
controller: controller.scrollController,
itemCount: controller.userProfiles.length + 1,
itemBuilder: (context, i) => i !=
controller.userProfiles.length
? UserProfileEntry(
pangeaProfile: controller.userProfiles[i],
controller: controller,
)
: controller.loading
? const Center(
child: CircularProgressIndicator
.adaptive(),
)
: const SizedBox.shrink(),
),
)
: ExpandedContainer(
body: Center(
child: Text(L10n.of(context).noResults),
),
),
],
),
),
),
);
}
}
class ExpandedContainer extends StatelessWidget {
const ExpandedContainer({
super.key,
required this.body,
});
final Widget body;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(0, 20, 0, 20),
child: body,
),
);
}
}
class ProfileSearchTextField extends StatelessWidget {
const ProfileSearchTextField({
super.key,
required this.controller,
});
final FindPartnerController controller;
@override
Widget build(BuildContext context) {
return TextField(
autofocus: true,
decoration: InputDecoration(
hintText: L10n.of(context).searchBy,
suffixIconConstraints: const BoxConstraints(
maxWidth: 48,
maxHeight: 48,
minWidth: 48,
),
suffixIcon: controller.initialLoad
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.search_outlined),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
onChanged: controller.searchUserProfilesWithCoolDown,
);
}
}
class PageTitleText extends StatelessWidget {
const PageTitleText({
super.key,
});
@override
Widget build(BuildContext context) {
return FittedBox(
child: Text(
L10n.of(context).iWantAConversationPartner,
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 18,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
);
}
}
class LanguageSelectionRow extends StatelessWidget {
const LanguageSelectionRow({
super.key,
required this.controller,
required this.isSource,
});
final FindPartnerController controller;
final bool isSource;
@override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: ListTile(
title: isSource
? Text(
L10n.of(context).iWantALanguagePartnerWhoSpeaks,
style: const TextStyle(fontSize: 16),
)
: Text(
L10n.of(context).iWantALanguagePartnerWhoIsLearning,
style: const TextStyle(fontSize: 16),
),
),
),
Flexible(
child: PLanguageDropdown(
languages: isSource
? controller.pangeaController.pLanguageStore.baseOptions
: controller.pangeaController.pLanguageStore.targetOptions,
onChange: (language) {
controller.filterUserProfiles(
sourceLanguage: isSource ? language : null,
targetLanguage: isSource ? null : language,
);
},
isL2List: !isSource,
initialLanguage: isSource
? controller.sourceLanguageSearch
: controller.targetLanguageSearch,
decorationText: isSource
? L10n.of(context).myBaseLanguage
: L10n.of(context).iWantToLearn,
),
),
],
);
}
}
class UserProfileEntry extends StatelessWidget {
final PangeaProfile pangeaProfile;
final FindPartnerController controller;
const UserProfileEntry({
super.key,
required this.pangeaProfile,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FutureBuilder<matrix.Profile>(
future: Matrix.of(context)
.client
.getProfileFromUserId(pangeaProfile.pangeaUserId),
builder: ((context, snapshot) {
final matrixProfile = snapshot.data;
return ListTile(
leading: Avatar(
name: matrixProfile == null || matrixProfile.avatarUrl == null
? pangeaProfile.pangeaUserId
: null,
mxContent: matrixProfile?.avatarUrl,
),
title: Row(
children: [
Flexible(
child: Text(
//PTODO - get matrix u and show displayName
matrixProfile?.displayName ??
pangeaProfile.pangeaUserId.replaceAll(
":${AppConfig.defaultHomeserver.replaceAll("matrix.", "")}",
"",
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 20),
RichText(
text: TextSpan(
text: CountryDisplayUtil.flagEmoji(pangeaProfile.country),
style: const TextStyle(fontSize: 15),
),
),
],
),
onTap: () => showModalBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: pangeaProfile.pangeaUserId,
outerContext: context,
),
),
);
}),
),
],
);
}
}

View file

@ -1,48 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import '../../common/network/requests.dart';
import '../../common/network/urls.dart';
import '../models/user_profile_search_model.dart';
class PUserRepo {
static Future<UserProfileSearchResponse> searchUserProfiles({
// List<String>? interests,
String? targetLanguage,
String? sourceLanguage,
String? country,
// String? speaks,
String? pageNumber,
required String accessToken,
required int limit,
}) async {
final Requests req = Requests(
accessToken: accessToken,
choreoApiKey: Environment.choreoApiKey,
);
final Map<String, dynamic> body = {};
// if (interests != null) body[ModelKey.userInterests] = interests.toString();
if (targetLanguage != null) {
body[ModelKey.userTargetLanguage] = targetLanguage;
}
if (sourceLanguage != null) {
body[ModelKey.userSourceLanguage] = sourceLanguage;
}
if (country != null) body[ModelKey.userCountry] = country;
final String searchUrl =
"${PApiUrls.searchUserProfiles}?limit=$limit${pageNumber != null ? '&page=$pageNumber' : ''}";
final Response res = await req.post(
url: searchUrl,
body: body,
);
//PTODO - implement paginiation - make another call with next url
return UserProfileSearchResponse.fromJson(jsonDecode(res.body));
}
}

View file

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
class ListPlaceholder extends StatelessWidget {
static const dummyChatCount = 5;
const ListPlaceholder({super.key});
@override
Widget build(BuildContext context) {
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return ListView.builder(
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: Material(
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
),
),
),
);
}
}

View file

@ -1,113 +0,0 @@
// // presents choices from vocab_bank_repo
// // displays them as emoji choices
// // once selection, these words are inserted into the input bar
// import 'dart:async';
// import 'package:fluffychat/config/themes.dart';
// import 'package:fluffychat/pages/chat/chat.dart';
// import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
// import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
// import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
// import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
// import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_emoji_choice_item.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_bank_repo.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_request.dart';
// import 'package:fluffychat/pangea/word_bank/vocab_response.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// import 'package:flutter/material.dart';
// class WritingAssistanceInputRow extends StatefulWidget {
// final ChatController controller;
// const WritingAssistanceInputRow(
// this.controller, {
// super.key,
// });
// @override
// WritingAssistanceInputRowState createState() =>
// WritingAssistanceInputRowState();
// }
// class WritingAssistanceInputRowState extends State<WritingAssistanceInputRow> {
// List<ConstructIdentifier> suggestions = [];
// StreamSubscription? _choreoSub;
// Choreographer get choreographer => widget.controller.choreographer;
// @override
// void initState() {
// // Rebuild the widget each time there's an update from choreo
// _choreoSub = choreographer.stateListener.stream.listen((_) {
// setSuggestions();
// });
// setSuggestions();
// super.initState();
// }
// @override
// void dispose() {
// _choreoSub?.cancel();
// super.dispose();
// }
// Future<void> setSuggestions() async {
// final String currentText = choreographer.currentText;
// final VocabRequest request = VocabRequest(
// langCode: MatrixState
// .pangeaController.languageController.userL2?.langCodeShort ??
// LanguageKeys.defaultLanguage,
// level: MatrixState
// .pangeaController.userController.profile.userSettings.cefrLevel,
// prefix: currentText,
// );
// final VocabResponse response = await VocabRepo.get(request);
// setState(() {
// suggestions = response.vocab;
// });
// }
// @override
// Widget build(BuildContext context) {
// return AnimatedContainer(
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// child: SingleChildScrollView(
// scrollDirection: Axis.horizontal,
// child: Row(
// children: suggestions
// .map(
// (suggestion) => MessageEmojiChoiceItem(
// topContent: EmojiStack(
// emoji: suggestion.userSetEmoji,
// // suggestion.userSetEmoji ??
// // MatrixState
// // .pangeaController.getAnalytics.constructListModel
// // .getConstructUses(suggestion)
// // ?.xpEmoji ??
// // AnalyticsConstants.emojiForSeed,
// style: const TextStyle(fontSize: 24),
// ),
// content: suggestion.lemma,
// onTap: () {
// choreographer.onPredictorSelect(suggestion.lemma);
// // setState(() {
// // suggestions = [];
// // });
// },
// isSelected: false,
// textSize: 16,
// greenHighlight: false,
// ),
// )
// .toList(),
// ),
// ),
// );
// }
// }

View file

@ -5,11 +5,17 @@ import 'package:fluffychat/config/themes.dart';
class TwoColumnLayout extends StatelessWidget {
final Widget mainView;
final Widget sideView;
// #Pangea
final Color? dividerColor;
// Pangea#
const TwoColumnLayout({
super.key,
required this.mainView,
required this.sideView,
// #Pangea
this.dividerColor,
// Pangea#
});
@override
Widget build(BuildContext context) {
@ -27,7 +33,10 @@ class TwoColumnLayout extends StatelessWidget {
),
Container(
width: 1.0,
color: theme.dividerColor,
// #Pangea
// color: theme.dividerColor,
color: dividerColor ?? theme.dividerColor,
// Pangea#
),
Expanded(
child: ClipRRect(

View file

@ -7,7 +7,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/navi_rail_item.dart';
import 'package:fluffychat/pangea/spaces/utils/space_code.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
@ -17,11 +16,17 @@ class SpacesNavigationRail extends StatelessWidget {
final String? activeSpaceId;
final void Function() onGoToChats;
final void Function(String) onGoToSpaceId;
// #Pangea
final void Function()? clearActiveSpace;
// Pangea#
const SpacesNavigationRail({
required this.activeSpaceId,
required this.onGoToChats,
required this.onGoToSpaceId,
// #Pangea
this.clearActiveSpace,
// Pangea#
super.key,
});
@ -35,12 +40,9 @@ class SpacesNavigationRail extends StatelessWidget {
.path
.startsWith('/rooms/settings');
// #Pangea
final isHomepage = GoRouter.of(context)
.routeInformationProvider
.value
.uri
.path
.contains('homepage');
final path = GoRouter.of(context).routeInformationProvider.value.uri.path;
final isHomepage = path.contains('homepage');
final isCommunities = path.contains('communities');
final isColumnMode = FluffyThemes.isColumnMode(context);
// return StreamBuilder(
return Material(
@ -78,18 +80,25 @@ class SpacesNavigationRail extends StatelessWidget {
scrollDirection: Axis.vertical,
// #Pangea
// itemCount: rootSpaces.length + 2,
itemCount: rootSpaces.length + 4,
itemCount: rootSpaces.length + 3,
// Pangea#
itemBuilder: (context, i) {
// #Pangea
if (i == 0) {
return NaviRailItem(
isSelected: isColumnMode
? activeSpaceId == null && !isSettings
? activeSpaceId == null &&
!isSettings &&
!isCommunities
: isHomepage,
onTap: () => isColumnMode
? onGoToChats()
: context.go("/rooms/homepage"),
onTap: () {
if (isColumnMode) {
onGoToChats();
} else {
clearActiveSpace?.call();
context.go("/rooms/homepage");
}
},
backgroundColor: Colors.transparent,
icon: FutureBuilder<Profile>(
future: client.fetchOwnProfile(),
@ -122,7 +131,8 @@ class SpacesNavigationRail extends StatelessWidget {
// isSelected: activeSpaceId == null && !isSettings,
isSelected: activeSpaceId == null &&
!isSettings &&
!isHomepage,
!isHomepage &&
!isCommunities,
// Pangea#
onTap: onGoToChats,
icon: const Padding(
@ -139,28 +149,26 @@ class SpacesNavigationRail extends StatelessWidget {
}
i--;
if (i == rootSpaces.length) {
// #Pangea
return NaviRailItem(
isSelected: false,
onTap: () =>
SpaceCodeUtil.joinWithSpaceCodeDialog(context),
// #Pangea
// isSelected: false,
// onTap: () => context.go('/rooms/newspace'),
// icon: const Padding(
// padding: EdgeInsets.all(8.0),
// child: Icon(Icons.add),
// ),
// toolTip: L10n.of(context).createNewSpace,
isSelected: isCommunities,
onTap: () {
clearActiveSpace?.call();
context.go('/rooms/communities');
},
icon: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.join_right_outlined),
padding: EdgeInsets.all(10.0),
child: Icon(Icons.groups),
),
toolTip: L10n.of(context).joinByCode,
);
}
if (i == rootSpaces.length + 1) {
// Pangea#
return NaviRailItem(
isSelected: false,
onTap: () => context.go('/rooms/newspace'),
icon: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.add),
),
toolTip: L10n.of(context).createNewSpace,
toolTip: L10n.of(context).findYourPeople,
// Pangea#
);
}
final space = rootSpaces[i];