1572-feedback-for-activity-planner (#1586)

* feat(activity_planner): major UI and functionality updates

* fix(intl_en): did not merge
This commit is contained in:
wcjord 2025-01-26 11:40:43 -05:00 committed by GitHub
parent a377200f5f
commit 3dedcf5bf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1025 additions and 576 deletions

View file

@ -4768,5 +4768,7 @@
"voiceMessage": "Voice message",
"nan": "Not applicable",
"activityPlannerOverviewInstructionsBody": "Choose a topic, mode, learning objective and generate an activity for the chat!",
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock"
"completeActivitiesToUnlock": "Complete the highlighted word activities to unlock",
"myBookmarkedActivities": "My Bookmarked Activities",
"noBookmarkedActivities": "No bookmarked activities"
}

View file

@ -1,12 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/learning_settings/utils/language_list_util.dar
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/error_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'config/setting_keys.dart';
import 'utils/background_push.dart';
import 'widgets/fluffy_chat_app.dart';

View file

@ -0,0 +1,147 @@
import 'dart:developer';
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.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/activity_plan_response.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'activity_plan_card.dart';
class ActivityListView extends StatefulWidget {
final Room? room;
/// if null, show saved activities
final ActivityPlanRequest? activityPlanRequest;
const ActivityListView({
super.key,
required this.room,
required this.activityPlanRequest,
});
@override
_ActivityListViewState createState() => _ActivityListViewState();
}
class _ActivityListViewState extends State<ActivityListView> {
bool get showBookmarkedActivities => widget.activityPlanRequest == null;
Future<List<ActivityPlanModel>> get savedActivities =>
Future.value(BookmarkedActivitiesRepo.get());
Future<List<ActivityPlanModel>> get activities async =>
showBookmarkedActivities
? await savedActivities
: (await ActivityPlanGenerationRepo.get(widget.activityPlanRequest!))
.activityPlans;
@override
void dispose() {
super.dispose();
}
Future<void> _onEdit(int index, ActivityPlanModel updatedActivity) async {
// in this case we're editing an activity plan that was generated recently
// via the repo and should be updated in the cached response
if (widget.activityPlanRequest != null) {
final activities = await this.activities;
activities[index] = updatedActivity;
ActivityPlanGenerationRepo.set(
widget.activityPlanRequest!,
ActivityPlanResponse(activityPlans: activities),
);
}
setState(() {});
}
Future<void> _onLaunch(int index) => showFutureLoadingDialog(
context: context,
future: () async {
final activity = (await activities)[index];
final eventId = await widget.room?.pangeaSendTextEvent(
activity.markdown,
messageTag: ModelKey.messageTagActivityPlan,
//include full model or should we move to a state event for this?
);
if (eventId == null) {
debugger(when: kDebugMode);
return;
}
await widget.room?.setPinnedEvents([eventId]);
Navigator.of(context).pop();
},
);
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return FutureBuilder<List<ActivityPlanModel>>(
future: activities,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
print(snapshot.error);
print(snapshot.stackTrace);
// debugger(when: kDebugMode);
ErrorHandler.logError(
e: snapshot.error,
s: snapshot.stackTrace,
data: {
'room': widget.room,
'activityPlanRequest': widget.activityPlanRequest,
'snapshot.data': snapshot.data,
'snapshot.error': snapshot.error,
},
);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(l10n.oopsSomethingWentWrong),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {});
},
child: Text(l10n.tryAgain),
),
],
),
);
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
if (showBookmarkedActivities) {
return Center(child: Text(l10n.noBookmarkedActivities));
}
return Center(child: Text(l10n.noDataFound));
} else {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return ActivityPlanCard(
activity: snapshot.data![index],
onLaunch: () => _onLaunch(index),
onEdit: (updatedActivity) => _onEdit(index, updatedActivity),
);
},
);
}
},
);
}
}

View file

