feat: add activity suggestions to new chat page (#2235)
This commit is contained in:
parent
0faeb6f6ae
commit
ee11c5596b
7 changed files with 328 additions and 4 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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#
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class ActivitySuggestionCard extends StatelessWidget {
|
|||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue