diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 0281396e8..d4750264b 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4855,5 +4855,6 @@ "goodJobTranslation": "Good work on this translation.", "makingProgress": "You're making progress!", "keepPracticing": "Keep practicing!", - "niceJob": "Nice job!" + "niceJob": "Nice job!", + "publicSpacesTitle": "Learning communities" } \ No newline at end of file diff --git a/lib/pangea/activity_planner/activity_planner_page.dart b/lib/pangea/activity_planner/activity_planner_page.dart index 18df46a14..e9caefe00 100644 --- a/lib/pangea/activity_planner/activity_planner_page.dart +++ b/lib/pangea/activity_planner/activity_planner_page.dart @@ -45,7 +45,6 @@ class ActivityPlannerPageState extends State { child: SingleChildScrollView( child: ActivitySuggestionsArea( scrollDirection: Axis.vertical, - showCreateChatCard: false, room: room, ), ), diff --git a/lib/pangea/activity_planner/bookmarked_activity_list.dart b/lib/pangea/activity_planner/bookmarked_activity_list.dart index 9bbd41ae1..b872190de 100644 --- a/lib/pangea/activity_planner/bookmarked_activity_list.dart +++ b/lib/pangea/activity_planner/bookmarked_activity_list.dart @@ -36,7 +36,6 @@ class BookmarkedActivitiesListState extends State { bool get _isColumnMode => FluffyThemes.isColumnMode(context); double get cardHeight => _isColumnMode ? 315.0 : 240.0; - double get cardPadding => _isColumnMode ? 8.0 : 0.0; double get cardWidth => _isColumnMode ? 225.0 : 150.0; Future _onEdit( @@ -98,7 +97,6 @@ class BookmarkedActivitiesListState extends State { }, width: cardWidth, height: cardHeight, - padding: cardPadding, onChange: () => setState(() {}), ); }).toList(), diff --git a/lib/pangea/activity_suggestions/activity_suggestion_card.dart b/lib/pangea/activity_suggestions/activity_suggestion_card.dart index 016334287..b019c364f 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_card.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_card.dart @@ -16,7 +16,6 @@ class ActivitySuggestionCard extends StatelessWidget { final double width; final double height; - final double padding; final bool selected; final VoidCallback onChange; @@ -27,7 +26,6 @@ class ActivitySuggestionCard extends StatelessWidget { required this.onPressed, required this.width, required this.height, - required this.padding, required this.onChange, this.selected = false, this.image, @@ -38,175 +36,171 @@ class ActivitySuggestionCard extends StatelessWidget { final theme = Theme.of(context); final isBookmarked = BookmarkedActivitiesRepo.isBookmarked(activity); - return Padding( - padding: EdgeInsets.all(padding), - child: PressableButton( - depressed: selected || onPressed == null, - onPressed: onPressed, - borderRadius: BorderRadius.circular(24.0), - color: theme.brightness == Brightness.dark - ? theme.colorScheme.primary - : theme.colorScheme.surfaceContainerHighest, - colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2, - child: Container( - decoration: BoxDecoration( - border: selected - ? Border.all( - color: theme.colorScheme.primary, - ) - : null, - borderRadius: BorderRadius.circular(24.0), - ), - height: height, - width: width, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(24.0), - ), + return PressableButton( + depressed: selected || onPressed == null, + onPressed: onPressed, + borderRadius: BorderRadius.circular(24.0), + color: theme.brightness == Brightness.dark + ? theme.colorScheme.primary + : theme.colorScheme.surfaceContainerHighest, + colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2, + child: Container( + decoration: BoxDecoration( + border: selected + ? Border.all( + color: theme.colorScheme.primary, + ) + : null, + borderRadius: BorderRadius.circular(24.0), + ), + height: height, + width: width, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(24.0), ), - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - height: width - 16.0, - width: width - 16.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - ), - margin: const EdgeInsets.only(top: 8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: image != null - ? Image.memory(image!) - : activity.imageURL != null - ? activity.imageURL!.startsWith("mxc") - ? MxcImage( - uri: Uri.parse(activity.imageURL!), - width: width - 16.0, - height: width - 16.0, - cacheKey: activity.bookmarkId, - ) - : CachedNetworkImage( - imageUrl: activity.imageURL!, - placeholder: (context, url) => - const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => - const SizedBox(), - ) - : null, - ), + ), + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: width - 16.0, + width: width - 16.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.0), ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - spacing: 8.0, - children: [ - Row( - children: [ + margin: const EdgeInsets.only(top: 8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: image != null + ? Image.memory(image!) + : activity.imageURL != null + ? activity.imageURL!.startsWith("mxc") + ? MxcImage( + uri: Uri.parse(activity.imageURL!), + width: width - 16.0, + height: width - 16.0, + cacheKey: activity.bookmarkId, + ) + : CachedNetworkImage( + imageUrl: activity.imageURL!, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + const SizedBox(), + ) + : null, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8.0, + children: [ + Row( + children: [ + Flexible( + child: Text( + activity.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(24.0), + ), + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 8.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + const Icon( + Icons.group_outlined, + size: 12.0, + ), + Text( + "${activity.req.numberOfParticipants}", + style: theme.textTheme.labelSmall, + ), + ], + ), + ), + if (activity.req.mode.isNotEmpty) Flexible( - child: Text( - activity.title, - style: const TextStyle( - fontWeight: FontWeight.bold, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(24.0), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - Container( - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(24.0), - ), - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - const Icon( - Icons.group_outlined, - size: 16.0, - ), - Text( - "${activity.req.numberOfParticipants}", - style: theme.textTheme.bodySmall, - ), - ], - ), - ), - if (activity.req.mode.isNotEmpty) - Flexible( - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(24.0), - ), - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - child: Text( - activity.req.mode, - style: theme.textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 8.0, + ), + child: Text( + activity.req.mode, + style: theme.textTheme.labelSmall, + overflow: TextOverflow.ellipsis, ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ), - ], - ), - Positioned( - top: 4.0, - right: 4.0, - child: IconButton( - icon: Icon( - isBookmarked ? Icons.bookmark : Icons.bookmark_border, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - onPressed: onPressed != null - ? () async { - await (isBookmarked - ? BookmarkedActivitiesRepo.remove( - activity.bookmarkId, - ) - : BookmarkedActivitiesRepo.save(activity)); - onChange(); - } - : null, - style: IconButton.styleFrom( - backgroundColor: Theme.of(context) - .colorScheme - .primaryContainer - .withAlpha(180), - ), + ), + ], + ), + Positioned( + top: 4.0, + right: 4.0, + child: IconButton( + icon: Icon( + isBookmarked ? Icons.bookmark : Icons.bookmark_border, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + onPressed: onPressed != null + ? () async { + await (isBookmarked + ? BookmarkedActivitiesRepo.remove( + activity.bookmarkId, + ) + : BookmarkedActivitiesRepo.save(activity)); + onChange(); + } + : null, + style: IconButton.styleFrom( + backgroundColor: Theme.of(context) + .colorScheme + .primaryContainer + .withAlpha(180), ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart index e16908b63..4f3f04614 100644 --- a/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart +++ b/lib/pangea/activity_suggestions/activity_suggestion_carousel.dart @@ -236,7 +236,6 @@ class ActivitySuggestionCarouselState widget.enabled ? _onClickCard : null, width: _cardWidth, height: _cardHeight, - padding: 0.0, image: _currentActivity == widget.selectedActivity ? widget.selectedActivityImage diff --git a/lib/pangea/activity_suggestions/activity_suggestions_area.dart b/lib/pangea/activity_suggestions/activity_suggestions_area.dart index abd147974..61ee9cbec 100644 --- a/lib/pangea/activity_suggestions/activity_suggestions_area.dart +++ b/lib/pangea/activity_suggestions/activity_suggestions_area.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart'; @@ -14,22 +16,22 @@ import 'package:fluffychat/pangea/activity_planner/media_enum.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_plan_search_repo.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_card.dart'; import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart'; +import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; +import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; class ActivitySuggestionsArea extends StatefulWidget { final Axis? scrollDirection; - final bool showCreateChatCard; - final bool showMakeActivityCard; + final bool showTitle; final Room? room; const ActivitySuggestionsArea({ super.key, this.scrollDirection, - this.showCreateChatCard = true, - this.showMakeActivityCard = true, + this.showTitle = false, this.room, }); @override @@ -55,7 +57,6 @@ class ActivitySuggestionsAreaState extends State { final List _activityItems = []; final ScrollController _scrollController = ScrollController(); double get cardHeight => _isColumnMode ? 315.0 : 240.0; - double get cardPadding => _isColumnMode ? 8.0 : 0.0; double get cardWidth => _isColumnMode ? 225.0 : 150.0; Future _setActivityItems() async { @@ -83,6 +84,8 @@ class ActivitySuggestionsAreaState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + final List cards = _loading ? List.generate(5, (i) { return Shimmer.fromColors( @@ -91,7 +94,6 @@ class ActivitySuggestionsAreaState extends State { child: Container( height: cardHeight, width: cardWidth, - margin: EdgeInsets.all(cardPadding), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(24.0), @@ -117,7 +119,6 @@ class ActivitySuggestionsAreaState extends State { }, width: cardWidth, height: cardHeight, - padding: cardPadding, onChange: () { if (mounted) setState(() {}); }, @@ -129,28 +130,141 @@ class ActivitySuggestionsAreaState extends State { final scrollDirection = widget.scrollDirection ?? (_isColumnMode ? Axis.horizontal : Axis.vertical); - return scrollDirection == Axis.horizontal - ? ConstrainedBox( - constraints: BoxConstraints(maxHeight: cardHeight + 36.0), - child: Scrollbar( - controller: _scrollController, - thumbVisibility: true, - child: ListView( - controller: _scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(vertical: 4.0), - children: cards, + return Column( + spacing: 8.0, + children: [ + if (widget.showTitle) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + L10n.of(context).startChat, + style: isColumnMode + ? theme.textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold) + : theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), ), - ), - ) - : SizedBox( - width: MediaQuery.of(context).size.width, - child: Wrap( - alignment: WrapAlignment.spaceEvenly, - runSpacing: 16.0, - spacing: 4.0, - children: cards, - ), - ); + Row( + spacing: 8.0, + children: [ + InkWell( + customBorder: const CircleBorder(), + onTap: () => context.go('/rooms/newgroup'), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(36.0), + ), + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 10.0, + ), + child: Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + CustomizedSvg( + svgUrl: + "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.plusIconPath}", + colorReplacements: { + "#CDBEF9": colorToHex( + Theme.of(context).colorScheme.secondary, + ), + }, + height: 16.0, + width: 16.0, + ), + Text( + isColumnMode + ? L10n.of(context).createOwnChat + : L10n.of(context).chat, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () => context.go('/rooms/planner'), + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(36.0), + ), + padding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 10.0, + ), + child: Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + CustomizedSvg( + svgUrl: + "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}", + colorReplacements: { + "#CDBEF9": colorToHex( + Theme.of(context).colorScheme.secondary, + ), + }, + height: 16.0, + width: 16.0, + ), + Text( + isColumnMode + ? L10n.of(context).makeYourOwnActivity + : L10n.of(context).createActivity, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + Container( + decoration: const BoxDecoration(), + child: scrollDirection == Axis.horizontal + ? Expanded( + child: Scrollbar( + thumbVisibility: true, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: Row( + spacing: 8.0, + children: cards, + ), + ), + ), + ), + ) + : SizedBox( + width: MediaQuery.of(context).size.width, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + runSpacing: 16.0, + spacing: 4.0, + children: cards, + ), + ), + ), + ], + ); } } diff --git a/lib/pangea/activity_suggestions/suggestions_page.dart b/lib/pangea/activity_suggestions/suggestions_page.dart index bdb97c88f..73115cade 100644 --- a/lib/pangea/activity_suggestions/suggestions_page.dart +++ b/lib/pangea/activity_suggestions/suggestions_page.dart @@ -1,147 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.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/activity_suggestions/activity_suggestions_area.dart'; -import 'package:fluffychat/pangea/activity_suggestions/activity_suggestions_constants.dart'; import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart'; -import 'package:fluffychat/pangea/common/widgets/customized_svg.dart'; +import 'package:fluffychat/pangea/public_spaces/public_spaces_area.dart'; class SuggestionsPage extends StatelessWidget { const SuggestionsPage({super.key}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); final isColumnMode = FluffyThemes.isColumnMode(context); return SafeArea( child: SingleChildScrollView( child: Padding( - padding: EdgeInsets.symmetric( - horizontal: isColumnMode ? 36.0 : 4.0, + padding: const EdgeInsets.symmetric( + horizontal: 24.0, vertical: 16.0, ), child: Column( mainAxisSize: MainAxisSize.min, + spacing: 24.0, children: [ - if (!isColumnMode) - Padding( - padding: - EdgeInsets.symmetric(horizontal: isColumnMode ? 0 : 12.0), - child: const LearningProgressIndicators(), - ), - Padding( - padding: EdgeInsets.only( - left: isColumnMode ? 0.0 : 4.0, - right: isColumnMode ? 0.0 : 4.0, - top: 16.0, - bottom: 16.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - L10n.of(context).startChat, - style: isColumnMode - ? theme.textTheme.titleLarge - ?.copyWith(fontWeight: FontWeight.bold) - : theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Row( - spacing: 8.0, - children: [ - InkWell( - customBorder: const CircleBorder(), - onTap: () => context.go('/rooms/newgroup'), - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(36.0), - ), - padding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 10.0, - ), - child: Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - CustomizedSvg( - svgUrl: - "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.plusIconPath}", - colorReplacements: { - "#CDBEF9": colorToHex( - Theme.of(context).colorScheme.secondary, - ), - }, - height: 16.0, - width: 16.0, - ), - Text( - isColumnMode - ? L10n.of(context).createOwnChat - : L10n.of(context).chat, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () => context.go('/rooms/planner'), - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(36.0), - ), - padding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 10.0, - ), - child: Row( - spacing: 8.0, - mainAxisSize: MainAxisSize.min, - children: [ - CustomizedSvg( - svgUrl: - "${AppConfig.assetsBaseURL}/${ActivitySuggestionsConstants.crayonIconPath}", - colorReplacements: { - "#CDBEF9": colorToHex( - Theme.of(context).colorScheme.secondary, - ), - }, - height: 16.0, - width: 16.0, - ), - Text( - isColumnMode - ? L10n.of(context).makeYourOwnActivity - : L10n.of(context).createActivity, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ), - ], - ), - ], - ), + if (!isColumnMode) const LearningProgressIndicators(), + const ActivitySuggestionsArea( + showTitle: true, + scrollDirection: Axis.horizontal, ), - const ActivitySuggestionsArea(), + const PublicSpacesArea(), ], ), ), diff --git a/lib/pangea/public_spaces/public_space_card.dart b/lib/pangea/public_spaces/public_space_card.dart new file mode 100644 index 000000000..ff6ac87bb --- /dev/null +++ b/lib/pangea/public_spaces/public_space_card.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; + +class PublicSpaceCard extends StatelessWidget { + final PublicRoomsChunk space; + final double width; + final double height; + + const PublicSpaceCard({ + super.key, + required this.space, + required this.width, + required this.height, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return PressableButton( + onPressed: () {}, + borderRadius: BorderRadius.circular(24.0), + color: theme.brightness == Brightness.dark + ? theme.colorScheme.primary + : theme.colorScheme.surfaceContainerHighest, + colorFactor: theme.brightness == Brightness.dark ? 0.6 : 0.2, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.0), + ), + height: height, + width: width, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(24.0), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: height - 16.0, + width: height - 16.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.0), + ), + margin: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: space.avatarUrl != null + ? MxcImage( + uri: space.avatarUrl!, + width: width - 16.0, + height: width - 16.0, + ) + : const SizedBox(), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + spacing: 4.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + space.name ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(24.0), + ), + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 8.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + const Icon( + Icons.group_outlined, + size: 12.0, + ), + Text( + L10n.of(context).countParticipants( + space.numJoinedMembers, + ), + style: theme.textTheme.labelSmall, + ), + ], + ), + ), + ], + ), + if (space.topic != null) + Flexible( + child: Text( + space.topic!, + style: theme.textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + maxLines: 4, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/public_spaces/public_spaces_area.dart b/lib/pangea/public_spaces/public_spaces_area.dart new file mode 100644 index 000000000..0dfa372de --- /dev/null +++ b/lib/pangea/public_spaces/public_spaces_area.dart @@ -0,0 +1,217 @@ +// shows n rows of activity suggestions vertically, where n is the number of rows +// as the user tries to scroll horizontally to the right, the client will fetch more activity suggestions + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shimmer/shimmer.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/public_spaces/public_space_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PublicSpacesArea extends StatefulWidget { + const PublicSpacesArea({super.key}); + + @override + PublicSpacesAreaState createState() => PublicSpacesAreaState(); +} + +class PublicSpacesAreaState extends State { + @override + void initState() { + super.initState(); + _setSpaceItems(); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + _coolDown?.cancel(); + super.dispose(); + } + + bool _loading = true; + bool _isSearching = false; + + final List _spaceItems = []; + + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + Timer? _coolDown; + + final double cardHeight = 150.0; + final double cardWidth = 450.0; + + Future _setSpaceItems() async { + _spaceItems.clear(); + setState(() => _loading = true); + 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)); + }); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + void _onSearchEnter(String text, {bool globalSearch = true}) { + if (text.isEmpty) { + _setSpaceItems(); + return; + } + + _coolDown?.cancel(); + _coolDown = Timer(const Duration(milliseconds: 500), _setSpaceItems); + } + + void _toggleSearching() { + setState(() { + _isSearching = !_isSearching; + _searchController.clear(); + _setSpaceItems(); + }); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isColumnMode = FluffyThemes.isColumnMode(context); + + final List cards = _loading && _spaceItems.isEmpty + ? List.generate(5, (i) { + return Shimmer.fromColors( + baseColor: theme.colorScheme.primary.withAlpha(20), + highlightColor: theme.colorScheme.primary.withAlpha(50), + child: Container( + height: cardHeight, + width: cardWidth, + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(24.0), + ), + ), + ); + }) + : _spaceItems + .map((space) { + return PublicSpaceCard( + space: space, + width: cardWidth, + height: cardHeight, + ); + }) + .cast() + .toList(); + + if (_loading && _spaceItems.isNotEmpty) { + cards.add( + const Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator.adaptive(), + ), + ); + } + + return Column( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: child, + ), + child: _isSearching + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + key: const ValueKey('search'), + children: [ + Expanded( + child: TextField( + autofocus: true, + controller: _searchController, + onChanged: _onSearchEnter, + decoration: const InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 12.0, + ), + isDense: true, + border: OutlineInputBorder(), + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: _toggleSearching, + ), + ], + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + key: const ValueKey('title'), + children: [ + Text( + L10n.of(context).publicSpacesTitle, + style: isColumnMode + ? theme.textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold) + : theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.search), + onPressed: _toggleSearching, + ), + ], + ), + ), + Container( + decoration: const BoxDecoration(), + child: Expanded( + child: Scrollbar( + thumbVisibility: true, + controller: _scrollController, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + child: Row( + spacing: 8.0, + children: cards, + ), + ), + ), + ), + ), + ), + ], + ); + } +}