@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import '../common/network/requests.dart';
class ActivityModeListRepo {

View file

@ -0,0 +1,305 @@
import 'dart:developer';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
class ActivityPlanCard extends StatefulWidget {
final ActivityPlanModel activity;
final VoidCallback onLaunch;
final ValueChanged<ActivityPlanModel> onEdit;
final double maxWidth;
const ActivityPlanCard({
super.key,
required this.activity,
required this.onLaunch,
required this.onEdit,
this.maxWidth = 400,
});
@override
ActivityPlanCardState createState() => ActivityPlanCardState();
}
class ActivityPlanCardState extends State<ActivityPlanCard> {
bool _isEditing = false;
late ActivityPlanModel _tempActivity;
late TextEditingController _titleController;
late TextEditingController _learningObjectiveController;
late TextEditingController _instructionsController;
final TextEditingController _newVocabController = TextEditingController();
@override
void initState() {
super.initState();
_tempActivity = widget.activity;
_titleController = TextEditingController(text: _tempActivity.title);
_learningObjectiveController =
TextEditingController(text: _tempActivity.learningObjective);
_instructionsController =
TextEditingController(text: _tempActivity.instructions);
}
static const double itemPadding = 8;
@override
void dispose() {
_titleController.dispose();
_learningObjectiveController.dispose();
_instructionsController.dispose();
_newVocabController.dispose();
super.dispose();
}
Future<void> _saveEdits() async {
final updatedActivity = ActivityPlanModel(
req: _tempActivity.req,
title: _titleController.text,
learningObjective: _learningObjectiveController.text,
instructions: _instructionsController.text,
vocab: _tempActivity.vocab,
);
final activityWithBookmarkId = await _addBookmark(updatedActivity);
// need to save in the repo as well
widget.onEdit(activityWithBookmarkId);
setState(() {
_isEditing = false;
});
}
Future<ActivityPlanModel> _addBookmark(ActivityPlanModel activity) =>
BookmarkedActivitiesRepo.save(activity).catchError((e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: stack, data: activity.toJson());
}).whenComplete(() {
setState(() {});
});
Future<void> _removeBookmark() =>
BookmarkedActivitiesRepo.remove(widget.activity.bookmarkId!)
.catchError((e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: stack, data: widget.activity.toJson());
}).whenComplete(() {
setState(() {});
});
void _addVocab() {
setState(() {
_tempActivity.vocab.add(Vocab(lemma: _newVocabController.text, pos: ''));
_newVocabController.clear();
});
}
void _removeVocab(int index) {
setState(() {
_tempActivity.vocab.removeAt(index);
});
}
bool get isBookmarked =>
BookmarkedActivitiesRepo.isBookmarked(widget.activity);
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxWidth),
child: Card(
margin: const EdgeInsets.symmetric(vertical: itemPadding),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.event_note_outlined),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _titleController,
decoration: InputDecoration(
labelText: L10n.of(context).title,
),
maxLines: null,
)
: Text(
widget.activity.title,
style: Theme.of(context).textTheme.bodyLarge,
),
),
if (!_isEditing)
IconButton(
onPressed: isBookmarked
? () => _removeBookmark()
: () => _addBookmark(widget.activity),
icon: Icon(
isBookmarked ? Icons.bookmark : Icons.bookmark_border,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.target,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _learningObjectiveController,
decoration: const InputDecoration(
labelText: 'Learning Objective',
),
maxLines: null,
)
: Text(
widget.activity.learningObjective,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
Row(
children: [
Icon(
Symbols.steps_rounded,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: _isEditing
? TextField(
controller: _instructionsController,
decoration: const InputDecoration(
labelText: 'Instructions',
),
maxLines: null,
)
: Text(
widget.activity.instructions,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
const SizedBox(height: itemPadding),
if (widget.activity.vocab.isNotEmpty) ...[
Row(
children: [
Icon(
Symbols.dictionary,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: itemPadding),
Expanded(
child: Wrap(
spacing: 4.0,
runSpacing: 4.0,
children: List<Widget>.generate(
_tempActivity.vocab.length, (int index) {
return _isEditing
? Chip(
label:
Text(_tempActivity.vocab[index].lemma),
onDeleted: () => _removeVocab(index),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side:
BorderSide(color: Colors.transparent),
),
)
: Chip(
label:
Text(_tempActivity.vocab[index].lemma),
backgroundColor: Colors.transparent,
visualDensity: VisualDensity.compact,
shape: const StadiumBorder(
side:
BorderSide(color: Colors.transparent),
),
);
}).toList(),
),
),
],
),
if (_isEditing)
Padding(
padding: const EdgeInsets.only(top: itemPadding),
child: Row(
children: [
Expanded(
child: TextField(
controller: _newVocabController,
decoration: const InputDecoration(
labelText: 'Add Vocabulary',
),
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _addVocab,
),
],
),
),
],
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
icon: Icon(!_isEditing ? Icons.edit : Icons.save),
onPressed: () => !_isEditing
? setState(() {
_isEditing = true;
})
: _saveEdits(),
isSelected: _isEditing,
),
if (_isEditing)
IconButton(
icon: const Icon(Icons.cancel),
onPressed: () {
setState(() {
_isEditing = false;
});
},
),
],
),
ElevatedButton.icon(
onPressed: !_isEditing ? widget.onLaunch : null,
icon: const Icon(Icons.send),
label: Text(l10n.launchActivityButton),
),
],
),
],
),
),
),
),
);
}
}

View file

@ -1,5 +1,7 @@
import 'dart:convert';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_response.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
@ -9,83 +11,6 @@ import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../common/network/requests.dart';
class ActivityPlanRequest {
final String topic;
final String mode;
final String objective;
final MediaEnum media;
final int cefrLevel;
final String languageOfInstructions;
final String targetLanguage;
final int count;
ActivityPlanRequest({
required this.topic,
required this.mode,
required this.objective,
required this.media,
required this.cefrLevel,
required this.languageOfInstructions,
required this.targetLanguage,
this.count = 3,
});
Map<String, dynamic> toJson() {
return {
'topic': topic,
'mode': mode,
'objective': objective,
'media': media.string,
'cefr_level': cefrLanguageLevel,
'language_of_instructions': languageOfInstructions,
'target_language': targetLanguage,
'count': count,
};
}
String get storageKey =>
'$topic-$mode-$objective-${media.string}-$cefrLevel-$languageOfInstructions-$targetLanguage';
String get cefrLanguageLevel {
switch (cefrLevel) {
case 0:
return 'Pre-A1';
case 1:
return 'A1';
case 2:
return 'A2';
case 3:
return 'B1';
case 4:
return 'B2';
case 5:
return 'C1';
case 6:
return 'C2';
default:
return 'Pre-A1';
}
}
}
class ActivityPlanResponse {
final List<String> activityPlans;
ActivityPlanResponse({required this.activityPlans});
factory ActivityPlanResponse.fromJson(Map<String, dynamic> json) {
return ActivityPlanResponse(
activityPlans: List<String>.from(json['activity_plans']),
);
}
Map<String, dynamic> toJson() {
return {
'activity_plans': activityPlans,
};
}
}
class ActivityPlanGenerationRepo {
static final GetStorage _activityPlanStorage =
GetStorage('activity_plan_storage');

View file

@ -0,0 +1,90 @@
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
class ActivityPlanModel {
final ActivityPlanRequest req;
final String title;
final String learningObjective;
final String instructions;
final List<Vocab> vocab;
String? bookmarkId;
ActivityPlanModel({
required this.req,
required this.title,
required this.learningObjective,
required this.instructions,
required this.vocab,
this.bookmarkId,
});
factory ActivityPlanModel.fromJson(Map<String, dynamic> json) {
return ActivityPlanModel(
req: ActivityPlanRequest.fromJson(json['req']),
title: json['title'],
learningObjective: json['learning_objective'],
instructions: json['instructions'],
vocab: List<Vocab>.from(
json['vocab'].map((vocab) => Vocab.fromJson(vocab)),
),
bookmarkId: json['bookmark_id'],
);
}
Map<String, dynamic> toJson() {
return {
'req': req.toJson(),
'title': title,
'learning_objective': learningObjective,
'instructions': instructions,
'vocab': vocab.map((vocab) => vocab.toJson()).toList(),
'bookmark_id': bookmarkId,
};
}
/// activity content displayed nicely in markdown
/// use target emoji for learning objective
/// use step emoji for instructions
String get markdown {
String markdown =
''' **$title** \n🎯 $learningObjective \n🪜 $instructions \n📖 ''';
// cycle through vocab with index
for (var i = 0; i < vocab.length; i++) {
// if the lemma appears more than once in the vocab list, show the pos
// vocab is a wrapped list of string, separated by commas
final v = vocab[i];
final bool showPos =
vocab.where((vocab) => vocab.lemma == v.lemma).length > 1;
markdown +=
'${v.lemma}${showPos ? ' (${v.pos})' : ''}${i + 1 < vocab.length ? ', ' : ''}';
}
return markdown;
}
bool get isBookmarked {
return bookmarkId != null;
}
}
class Vocab {
final String lemma;
final String pos;
Vocab({
required this.lemma,
required this.pos,
});
factory Vocab.fromJson(Map<String, dynamic> json) {
return Vocab(
lemma: json['lemma'],
pos: json['pos'],
);
}
Map<String, dynamic> toJson() {
return {
'lemma': lemma,
'pos': pos,
};
}
}

View file

@ -1,10 +1,9 @@
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/pages/chat/chat.dart';
class ActivityPlanPageLaunchIconButton extends StatelessWidget {
const ActivityPlanPageLaunchIconButton({
super.key,
@ -15,6 +14,9 @@ class ActivityPlanPageLaunchIconButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (controller.room.isBotDM) {
return const SizedBox();
}
return IconButton(
icon: const Icon(Icons.event_note_outlined),
tooltip: L10n.of(context).activityPlannerTitle,

View file

@ -0,0 +1,97 @@
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
class ActivityPlanRequest {
final String topic;
final String mode;
final String objective;
final MediaEnum media;
final int cefrLevel;
final String languageOfInstructions;
final String targetLanguage;
final int count;
ActivityPlanRequest({
required this.topic,
required this.mode,
required this.objective,
required this.media,
required this.cefrLevel,
required this.languageOfInstructions,
required this.targetLanguage,
this.count = 3,
});
Map<String, dynamic> toJson() {
return {
'topic': topic,
'mode': mode,
'objective': objective,
'media': media.string,
'cefr_level': cefrLanguageLevel,
'language_of_instructions': languageOfInstructions,
'target_language': targetLanguage,
'count': count,
};
}
factory ActivityPlanRequest.fromJson(Map<String, dynamic> json) {
int cefrLevel = 0;
switch (json['cefr_level']) {
case 'Pre-A1':
cefrLevel = 0;
break;
case 'A1':
cefrLevel = 1;
break;
case 'A2':
cefrLevel = 2;
break;
case 'B1':
cefrLevel = 3;
break;
case 'B2':
cefrLevel = 4;
break;
case 'C1':
cefrLevel = 5;
break;
case 'C2':
cefrLevel = 6;
break;
}
return ActivityPlanRequest(
topic: json['topic'],
mode: json['mode'],
objective: json['objective'],
media: MediaEnum.nan.fromString(json['media']),
cefrLevel: cefrLevel,
languageOfInstructions: json['language_of_instructions'],
targetLanguage: json['target_language'],
count: json['count'],
);
}
String get storageKey =>
'$topic-$mode-$objective-${media.string}-$cefrLevel-$languageOfInstructions-$targetLanguage';
String get cefrLanguageLevel {
switch (cefrLevel) {
case 0:
return 'Pre-A1';
case 1:
return 'A1';
case 2:
return 'A2';
case 3:
return 'B1';
case 4:
return 'B2';
case 5:
return 'C1';
case 6:
return 'C2';
default:
return 'Pre-A1';
}
}
}

View file

@ -0,0 +1,21 @@
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
class ActivityPlanResponse {
final List<ActivityPlanModel> activityPlans;
ActivityPlanResponse({required this.activityPlans});
factory ActivityPlanResponse.fromJson(Map<String, dynamic> json) {
return ActivityPlanResponse(
activityPlans: (json['activity_plans'] as List)
.map((e) => ActivityPlanModel.fromJson(e))
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'activity_plans': activityPlans.map((e) => e.toJson()).toList(),
};
}
}

View file

@ -1,93 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ActivityPlanTile extends StatefulWidget {
final String activity;
final VoidCallback onLaunch;
final ValueChanged<String> onEdit;
const ActivityPlanTile({
super.key,
required this.activity,
required this.onLaunch,
required this.onEdit,
});
@override
ActivityPlanTileState createState() => ActivityPlanTileState();
}
class ActivityPlanTileState extends State<ActivityPlanTile> {
late TextEditingController _controller;
bool editMode = false;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.activity);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
return Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (editMode)
TextField(
controller: _controller,
onChanged: widget.onEdit,
maxLines: null,
)
else
Text(
widget.activity,
maxLines: null,
overflow: TextOverflow.visible,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
onPressed: () {
setState(() {
editMode = !editMode;
});
},
child: Text(!editMode ? l10n.edit : l10n.cancel),
),
const SizedBox(width: 8.0),
ElevatedButton(
onPressed: !editMode
? widget.onLaunch
: () {
setState(() {
widget.onEdit(_controller.text);
editMode = !editMode;
});
},
child: Text(
!editMode ? l10n.launchActivityButton : l10n.saveChanges,
),
),
],
),
],
),
),
);
}
}

