feat: add activity suggestions to new chat page (#2235)

This commit is contained in:
ggurdin 2025-03-27 10:37:08 -04:00 committed by GitHub
parent 0faeb6f6ae
commit ee11c5596b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 328 additions and 4 deletions

View file

@ -4833,5 +4833,8 @@
"youUnlocked": "You've unlocked",
"resetInstructionTooltipsTitle": "Reset instruction tooltips",
"resetInstructionTooltipsDesc": "Click to show instruction tooltips like for a brand new user.",
"selectForGrammar": "Select a grammar icon for activities and details."
"selectForGrammar": "Select a grammar icon for activities and details.",
"newChatActivityTitle": "Add a fun activity",
"newChatActivityDesc": "Make every group chat an adventure with Activity Planner! Set captivating topics and objectives for the group, and bring conversations to life with stunning images. Spark imaginative discussions and keep the fun flowing effortlessly!",
"exploreMore": "Explore more"
}

View file

@ -10,6 +10,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/new_group/new_group_view.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
@ -36,6 +37,7 @@ class NewGroupController extends State<NewGroup> {
TextEditingController nameController = TextEditingController();
// #Pangea
ActivityPlanModel? _selectedActivity;
bool requiredCodeToJoin = false;
// bool publicGroup = false;
// Pangea#
@ -61,6 +63,9 @@ class NewGroupController extends State<NewGroup> {
// void setPublicGroup(bool b) =>
// setState(() => publicGroup = groupCanBeFound = b);
void setRequireCode(bool b) => setState(() => requiredCodeToJoin = b);
void setSelectedActivity(ActivityPlanModel? activity) =>
setState(() => _selectedActivity = activity);
// Pangea#
void setGroupCanBeFound(bool b) => setState(() => groupCanBeFound = b);
@ -113,6 +118,15 @@ class NewGroupController extends State<NewGroup> {
);
if (!mounted) return;
// #Pangea
if (_selectedActivity != null) {
Room? room = Matrix.of(context).client.getRoomById(roomId);
if (room == null) {
await Matrix.of(context).client.waitForRoomInSync(roomId);
room = Matrix.of(context).client.getRoomById(roomId);
}
if (room == null) return;
await room.sendActivityPlan(_selectedActivity!);
}
// if a timeout happened, don't redirect to the chat
if (error != null) return;
// Pangea#

View file

@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/new_group/new_group.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_carousel.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
@ -36,6 +37,9 @@ class NewGroupView extends StatelessWidget {
),
),
body: MaxWidthBody(
// #Pangea
showBorder: false,
// Pangea#
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -171,6 +175,13 @@ class NewGroupView extends StatelessWidget {
// onChanged: null,
// ),
// ),
if (controller.createGroupType == CreateGroupType.group)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ActivitySuggestionCarousel(
onActivitySelected: controller.setSelectedActivity,
),
),
// Pangea#
AnimatedSize(
duration: FluffyThemes.animationDuration,

View file

@ -79,7 +79,7 @@ class ActivitySuggestionCard extends StatelessWidget {
style: const TextStyle(
fontWeight: FontWeight.bold,
),
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),

View file

@ -0,0 +1,285 @@
import 'dart:ui';
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:shimmer/shimmer.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';
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/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 ActivitySuggestionCarousel extends StatefulWidget {
final Function(ActivityPlanModel?) onActivitySelected;
const ActivitySuggestionCarousel({
required this.onActivitySelected,
super.key,
});
@override
ActivitySuggestionCarouselState createState() =>
ActivitySuggestionCarouselState();
}
class ActivitySuggestionCarouselState
extends State<ActivitySuggestionCarousel> {
bool _isOpen = true;
bool _loading = true;
String? _error;
double get _cardWidth => _isColumnMode ? 250.0 : 200.0;
final double _cardHeight = 275.0;
ActivityPlanModel? _currentActivity;
final List<ActivityPlanModel> _activityItems = [];
@override
void initState() {
super.initState();
_setActivityItems();
}
Future<void> _setActivityItems() async {
try {
final ActivityPlanRequest request = ActivityPlanRequest(
topic: "",
mode: "",
objective: "",
media: MediaEnum.nan,
cefrLevel: LanguageLevelTypeEnum.a1,
languageOfInstructions: LanguageKeys.defaultLanguage,
targetLanguage:
MatrixState.pangeaController.languageController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
numberOfParticipants: 3,
count: 5,
);
final resp = await ActivitySearchRepo.get(request);
_activityItems.addAll(resp.activityPlans);
} catch (e) {
_error = e.toString();
} finally {
_loading = false;
_currentActivity =
_activityItems.isNotEmpty ? _activityItems.first : null;
if (mounted) setState(() {});
}
}
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
int? get _currentIndex {
if (_currentActivity == null) return null;
final index = _activityItems.indexOf(_currentActivity!);
return index == -1 ? null : index;
}
bool get _canMoveLeft => _currentIndex != null && _currentIndex! > 0;
bool get _canMoveRight =>
_currentIndex != null && _currentIndex! < _activityItems.length - 1;
void _moveLeft() {
if (!_canMoveLeft) return;
_setActivityByIndex(_currentIndex! - 1);
}
void _moveRight() {
if (!_canMoveRight) return;
_setActivityByIndex(_currentIndex! + 1);
}
void _setActivityByIndex(int index) {
if (index < 0 || index >= _activityItems.length) return;
widget.onActivitySelected(_activityItems[index]);
setState(() {
_currentActivity = _activityItems[index];
});
}
void _close() {
widget.onActivitySelected(null);
setState(() => _isOpen = false);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedSize(
duration: FluffyThemes.animationDuration,
child: !_isOpen
? const SizedBox.shrink()
: Container(
decoration: BoxDecoration(
border: Border.all(color: theme.dividerColor),
borderRadius: BorderRadius.circular(24.0),
),
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 16.0,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
L10n.of(context).newChatActivityTitle,
style: theme.textTheme.titleLarge,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: _close,
),
],
),
Text(L10n.of(context).newChatActivityDesc),
Row(
spacing: _isColumnMode ? 16.0 : 4.0,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MouseRegion(
cursor: _canMoveLeft
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveLeft ? _moveLeft : null,
child: Icon(
Icons.chevron_left_outlined,
size: 32.0,
color: _canMoveLeft ? null : theme.disabledColor,
),
),
),
Container(
constraints:
BoxConstraints(maxHeight: _cardHeight + 12.0),
child: _error != null ||
(_currentActivity == null && !_loading)
? const SizedBox.shrink()
: _loading
? Shimmer.fromColors(
baseColor:
theme.colorScheme.primary.withAlpha(50),
highlightColor: theme.colorScheme.primary
.withAlpha(150),
child: Container(
height: _cardHeight,
width: _cardWidth,
decoration: BoxDecoration(
color:
theme.colorScheme.surfaceContainer,
borderRadius:
BorderRadius.circular(24.0),
),
),
)
: ActivitySuggestionCard(
activity: _currentActivity!,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
activity: _currentActivity!,
);
},
);
},
width: _cardWidth,
height: _cardHeight,
padding: 0.0,
),
),
MouseRegion(
cursor: _canMoveRight
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: GestureDetector(
onTap: _canMoveRight ? _moveRight : null,
child: Icon(
Icons.chevron_right_outlined,
size: 32.0,
color: _canMoveRight ? null : theme.disabledColor,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16.0,
children: _activityItems.mapIndexed((i, activity) {
final selected = activity == _currentActivity;
return InkWell(
borderRadius: BorderRadius.circular(12.0),
onTap: () => _setActivityByIndex(i),
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: selected ? 0.0 : 0.5,
sigmaY: selected ? 0.0 : 0.5,
),
child: Opacity(
opacity: selected ? 1.0 : 0.5,
child: ClipOval(
child: SizedBox.fromSize(
size: const Size.fromRadius(12.0),
child: activity.imageURL != null
? CachedNetworkImage(
imageUrl: activity.imageURL!,
errorWidget: (context, url, error) {
return CircleAvatar(
backgroundColor:
theme.colorScheme.secondary,
radius: 12.0,
);
},
progressIndicatorBuilder:
(context, url, progress) {
return CircularProgressIndicator(
value: progress.progress,
);
},
)
: CircleAvatar(
backgroundColor:
theme.colorScheme.secondary,
radius: 12.0,
),
),
),
),
),
);
}).toList(),
),
ElevatedButton(
onPressed: () => _isColumnMode
? context.go("/rooms")
: context.go("/rooms/homepage"),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primaryContainer,
),
child: _loading
? const LinearProgressIndicator()
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).exploreMore),
],
),
),
],
),
),
);
}
}

View file

@ -38,7 +38,7 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
final List<ActivityPlanModel> _activityItems = [];
final ScrollController _scrollController = ScrollController();
final double cardHeight = 235.0;
final double cardHeight = 250.0;
double get cardPadding => _isColumnMode ? 8.0 : 0.0;
double get cardWidth => _isColumnMode ? 225.0 : 150.0;

View file

@ -8,12 +8,18 @@ class MaxWidthBody extends StatelessWidget {
final double maxWidth;
final bool withScrolling;
final EdgeInsets? innerPadding;
// #Pangea
final bool showBorder;
// Pangea#
const MaxWidthBody({
required this.child,
this.maxWidth = 600,
this.withScrolling = true,
this.innerPadding,
// #Pangea
this.showBorder = true,
// Pangea#
super.key,
});
@override
@ -38,7 +44,12 @@ class MaxWidthBody extends StatelessWidget {
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
side: BorderSide(
color: theme.dividerColor,
// #Pangea
// color: theme.dividerColor,
color: showBorder
? theme.dividerColor
: Colors.transparent,
// Pangea#
),
),
clipBehavior: Clip.hardEdge,