View file

@ -1,60 +1,26 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_list_view.dart';
import 'package:fluffychat/pangea/activity_planner/activity_mode_list_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_generation_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_tile.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_request.dart';
import 'package:fluffychat/pangea/activity_planner/learning_objective_list_repo.dart';
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:fluffychat/pangea/activity_planner/media_enum.dart';
import 'package:fluffychat/pangea/activity_planner/suggestion_form_field.dart';
import 'package:fluffychat/pangea/activity_planner/topic_list_repo.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/widgets/p_language_dropdown.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
// a page to allow the user to choose settings and then generate a list of activities
// has app bar with a back button to go back to content 1 (disabled if on content 1), and a title of "Activity Planner", and close button to close the activity planner
// content 1 - settings
// content 2 - display of activities generated by the system, allowing edit and selection
// use standard flutter material widgets and theme colors/styles. all copy should be defined in intl_en.arb and used with L10n.of(context).copyKey
// content 1
// should have a maxWidth, pulled from appconfig
// a. topic input with drop-down of suggestions pulled from TopicListRepo. label text of "topic" and placeholder of some random suggestions from repo
// b. mode input with drop-down of suggestions pulled from ModeListRepo. label text of "mode" and placeholder of some random suggestions from repo
// c. objective input with drop-down of suggestions pulled from LearningObjectiveListRepo. label text of "learning objective" and placeholder of some random suggestions from repo.
// e. dropdown for media type with text "media students should share as part of the activity"
// d. dropdown for selecting "language of activity instructions" which is auto-populated with the user's l1 but can be changed with options coming from pangeaController.pLanguageStore.baseOptions
// f. dropdown for selecting "target language" which is auto-populated with the user's l2 but can be changed with options coming from pangeaController.pLanguageStore.targetOptions
// g. selection for language level
// h. button to generate activities
// content 2
// a. app bar with a back button to go back to content 1, and a title of "Activity Planner", and close button to close the activity planner
// b. display of activities generated by the system, arranged in a column. if there is enough horizontal space, the activities should be arranged in a row
// a1. each activity should have a button to "launch activity" which calls a callback. this can be blank for now.
// a2. each activity should have a button to edit the activity. upon edit, the activity should become an input form where the user can freely edit the activity content
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
enum _PageMode {
settings,
activities,
generatedActivities,
savedActivities,
}
class ActivityPlannerPage extends StatefulWidget {
@ -80,18 +46,8 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
String? _selectedTargetLanguage;
int? _selectedCefrLevel;
/// fetch data from repos
List<ActivitySettingResponseSchema> _topicItems = [];
List<ActivitySettingResponseSchema> _modeItems = [];
List<ActivitySettingResponseSchema> _objectiveItems = [];
List<String> activities = [];
/// List of activities generated by the system
List<String> _activities = [];
final _topicSearchController = TextEditingController();
final _objectiveSearchController = TextEditingController();
final List<TextEditingController> _activityControllers = [];
Room? get room => Matrix.of(context).client.getRoomById(widget.roomID);
@override
@ -102,138 +58,69 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
return;
}
_loadDropdownData();
_selectedLanguageOfInstructions =
MatrixState.pangeaController.languageController.userL1?.langCode;
_selectedTargetLanguage =
MatrixState.pangeaController.languageController.userL2?.langCode;
_selectedCefrLevel = 0;
// Initialize controllers for activity editing
for (final activity in _activities) {
_activityControllers.add(TextEditingController(text: activity));
}
}
final _topicController = TextEditingController();
final _objectiveController = TextEditingController();
final _modeController = TextEditingController();
@override
void dispose() {
_topicSearchController.dispose();
_objectiveSearchController.dispose();
disposeAndClearActivityControllers();
_topicController.dispose();
_objectiveController.dispose();
_modeController.dispose();
super.dispose();
}
void disposeAndClearActivityControllers() {
for (final controller in _activityControllers) {
controller.dispose();
}
_activityControllers.clear();
}
ActivitySettingRequestSchema get req => ActivitySettingRequestSchema(
langCode:
MatrixState.pangeaController.languageController.userL2?.langCode ??
LanguageKeys.defaultLanguage,
);
Future<void> _loadDropdownData() async {
final topics = await TopicListRepo.get(req);
final modes = await ActivityModeListRepo.get(req);
final objectives = await LearningObjectiveListRepo.get(req);
setState(() {
_topicItems = topics;
_modeItems = modes;
_objectiveItems = objectives;
});
}
Future<List<ActivitySettingResponseSchema>> get _topicItems =>
TopicListRepo.get(req);
// send the activity as a message to the room
Future<void> onLaunch(int index) => showFutureLoadingDialog(
context: context,
future: () async {
// this shouldn't often error but just in case since it's not necessary for the activity to be sent
List<PangeaToken>? tokens;
try {
tokens = await MatrixState.pangeaController.messageData.getTokens(
repEventId: null,
req: TokensRequestModel(
fullText: _activities[index],
langCode: _selectedLanguageOfInstructions!,
senderL1: _selectedLanguageOfInstructions!,
senderL2: _selectedLanguageOfInstructions!,
),
room: null,
);
} catch (e) {
debugger(when: kDebugMode);
}
Future<List<ActivitySettingResponseSchema>> get _modeItems =>
ActivityModeListRepo.get(req);
final eventId = await room?.pangeaSendTextEvent(
_activities[index],
messageTag: ModelKey.messageTagActivityPlan,
originalSent: PangeaRepresentation(
langCode: _selectedLanguageOfInstructions!,
text: _activities[index],
originalSent: true,
originalWritten: false,
),
tokensSent:
tokens != null ? PangeaMessageTokens(tokens: tokens) : null,
);
if (eventId == null) {
debugger(when: kDebugMode);
return;
}
await room?.setPinnedEvents([eventId]);
Navigator.of(context).pop();
},
);
Future<List<ActivitySettingResponseSchema>> get _objectiveItems =>
LearningObjectiveListRepo.get(req);
Future<void> _generateActivities() async {
if (_formKey.currentState?.validate() ?? false) {
final request = ActivityPlanRequest(
topic: _selectedTopic!,
mode: _selectedMode!,
objective: _selectedObjective!,
media: _selectedMedia,
languageOfInstructions: _selectedLanguageOfInstructions!,
targetLanguage: _selectedTargetLanguage!,
cefrLevel: _selectedCefrLevel!,
);
await showFutureLoadingDialog(
context: context,
future: () async {
final response = await ActivityPlanGenerationRepo.get(request);
setState(() {
_activities = response.activityPlans;
disposeAndClearActivityControllers();
for (final activity in _activities) {
_activityControllers.add(TextEditingController(text: activity));
}
_pageMode = _PageMode.activities;
});
},
);
}
_pageMode = _PageMode.generatedActivities;
setState(() {});
}
bool get _canRandomizeSelections =>
_topicItems.isNotEmpty &&
_objectiveItems.isNotEmpty &&
_modeItems.isNotEmpty;
Future<String> _randomTopic() async {
final topics = await _topicItems;
return (topics..shuffle()).first.name;
}
void _randomizeSelections() {
if (!_canRandomizeSelections) return;
Future<String> _randomObjective() async {
final objectives = await _objectiveItems;
return (objectives..shuffle()).first.name;
}
Future<String> _randomMode() async {
final modes = await _modeItems;
return (modes..shuffle()).first.name;
}
void _randomizeSelections() async {
_selectedTopic = await _randomTopic();
_selectedObjective = await _randomObjective();
_selectedMode = await _randomMode();
setState(() {
_selectedTopic = (_topicItems..shuffle()).first.name;
_selectedObjective = (_objectiveItems..shuffle()).first.name;
_selectedMode = (_modeItems..shuffle()).first.name;
_topicController.text = _selectedTopic!;
_objectiveController.text = _selectedObjective!;
_modeController.text = _selectedMode!;
});
}
@ -259,223 +146,157 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
onPressed: () => setState(() => _pageMode = _PageMode.settings),
icon: const Icon(Icons.arrow_back),
),
title: Text(l10n.activityPlannerTitle),
title: _pageMode == _PageMode.savedActivities
? Text(l10n.myBookmarkedActivities)
: Text(l10n.activityPlannerTitle),
actions: [
IconButton(
onPressed: () =>
setState(() => _pageMode = _PageMode.savedActivities),
icon: const Icon(Icons.bookmarks),
),
],
),
body: _pageMode == _PageMode.settings
? _buildSettingsForm(l10n)
: _buildActivitiesView(l10n),
);
}
Widget _buildSettingsForm(L10n l10n) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.activityPlannerOverview,
),
DropdownButtonFormField2<String>(
hint: Text(l10n.topicPlaceholder),
value: _selectedTopic,
decoration: _selectedTopic != null
? InputDecoration(
labelText: l10n.topicLabel,
)
: null,
isExpanded: true,
validator: (value) => _validateNotNull(value),
dropdownSearchData: DropdownSearchData(
searchController: _topicSearchController,
searchInnerWidget: InnerSearchWidget(
searchController: _topicSearchController,
),
searchInnerWidgetHeight: 60,
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
items: _topicItems
.map(
(e) => DropdownMenuItem(
value: e.name,
child: Text(e.name),
),
)
.toList(),
onChanged: (value) {
_selectedTopic = value;
},
dropdownStyleData: const DropdownStyleData(maxHeight: 400),
),
const SizedBox(height: 24),
DropdownButtonFormField2<String>(
hint: Text(l10n.learningObjectivePlaceholder),
decoration: _selectedObjective != null
? InputDecoration(labelText: l10n.learningObjectiveLabel)
: null,
validator: (value) => _validateNotNull(value),
items: _objectiveItems
.map(
(e) =>
DropdownMenuItem(value: e.name, child: Text(e.name)),
)
.toList(),
onChanged: (val) => _selectedObjective = val,
dropdownStyleData: const DropdownStyleData(maxHeight: 400),
value: _selectedObjective,
dropdownSearchData: DropdownSearchData(
searchController: _objectiveSearchController,
searchInnerWidget: InnerSearchWidget(
searchController: _objectiveSearchController,
),
searchInnerWidgetHeight: 60,
searchMatchFn: (item, searchValue) {
return item.value
.toString()
.toLowerCase()
.contains(searchValue.toLowerCase());
},
),
),
const SizedBox(height: 24),
DropdownButtonFormField2<String>(
decoration: _selectedMode != null
? InputDecoration(labelText: l10n.modeLabel)
: null,
hint: Text(l10n.modePlaceholder),
validator: (value) => _validateNotNull(value),
items: _modeItems
.map(
(e) =>
DropdownMenuItem(value: e.name, child: Text(e.name)),
)
.toList(),
onChanged: (val) => _selectedMode = val,
value: _selectedMode,
),
const SizedBox(height: 24),
DropdownButtonFormField2<MediaEnum>(
decoration: InputDecoration(labelText: l10n.mediaLabel),
items: MediaEnum.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.toDisplayCopyUsingL10n(context)),
),
)
.toList(),
onChanged: (val) => _selectedMedia = val ?? MediaEnum.nan,
value: _selectedMedia,
),
const SizedBox(height: 24),
LanguageLevelDropdown(
initialLevel: 0,
onChanged: (val) => _selectedCefrLevel = val,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages:
MatrixState.pangeaController.pLanguageStore.baseOptions,
onChange: (val) => _selectedTargetLanguage = val.langCode,
initialLanguage:
MatrixState.pangeaController.languageController.userL1,
isL2List: false,
decorationText: L10n.of(context).languageOfInstructionsLabel,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages:
MatrixState.pangeaController.pLanguageStore.targetOptions,
onChange: (val) => _selectedTargetLanguage = val.langCode,
initialLanguage:
MatrixState.pangeaController.languageController.userL2,
decorationText: L10n.of(context).targetLanguageLabel,
isL2List: true,
),
const SizedBox(height: 24),
Row(
children: [
IconButton(
icon: const Icon(Icons.shuffle),
onPressed:
_canRandomizeSelections ? _randomizeSelections : null,
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _generateActivities,
child: Text(l10n.generateActivitiesButton),
body: _pageMode != _PageMode.settings
? ActivityListView(
room: room,
activityPlanRequest: _PageMode.savedActivities == _pageMode
? null
: ActivityPlanRequest(
topic: _selectedTopic!,
mode: _selectedMode!,
objective: _selectedObjective!,
media: _selectedMedia,
languageOfInstructions: _selectedLanguageOfInstructions!,
targetLanguage: _selectedTargetLanguage!,
cefrLevel: _selectedCefrLevel!,
),
)
: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
const InstructionsInlineTooltip(
instructionsEnum:
InstructionsEnum.activityPlannerOverview,
),
Row(
children: [
Expanded(
child: Column(
children: [
SuggestionFormField(
suggestions: _topicItems,
validator: _validateNotNull,
label: l10n.topicLabel,
placeholder: l10n.topicPlaceholder,
onSelected: (val) => _selectedTopic = val,
initialValue: _selectedTopic,
controller: _topicController,
),
const SizedBox(height: 24),
SuggestionFormField(
suggestions: _objectiveItems,
validator: _validateNotNull,
label: l10n.learningObjectiveLabel,
placeholder:
l10n.learningObjectivePlaceholder,
onSelected: (val) => _selectedObjective = val,
initialValue: _selectedObjective,
controller: _objectiveController,
),
const SizedBox(height: 24),
SuggestionFormField(
suggestions: _modeItems,
validator: _validateNotNull,
label: l10n.modeLabel,
placeholder: l10n.modePlaceholder,
onSelected: (val) => _selectedMode = val,
initialValue: _selectedMode,
controller: _modeController,
),
],
),
),
const SizedBox(width: 4),
Column(
children: [
IconButton(
icon: const Icon(Icons.shuffle),
onPressed: _randomizeSelections,
),
],
),
],
),
const SizedBox(height: 24),
DropdownButtonFormField2<MediaEnum>(
decoration: InputDecoration(labelText: l10n.mediaLabel),
items: MediaEnum.values
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(e.toDisplayCopyUsingL10n(context)),
),
)
.toList(),
onChanged: (val) =>
_selectedMedia = val ?? MediaEnum.nan,
value: _selectedMedia,
),
const SizedBox(height: 24),
LanguageLevelDropdown(
initialLevel: 0,
onChanged: (val) => _selectedCefrLevel = val,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages: MatrixState
.pangeaController.pLanguageStore.baseOptions,
onChange: (val) =>
_selectedTargetLanguage = val.langCode,
initialLanguage: MatrixState
.pangeaController.languageController.userL1,
isL2List: false,
decorationText:
L10n.of(context).languageOfInstructionsLabel,
),
const SizedBox(height: 24),
PLanguageDropdown(
languages: MatrixState
.pangeaController.pLanguageStore.targetOptions,
onChange: (val) =>
_selectedTargetLanguage = val.langCode,
initialLanguage: MatrixState
.pangeaController.languageController.userL2,
decorationText: L10n.of(context).targetLanguageLabel,
isL2List: true,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_generateActivities();
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.lightbulb_outline),
const SizedBox(width: 8),
Text(l10n.generateActivitiesButton),
],
),
),
],
),
],
),
),
],
),
),
),
);
}
Widget _buildActivitiesView(L10n l10n) {
return ListView(
padding: const EdgeInsets.all(16),
children: _activities.asMap().entries.map((entry) {
final index = entry.key;
return ActivityPlanTile(
activity: _activities[index],
onLaunch: () => onLaunch(index),
onEdit: (val) {
setState(() {
_activities[index] = val;
});
},
);
}).toList(),
);
}
}
class InnerSearchWidget extends StatelessWidget {
const InnerSearchWidget({
super.key,
required TextEditingController searchController,
}) : _objectiveSearchController = searchController;
final TextEditingController _objectiveSearchController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
controller: _objectiveSearchController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: L10n.of(context).search,
icon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
);
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class ActivityPlannerSettingsSearchWidget extends StatelessWidget {
const ActivityPlannerSettingsSearchWidget({
super.key,
required TextEditingController searchController,
}) : _objectiveSearchController = searchController;
final TextEditingController _objectiveSearchController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 8,
bottom: 4,
right: 8,
left: 8,
),
child: TextFormField(
controller: _objectiveSearchController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 8,
),
hintText: L10n.of(context).search,
icon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
);
}
}

View file

@ -0,0 +1,39 @@
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:get_storage/get_storage.dart';
import 'package:uuid/uuid.dart';
class BookmarkedActivitiesRepo {
static final GetStorage _storage = GetStorage('bookmarked_activities');
static const Uuid _uuid = Uuid();
/// save an activity to the list of bookmarked activities
/// returns the activity with a bookmarkId
static Future<ActivityPlanModel> save(ActivityPlanModel activity) async {
activity.bookmarkId ??= _uuid.v4();
await _storage.write(
activity.bookmarkId!,
activity.toJson(),
);
//now it has a bookmarkId
return activity;
}
static Future<void> remove(String bookmarkId) => _storage.remove(bookmarkId);
static bool isBookmarked(ActivityPlanModel activity) {
return activity.bookmarkId != null &&
_storage.read(activity.bookmarkId!) != null;
}
static List<ActivityPlanModel> get() {
final list = _storage.getValues();
if (list == null) return [];
return (list as Iterable)
.map((json) => ActivityPlanModel.fromJson(json))
.toList();
}
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum MediaEnum {
@ -11,7 +10,7 @@ enum MediaEnum {
extension MediaEnumExtension on MediaEnum {
//fromString
static MediaEnum fromString(String value) {
MediaEnum fromString(String value) {
switch (value) {
case 'images':
return MediaEnum.images;

View file

@ -0,0 +1,57 @@
import 'package:fluffychat/pangea/activity_planner/list_request_schema.dart';
import 'package:flutter/material.dart';
class SuggestionFormField extends StatelessWidget {
final Future<List<ActivitySettingResponseSchema>> suggestions;
final String? Function(String?)? validator;
final String label;
final String placeholder;
final void Function(String) onSelected;
final String? initialValue;
final TextEditingController controller;
const SuggestionFormField({
super.key,
required this.suggestions,
required this.placeholder,
this.validator,
required this.label,
required this.onSelected,
required this.initialValue,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Autocomplete<String>(
initialValue:
initialValue != null ? TextEditingValue(text: initialValue!) : null,
optionsBuilder: (TextEditingValue textEditingValue) async {
return (await suggestions)
.where((ActivitySettingResponseSchema option) {
return option.name
.toLowerCase()
.contains(textEditingValue.text.toLowerCase());
}).map((ActivitySettingResponseSchema e) => e.name);
},
onSelected: onSelected,
fieldViewBuilder: (
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
) {
textEditingController.value = controller.value;
return TextFormField(
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
labelText: label,
hintText: placeholder,
),
validator: validator,
);
},
);
}
}

View file

@ -2,11 +2,6 @@ import 'dart:async';
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
@ -30,6 +25,10 @@ import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.
import 'package:fluffychat/pangea/user/controllers/permissions_controller.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../config/app_config.dart';
import '../../choreographer/controllers/it_feedback_controller.dart';
import '../utils/firebase_analytics.dart';
@ -186,7 +185,7 @@ class PangeaController {
final List<Room> botDMs = [];
for (final room in matrixState.client.rooms) {
if (await room.isBotDM) {
if (room.isBotDM) {
botDMs.add(room);
}
}

View file

@ -4,18 +4,8 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:html_unescape/html_unescape.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/markdown.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics/models/constructs_event.dart';
import 'package:fluffychat/pangea/analytics/models/constructs_model.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
@ -29,6 +19,15 @@ import 'package:fluffychat/pangea/spaces/constants/space_constants.dart';
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:html_unescape/html_unescape.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/markdown.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../choreographer/models/choreo_record.dart';
import '../events/constants/pangea_event_types.dart';
import '../events/models/representation_content_model.dart';
@ -195,7 +194,7 @@ extension PangeaRoom on Room {
Future<bool> get botIsInRoom async => await _botIsInRoom;
Future<bool> get isBotDM async => await _isBotDM;
bool get isBotDM => _isBotDM;
// bool get isLocked => _isLocked;

View file

@ -41,7 +41,7 @@ extension RoomInformationRoomExtension on Room {
);
}
Future<bool> get _isBotDM async => botOptions?.mode == BotMode.directChat;
bool get _isBotDM => botOptions?.mode == BotMode.directChat;
bool _isAnalyticsRoomOfUser(String userId) =>
isAnalyticsRoom && isMadeByUser(userId);