3517 non local storage of bookmarked activities (#3761)

This commit is contained in:
ggurdin 2025-08-18 11:43:00 -04:00 committed by GitHub
parent bae5765a97
commit 7c03c70105
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 624 additions and 465 deletions

View file

@ -8,8 +8,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.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/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/utils/firebase_analytics.dart';
@ -42,10 +40,6 @@ class NewGroupController extends State<NewGroup> {
// #Pangea
// bool publicGroup = false;
// bool groupCanBeFound = false;
ActivityPlanModel? selectedActivity;
Uint8List? selectedActivityImage;
String? selectedActivityImageFilename;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final FocusNode focusNode = FocusNode();
@ -72,22 +66,6 @@ class NewGroupController extends State<NewGroup> {
// void setPublicGroup(bool b) =>
// setState(() => publicGroup = groupCanBeFound = b);
void setSelectedActivity(
ActivityPlanModel? activity,
Uint8List? image,
String? imageFilename,
) {
setState(() {
selectedActivity = activity;
selectedActivityImage = image;
selectedActivityImageFilename = imageFilename;
if (avatar == null) {
avatar = image;
avatarUrl = null;
}
});
}
@override
void initState() {
super.initState();
@ -189,21 +167,6 @@ class NewGroupController extends State<NewGroup> {
);
}
}
if (selectedActivity != null) {
try {
await room.sendActivityPlan(
selectedActivity!,
avatar: selectedActivityImage,
filename: selectedActivityImageFilename,
);
} catch (err) {
ErrorHandler.logError(
e: "Failed to send activity plan",
data: {"roomId": roomId, "error": err},
);
}
}
context.go('/rooms/$roomId/invite');
// Pangea#
}

View file

@ -1,6 +1,3 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
@ -9,19 +6,16 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_suggestion_dialog.dart';
import 'package:fluffychat/pangea/chat_settings/widgets/language_level_dropdown.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivityPlanCard extends StatefulWidget {
class ActivityPlanCard extends StatelessWidget {
final VoidCallback regenerate;
final ActivityPlannerBuilderState controller;
@ -31,73 +25,31 @@ class ActivityPlanCard extends StatefulWidget {
required this.controller,
});
@override
ActivityPlanCardState createState() => ActivityPlanCardState();
}
class ActivityPlanCardState extends State<ActivityPlanCard> {
static const double itemPadding = 12;
Future<ActivityPlanModel> _addBookmark(ActivityPlanModel activity) async {
try {
return BookmarkedActivitiesRepo.save(activity);
} catch (e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: stack, data: activity.toJson());
return activity; // Return the original activity in case of error
} finally {
if (mounted) {
setState(() {});
}
}
}
Future<void> _removeBookmark() async {
try {
BookmarkedActivitiesRepo.remove(
widget.controller.updatedActivity.bookmarkId,
);
} catch (e, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: e,
s: stack,
data: widget.controller.updatedActivity.toJson(),
);
} finally {
if (mounted) {
setState(() {});
}
}
}
Future<void> _onLaunch() async {
Future<void> _onLaunch(BuildContext context) async {
final resp = await showFutureLoadingDialog(
context: context,
future: () async {
if (!widget.controller.room.isSpace) {
if (!controller.room.isSpace) {
throw Exception(
"Cannot launch activity in a non-space room",
);
}
final ids = await widget.controller.launchToSpace();
final ids = await controller.launchToSpace();
ids.length == 1
? context.go("/rooms/${ids.first}")
: context.go("/rooms?spaceId=${widget.controller.room.id}");
: context.go("/rooms?spaceId=${controller.room.id}");
Navigator.of(context).pop();
},
);
if (!resp.isError) {
context.go("/rooms?spaceId=${widget.controller.room.id}");
context.go("/rooms?spaceId=${controller.room.id}");
}
}
bool get _isBookmarked => BookmarkedActivitiesRepo.isBookmarked(
widget.controller.updatedActivity,
);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -108,7 +60,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
child: Card(
margin: const EdgeInsets.symmetric(vertical: itemPadding),
child: Form(
key: widget.controller.formKey,
key: controller.formKey,
child: Column(
children: [
AnimatedSize(
@ -124,11 +76,10 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
clipBehavior: Clip.hardEdge,
alignment: Alignment.center,
child: widget.controller.isLaunching
child: controller.isLaunching
? Avatar(
mxContent: widget.controller.room.avatar,
name: widget.controller.room
.getLocalizedDisplayname(
mxContent: controller.room.avatar,
name: controller.room.getLocalizedDisplayname(
MatrixLocals(
L10n.of(context),
),
@ -136,15 +87,14 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
borderRadius: BorderRadius.circular(12.0),
size: 200.0,
)
: widget.controller.imageURL != null ||
widget.controller.avatar != null
: controller.imageURL != null ||
controller.avatar != null
? ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: widget.controller.avatar == null
child: controller.avatar == null
? CachedNetworkImage(
fit: BoxFit.cover,
imageUrl:
widget.controller.imageURL!,
imageUrl: controller.imageURL!,
placeholder: (context, url) {
return const Center(
child:
@ -158,7 +108,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
},
)
: Image.memory(
widget.controller.avatar!,
controller.avatar!,
fit: BoxFit.cover,
),
)
@ -166,10 +116,10 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
padding: EdgeInsets.all(28.0),
),
),
if (widget.controller.isEditing)
if (controller.isEditing)
InkWell(
borderRadius: BorderRadius.circular(90),
onTap: widget.controller.selectAvatar,
onTap: controller.selectAvatar,
child: CircleAvatar(
backgroundColor:
Theme.of(context).colorScheme.secondary,
@ -188,15 +138,14 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: widget.controller.isLaunching
children: controller.isLaunching
? [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Avatar(
mxContent: widget.controller.room.avatar,
name: widget.controller.room
.getLocalizedDisplayname(
mxContent: controller.room.avatar,
name: controller.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
),
size: 24.0,
@ -205,8 +154,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
const SizedBox(width: itemPadding),
Expanded(
child: Text(
widget.controller.room
.getLocalizedDisplayname(
controller.room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
),
style:
@ -219,29 +167,26 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
widget.controller.updatedActivity.imageURL !=
null
controller.updatedActivity.imageURL != null
? ClipRRect(
borderRadius:
BorderRadius.circular(4.0),
child: widget.controller.updatedActivity
.imageURL!
child: controller
.updatedActivity.imageURL!
.startsWith("mxc")
? MxcImage(
uri: Uri.parse(
widget
.controller
.updatedActivity
controller.updatedActivity
.imageURL!,
),
width: 24.0,
height: 24.0,
cacheKey: widget.controller
cacheKey: controller
.updatedActivity.bookmarkId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: widget.controller
imageUrl: controller
.updatedActivity.imageURL!,
fit: BoxFit.cover,
width: 24.0,
@ -269,7 +214,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
const SizedBox(width: itemPadding),
Expanded(
child: Text(
widget.controller.updatedActivity.title,
controller.updatedActivity.title,
style:
Theme.of(context).textTheme.bodyLarge,
),
@ -286,7 +231,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
child: Text(
L10n.of(context)
.maximumActivityParticipants(
widget.controller.updatedActivity.req
controller.updatedActivity.req
.numberOfParticipants,
),
style:
@ -315,9 +260,8 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
.bodyLarge,
),
NumberCounter(
count: widget.controller.numActivities,
update:
widget.controller.setNumActivities,
count: controller.numActivities,
update: controller.setNumActivities,
min: 1,
max: 5,
),
@ -337,7 +281,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
horizontal: 12.0,
),
),
onPressed: _onLaunch,
onPressed: () => _onLaunch(context),
child: Row(
children: [
const Icon(Icons.send_outlined),
@ -358,10 +302,10 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
const Icon(Icons.event_note_outlined),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
child: controller.isEditing
? TextField(
controller:
widget.controller.titleController,
controller.titleController,
decoration: InputDecoration(
labelText:
L10n.of(context).activityTitle,
@ -369,22 +313,18 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
maxLines: null,
)
: Text(
widget
.controller.updatedActivity.title,
controller.updatedActivity.title,
style: Theme.of(context)
.textTheme
.bodyLarge,
),
),
if (!widget.controller.isEditing)
if (!controller.isEditing)
IconButton(
onPressed: _isBookmarked
? () => _removeBookmark()
: () => _addBookmark(
widget.controller.updatedActivity,
),
onPressed:
controller.toggleBookmarkedActivity,
icon: Icon(
_isBookmarked
controller.isBookmarked
? Icons.save
: Icons.save_outlined,
),
@ -401,9 +341,9 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
child: controller.isEditing
? TextField(
controller: widget.controller
controller: controller
.learningObjectivesController,
decoration: InputDecoration(
labelText:
@ -412,7 +352,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
maxLines: null,
)
: Text(
widget.controller.updatedActivity
controller.updatedActivity
.learningObjective,
style: Theme.of(context)
.textTheme
@ -431,18 +371,18 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
child: controller.isEditing
? TextField(
controller: widget.controller
.instructionsController,
controller:
controller.instructionsController,
decoration: InputDecoration(
labelText: l10n.instructions,
),
maxLines: null,
)
: Text(
widget.controller.updatedActivity
.instructions,
controller
.updatedActivity.instructions,
style: Theme.of(context)
.textTheme
.bodyMedium,
@ -460,16 +400,16 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
const SizedBox(width: itemPadding),
Expanded(
child: widget.controller.isEditing
child: controller.isEditing
? LanguageLevelDropdown(
initialLevel:
widget.controller.languageLevel,
onChanged: widget
.controller.setLanguageLevel,
controller.languageLevel,
onChanged:
controller.setLanguageLevel,
)
: Text(
widget.controller.updatedActivity.req
.cefrLevel
controller
.updatedActivity.req.cefrLevel
.title(context),
style: Theme.of(context)
.textTheme
@ -479,7 +419,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
],
),
const SizedBox(height: itemPadding),
if (widget.controller.vocab.isNotEmpty) ...[
if (controller.vocab.isNotEmpty) ...[
Row(
children: [
Icon(
@ -493,16 +433,13 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
spacing: 4.0,
runSpacing: 4.0,
children: List<Widget>.generate(
widget.controller.vocab.length,
(int index) {
return widget.controller.isEditing
controller.vocab.length, (int index) {
return controller.isEditing
? Chip(
label: Text(
widget.controller.vocab[index]
.lemma,
controller.vocab[index].lemma,
),
onDeleted: () => widget
.controller
onDeleted: () => controller
.removeVocab(index),
backgroundColor:
Colors.transparent,
@ -516,8 +453,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
)
: Chip(
label: Text(
widget.controller.vocab[index]
.lemma,
controller.vocab[index].lemma,
),
backgroundColor:
Colors.transparent,
@ -535,7 +471,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
],
),
],
if (widget.controller.isEditing) ...[
if (controller.isEditing) ...[
const SizedBox(height: itemPadding),
Padding(
padding:
@ -544,19 +480,18 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
children: [
Expanded(
child: TextField(
controller:
widget.controller.vocabController,
controller: controller.vocabController,
decoration: InputDecoration(
labelText: l10n.addVocabulary,
),
onSubmitted: (value) {
widget.controller.addVocab();
controller.addVocab();
},
),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: widget.controller.addVocab,
onPressed: controller.addVocab,
),
],
),
@ -576,7 +511,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
horizontal: 12.0,
),
),
onPressed: widget.controller.saveEdits,
onPressed: controller.saveEdits,
child: Row(
children: [
const Icon(Icons.save),
@ -601,7 +536,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
horizontal: 12.0,
),
),
onPressed: widget.controller.clearEdits,
onPressed: controller.clearEdits,
child: Row(
children: [
const Icon(Icons.cancel),
@ -636,8 +571,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
horizontal: 12.0,
),
),
onPressed:
widget.controller.startEditing,
onPressed: controller.startEditing,
child: Row(
children: [
const Icon(Icons.edit),
@ -662,7 +596,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
horizontal: 12.0,
),
),
onPressed: widget.regenerate,
onPressed: regenerate,
child: Row(
children: [
const Icon(
@ -694,7 +628,7 @@ class ActivityPlanCardState extends State<ActivityPlanCard> {
),
),
onPressed: () {
widget.controller.setLaunchState(
controller.setLaunchState(
ActivityLaunchState.launching,
);
},

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide Visibility;
@ -8,13 +10,14 @@ import 'package:matrix/matrix.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/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_repo.dart';
import 'package:fluffychat/pangea/chat/constants/default_power_level.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/extensions/join_rule_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/enums/language_level_type_enum.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/utils/client_download_content_extension.dart';
import 'package:fluffychat/utils/file_selector.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -64,6 +67,8 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final StreamController stateStream = StreamController.broadcast();
@override
void initState() {
super.initState();
@ -77,9 +82,17 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
instructionsController.dispose();
vocabController.dispose();
participantsController.dispose();
stateStream.close();
super.dispose();
}
void update() {
if (mounted) setState(() {});
if (!stateStream.isClosed) {
stateStream.add(null);
}
}
Room get room => widget.room;
bool get isEditing => launchState == ActivityLaunchState.editing;
@ -129,7 +142,8 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
if (widget.initialActivity.imageURL != null) {
await _setAvatarByURL(widget.initialActivity.imageURL!);
}
if (mounted) setState(() {});
update();
}
Future<void> overrideActivity(ActivityPlanModel override) async {
@ -148,18 +162,21 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
if (override.imageURL != null) {
await _setAvatarByURL(override.imageURL!);
}
if (mounted) setState(() {});
update();
}
void startEditing() => setLaunchState(ActivityLaunchState.editing);
void startEditing() {
setLaunchState(ActivityLaunchState.editing);
}
void setLaunchState(ActivityLaunchState state) {
if (state == ActivityLaunchState.launching) {
BookmarkedActivitiesRepo.save(updatedActivity);
_addBookmarkedActivity();
}
launchState = state;
if (mounted) setState(() {});
update();
}
void addVocab() {
@ -171,37 +188,35 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
),
);
vocabController.clear();
if (mounted) setState(() {});
update();
}
void removeVocab(int index) {
vocab.removeAt(index);
if (mounted) setState(() {});
update();
}
void setLanguageLevel(LanguageLevelTypeEnum level) {
languageLevel = level;
if (mounted) setState(() {});
update();
}
void selectAvatar() async {
Future<void> selectAvatar() async {
final photo = await selectFiles(
context,
type: FileSelectorType.images,
allowMultiple: false,
);
final bytes = await photo.singleOrNull?.readAsBytes();
if (mounted) {
setState(() {
avatar = bytes;
imageURL = null;
filename = photo.singleOrNull?.name;
});
}
avatar = bytes;
imageURL = null;
filename = photo.singleOrNull?.name;
update();
}
void setNumActivities(int count) {
if (mounted) setState(() => numActivities = count);
numActivities = count;
update();
}
Future<void> _setAvatarByURL(String url) async {
@ -240,10 +255,8 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
avatar!,
filename: filename,
);
if (!mounted) return;
setState(() {
imageURL = url.toString();
});
imageURL = url.toString();
update();
}
Future<void> saveEdits() async {
@ -251,9 +264,8 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
await updateImageURL();
setLaunchState(ActivityLaunchState.base);
await BookmarkedActivitiesRepo.remove(widget.initialActivity.bookmarkId);
await BookmarkedActivitiesRepo.save(updatedActivity);
if (mounted) setState(() {});
await _updateBookmarkedActivity();
update();
}
Future<void> clearEdits() async {
@ -261,6 +273,49 @@ class ActivityPlannerBuilderState extends State<ActivityPlannerBuilder> {
setLaunchState(ActivityLaunchState.base);
}
UserController get _userController =>
MatrixState.pangeaController.userController;
bool get isBookmarked =>
_userController.isBookmarked(updatedActivity.bookmarkId);
Future<void> toggleBookmarkedActivity() async {
isBookmarked
? await _removeBookmarkedActivity()
: await _addBookmarkedActivity();
update();
}
Future<void> _addBookmarkedActivity() async {
await _userController.addBookmarkedActivity(
activityId: updatedActivity.bookmarkId,
);
await ActivityPlanRepo.set(updatedActivity);
}
Future<void> _updateBookmarkedActivity() async {
// save updates locally, in case choreo results in error
await ActivityPlanRepo.set(updatedActivity);
// prevent an error or delay from the choreo endpoint bubbling up
// in the UI, since the changes are still stored locally
ActivityPlanRepo.update(
updatedActivity,
).then((resp) {
_userController.updateBookmarkedActivity(
activityId: widget.initialActivity.bookmarkId,
newActivityId: resp.bookmarkId,
);
});
}
Future<void> _removeBookmarkedActivity() async {
await _userController.removeBookmarkedActivity(
activityId: updatedActivity.bookmarkId,
);
await ActivityPlanRepo.remove(updatedActivity.bookmarkId);
}
Future<List<String>> launchToSpace() async {
final List<String> activityRoomIDs = [];
try {

View file

@ -43,10 +43,7 @@ class ActivityPlannerPageState extends State<ActivityPlannerPage> {
switch (pageMode) {
case PageMode.savedActivities:
if (room != null) {
body = BookmarkedActivitiesList(
room: room!,
controller: this,
);
body = BookmarkedActivitiesList(room: room!);
}
break;
case PageMode.featuredActivities:

View file

@ -1,57 +0,0 @@
// ignore_for_file: depend_on_referenced_packages
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
class BookmarkedActivitiesRepo {
static final GetStorage _bookStorage = GetStorage('bookmarked_activities');
/// save an activity to the list of bookmarked activities
/// returns the activity with a bookmarkId
static Future<ActivityPlanModel> save(
ActivityPlanModel activity,
) async {
await _bookStorage.write(
activity.bookmarkId,
activity.toJson(),
);
//now it has a bookmarkId
return activity;
}
static Future<void> remove(String bookmarkId) =>
_bookStorage.remove(bookmarkId);
static bool isBookmarked(ActivityPlanModel activity) {
return _bookStorage.read(activity.bookmarkId) != null;
}
static List<ActivityPlanModel> get() {
final List<String> keys = List<String>.from(_bookStorage.getKeys());
if (keys.isEmpty) return [];
final List<ActivityPlanModel> activities = [];
for (final key in keys) {
final json = _bookStorage.read(key);
if (json == null) continue;
ActivityPlanModel? activity;
try {
activity = ActivityPlanModel.fromJson(json);
} catch (e) {
_bookStorage.remove(key);
continue;
}
if (key != activity.bookmarkId) {
_bookStorage.remove(key);
_bookStorage.write(activity.bookmarkId, activity.toJson());
}
activities.add(activity);
}
return activities;
}
}

View file

@ -6,20 +6,17 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_page.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_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/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
class BookmarkedActivitiesList extends StatefulWidget {
final Room room;
final ActivityPlannerPageState controller;
const BookmarkedActivitiesList({
super.key,
required this.room,
required this.controller,
});
@override
@ -28,17 +25,51 @@ class BookmarkedActivitiesList extends StatefulWidget {
}
class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
bool _loading = true;
@override
void initState() {
super.initState();
_loadBookmarkedActivities();
}
List<ActivityPlanModel> get _bookmarkedActivities =>
BookmarkedActivitiesRepo.get();
_userController.getBookmarkedActivitiesSync();
bool get _isColumnMode => FluffyThemes.isColumnMode(context);
double get cardHeight => _isColumnMode ? 325.0 : 250.0;
double get cardWidth => _isColumnMode ? 225.0 : 150.0;
UserController get _userController =>
MatrixState.pangeaController.userController;
Future<void> _loadBookmarkedActivities() async {
try {
setState(() => _loading = true);
await _userController.getBookmarkedActivities();
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
data: {
'roomId': widget.room.id,
},
);
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
if (_loading) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
}
if (_bookmarkedActivities.isEmpty) {
return Center(
child: Container(
@ -60,16 +91,16 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
runSpacing: 16.0,
spacing: 4.0,
children: _bookmarkedActivities.map((activity) {
return ActivitySuggestionCard(
activity: activity,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ActivityPlannerBuilder(
initialActivity: activity,
room: widget.room,
builder: (controller) {
return ActivityPlannerBuilder(
initialActivity: activity,
room: widget.room,
builder: (controller) {
return ActivitySuggestionCard(
controller: controller,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
controller: controller,
buttonText: l10n.launchActivityButton,
@ -77,11 +108,10 @@ class BookmarkedActivitiesListState extends State<BookmarkedActivitiesList> {
},
);
},
width: cardWidth,
height: cardHeight,
);
},
width: cardWidth,
height: cardHeight,
onChange: () => setState(() {}),
);
}).toList(),
),

View file

@ -6,7 +6,6 @@ import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart';
import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart';
@ -25,8 +24,6 @@ extension ActivityRoomExtension on Room {
Uint8List? avatar,
String? filename,
}) async {
BookmarkedActivitiesRepo.save(activity);
if (canChangeStateEvent(PangeaEventTypes.activityPlan)) {
await client.setRoomStateWithKey(
id,

View file

@ -0,0 +1,79 @@
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/pangea/common/network/urls.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ActivityPlanRepo {
static final GetStorage _activityPlanStorage =
GetStorage('activity_plan_by_id_storage');
static ActivityPlanModel? getCached(String id) {
final cachedJson = _activityPlanStorage.read(id);
if (cachedJson == null) return null;
try {
return ActivityPlanModel.fromJson(cachedJson);
} catch (e) {
_removeCached(id);
return null;
}
}
static Future<void> _setCached(ActivityPlanModel response) =>
_activityPlanStorage.write(response.bookmarkId, response.toJson());
static Future<void> _removeCached(String id) =>
_activityPlanStorage.remove(id);
static Future<void> set(ActivityPlanModel activity) => _setCached(activity);
static Future<ActivityPlanModel> get(String id) async {
final cached = getCached(id);
if (cached != null) return cached;
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.get(
url: "${PApiUrls.activityPlan}/$id",
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = ActivityPlanModel.fromJson(decodedBody["plan"]);
_setCached(response);
return response;
}
static Future<ActivityPlanModel> update(
ActivityPlanModel update,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.patch(
url: "${PApiUrls.activityPlan}/${update.bookmarkId}",
body: update.toJson(),
);
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
final response = ActivityPlanModel.fromJson(decodedBody["plan"]);
_removeCached(update.bookmarkId);
_setCached(response);
return response;
}
static Future<void> remove(String id) => _removeCached(id);
}

View file

@ -1,43 +1,32 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_planner/bookmarked_activities_repo.dart';
import 'package:fluffychat/pangea/activity_planner/activity_planner_builder.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ActivitySuggestionCard extends StatelessWidget {
final ActivityPlanModel activity;
final Uint8List? image;
final VoidCallback? onPressed;
final ActivityPlannerBuilderState controller;
final VoidCallback onPressed;
final double width;
final double height;
final bool selected;
final VoidCallback onChange;
const ActivitySuggestionCard({
super.key,
required this.activity,
required this.controller,
required this.onPressed,
required this.width,
required this.height,
required this.onChange,
this.selected = false,
this.image,
});
ActivityPlanModel get activity => controller.updatedActivity;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isBookmarked = BookmarkedActivitiesRepo.isBookmarked(activity);
return PressableButton(
depressed: selected || onPressed == null,
onPressed: onPressed,
borderRadius: BorderRadius.circular(24.0),
color: theme.brightness == Brightness.dark
@ -46,11 +35,6 @@ class ActivitySuggestionCard extends StatelessWidget {
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,
@ -76,27 +60,25 @@ class ActivitySuggestionCard extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24.0),
child: image != null
? Image.memory(image!, fit: BoxFit.cover)
: activity.imageURL != null
? activity.imageURL!.startsWith("mxc")
? MxcImage(
uri: Uri.parse(activity.imageURL!),
width: width,
height: width,
cacheKey: activity.bookmarkId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: activity.imageURL!,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const SizedBox(),
fit: BoxFit.cover,
)
: null,
child: activity.imageURL != null
? activity.imageURL!.startsWith("mxc")
? MxcImage(
uri: Uri.parse(activity.imageURL!),
width: width,
height: width,
cacheKey: activity.bookmarkId,
fit: BoxFit.cover,
)
: CachedNetworkImage(
imageUrl: activity.imageURL!,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) =>
const SizedBox(),
fit: BoxFit.cover,
)
: null,
),
),
Expanded(
@ -180,19 +162,10 @@ class ActivitySuggestionCard extends StatelessWidget {
right: 4.0,
child: IconButton(
icon: Icon(
isBookmarked ? Icons.save : Icons.save_outlined,
controller.isBookmarked ? Icons.save : Icons.save_outlined,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
onPressed: onPressed != null
? () async {
await (isBookmarked
? BookmarkedActivitiesRepo.remove(
activity.bookmarkId,
)
: BookmarkedActivitiesRepo.save(activity));
onChange();
}
: null,
onPressed: controller.toggleBookmarkedActivity,
style: IconButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@ -40,6 +42,22 @@ class ActivitySuggestionDialogState extends State<ActivitySuggestionDialog> {
? 400.0
: MediaQuery.of(context).size.width;
StreamSubscription? _stateSubscription;
@override
void initState() {
super.initState();
_stateSubscription = widget.controller.stateStream.stream.listen((state) {
if (mounted) setState(() {});
});
}
@override
void dispose() {
_stateSubscription?.cancel();
super.dispose();
}
Future<void> launchActivity() async {
try {
if (!widget.controller.room.isSpace) {

View file

@ -185,16 +185,16 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
})
: _activityItems
.mapIndexed((index, activity) {
return ActivitySuggestionCard(
activity: activity,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ActivityPlannerBuilder(
initialActivity: activity,
room: widget.room,
builder: (controller) {
return ActivityPlannerBuilder(
initialActivity: activity,
room: widget.room,
builder: (controller) {
return ActivitySuggestionCard(
controller: controller,
onPressed: () {
showDialog(
context: context,
builder: (context) {
return ActivitySuggestionDialog(
controller: controller,
buttonText: L10n.of(context).saveAndLaunch,
@ -204,13 +204,10 @@ class ActivitySuggestionsAreaState extends State<ActivitySuggestionsArea> {
},
);
},
width: cardWidth,
height: cardHeight,
);
},
width: cardWidth,
height: cardHeight,
onChange: () {
if (mounted) setState(() {});
},
);
})
.cast<Widget>()

View file

@ -93,7 +93,7 @@ class GetAnalyticsController extends BaseController {
await _getConstructs();
final offset =
_pangeaController.userController.publicProfile?.xpOffset ?? 0;
_pangeaController.userController.analyticsProfile?.xpOffset ?? 0;
constructListModel.updateConstructs(
[
...(_getConstructsLocal() ?? []),
@ -149,7 +149,7 @@ class GetAnalyticsController extends BaseController {
final oldLevel = constructListModel.level;
final offset =
_pangeaController.userController.publicProfile?.xpOffset ?? 0;
_pangeaController.userController.analyticsProfile?.xpOffset ?? 0;
final prevUnlockedMorphs = constructListModel
.unlockedLemmas(
@ -203,7 +203,7 @@ class GetAnalyticsController extends BaseController {
// If the level hasn't changed, this will not send an update to the server.
// Do this on all updates (not just on level updates) to account for cases
// of target language updates being missed (https://github.com/pangeachat/client/issues/2006)
_pangeaController.userController.updatePublicProfile(
_pangeaController.userController.updateAnalyticsProfile(
level: constructListModel.level,
);
}
@ -237,7 +237,7 @@ class GetAnalyticsController extends BaseController {
await _pangeaController.userController.addXPOffset(offset);
constructListModel.updateConstructs(
[],
_pangeaController.userController.publicProfile!.xpOffset!,
_pangeaController.userController.analyticsProfile!.xpOffset!,
);
}

View file

@ -23,7 +23,7 @@ class LevelDisplayName extends StatelessWidget {
),
child: FutureBuilder(
future: MatrixState.pangeaController.userController
.getPublicProfile(userId),
.getPublicAnalyticsProfile(userId),
builder: (context, snapshot) {
return Row(
mainAxisSize: MainAxisSize.min,

View file

@ -151,7 +151,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
);
_pangeaController.resetAnalytics().then((_) {
final level = _pangeaController.getAnalytics.constructListModel.level;
_pangeaController.userController.updatePublicProfile(level: level);
_pangeaController.userController.updateAnalyticsProfile(level: level);
});
}

View file

@ -738,7 +738,7 @@ class RoomParticipantsSection extends StatelessWidget {
Membership.leave => null,
};
final publicProfile = participantsLoader.getPublicProfile(
final publicProfile = participantsLoader.getAnalyticsProfile(
user.id,
);

View file

@ -181,4 +181,6 @@ class ModelKey {
static const String autoIGC = "auto_igc";
static const String roomIds = "room_ids";
static const String bookmarkedActivities = "bookmarked_activities";
}

View file

@ -111,6 +111,7 @@ class PangeaController {
static final List<String> _storageKeys = [
'mode_list_storage',
'activity_plan_storage',
'activity_plan_by_id_storage',
'bookmarked_activities',
'objective_list_storage',
'topic_list_storage',

View file

@ -66,6 +66,29 @@ class Requests {
return response;
}
Future<http.Response> patch({
required String url,
required Map<dynamic, dynamic> body,
}) async {
body[ModelKey.cefrLevel] = MatrixState
.pangeaController.userController.profile.userSettings.cefrLevel.string;
dynamic encoded;
encoded = jsonEncode(body);
debugPrint(baseUrl! + url);
final http.Response response = await http.patch(
_uriBuilder(url),
body: encoded,
headers: _headers,
);
handleError(response, body: body);
return response;
}
Future<http.Response> get({required String url, String objectId = ""}) async {
final http.Response response =
await http.get(_uriBuilder(url + objectId), headers: _headers);

View file

@ -58,13 +58,16 @@ class PApiUrls {
"${PApiUrls.choreoEndpoint}/lemma_definition/edit";
static String morphDictionary = "${PApiUrls.choreoEndpoint}/morph_meaning";
static String activityPlan = "${PApiUrls.choreoEndpoint}/activity_plan";
static String activityPlanGeneration =
"${PApiUrls.choreoEndpoint}/activity_plan";
"${PApiUrls.choreoEndpoint}/activity_plan/generate";
static String activityPlanSearch =
"${PApiUrls.choreoEndpoint}/activity_plan/search";
static String activityModeList = "${PApiUrls.choreoEndpoint}/modes";
static String objectiveList = "${PApiUrls.choreoEndpoint}/objectives";
static String topicList = "${PApiUrls.choreoEndpoint}/topics";
static String activityPlanSearch =
"${PApiUrls.choreoEndpoint}/activity_plan/search";
static String activitySummary = "${PApiUrls.choreoEndpoint}/activity_summary";
static String morphFeaturesAndTags = "${PApiUrls.choreoEndpoint}/morphs";

View file

@ -46,6 +46,7 @@ class PangeaEventTypes {
/// Profile information related to a user's analytics
static const profileAnalytics = "pangea.analytics_profile";
static const profileActivities = "pangea.activities_profile";
static const activityRoomIds = "pangea.activity_room_ids";
}

View file

@ -225,7 +225,7 @@ class UserSettingsState extends State<UserSettingsPage> {
},
waitForDataInSync: true,
),
_pangeaController.userController.updatePublicProfile(
_pangeaController.userController.updateAnalyticsProfile(
targetLanguage: selectedTargetLanguage,
baseLanguage: _systemLanguage,
level: 1,

View file

@ -19,7 +19,7 @@ import 'package:fluffychat/pangea/space_analytics/space_analytics_download_enum.
import 'package:fluffychat/pangea/space_analytics/space_analytics_inactive_dialog.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics_request_dialog.dart';
import 'package:fluffychat/pangea/space_analytics/space_analytics_view.dart';
import 'package:fluffychat/pangea/user/models/profile_model.dart';
import 'package:fluffychat/pangea/user/models/analytics_profile_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -119,7 +119,7 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
Map<User, AnalyticsDownload> downloads = {};
DateTime? _lastUpdated;
final Map<User, PublicProfileModel> _profiles = {};
final Map<User, AnalyticsProfileModel> _profiles = {};
final Map<LanguageModel, List<User>> _langsToUsers = {};
Room? get room => Matrix.of(context).client.getRoomById(widget.roomId);
@ -233,7 +233,7 @@ class SpaceAnalyticsState extends State<SpaceAnalytics> {
Future<void> _loadProfiles() async {
final futures = _availableUsers.map((u) async {
final resp = await MatrixState.pangeaController.userController
.getPublicProfile(u.id);
.getPublicAnalyticsProfile(u.id);
_profiles[u] = resp;
if (resp.languageAnalytics == null) return;

View file

@ -5,7 +5,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/user/models/profile_model.dart';
import 'package:fluffychat/pangea/user/models/analytics_profile_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
class LoadParticipantsUtil extends StatefulWidget {
@ -26,7 +26,7 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
bool loading = true;
String? error;
final Map<String, PublicProfileModel> _levelsCache = {};
final Map<String, AnalyticsProfileModel> _levelsCache = {};
List<User> get participants => widget.space.getParticipants();
@ -92,8 +92,8 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
return -1;
}
final PublicProfileModel? aProfile = _levelsCache[a.id];
final PublicProfileModel? bProfile = _levelsCache[b.id];
final AnalyticsProfileModel? aProfile = _levelsCache[a.id];
final AnalyticsProfileModel? bProfile = _levelsCache[b.id];
return (bProfile?.level ?? 0).compareTo(aProfile?.level ?? 0);
});
@ -106,12 +106,12 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
if (_levelsCache[user.id] == null && user.membership == Membership.join) {
_levelsCache[user.id] = await MatrixState
.pangeaController.userController
.getPublicProfile(user.id);
.getPublicAnalyticsProfile(user.id);
}
}
}
PublicProfileModel? getPublicProfile(String userId) {
AnalyticsProfileModel? getAnalyticsProfile(String userId) {
return _levelsCache[userId];
}

View file

@ -71,7 +71,8 @@ class LeaderboardParticipantListState
itemCount: participants.length,
itemBuilder: (context, i) {
final user = participants[i];
final publicProfile = participantsLoader.getPublicProfile(
final publicProfile =
participantsLoader.getAnalyticsProfile(
user.id,
);

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:get_storage/get_storage.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'package:matrix/matrix.dart' as matrix;
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
@ -15,7 +17,8 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
import 'package:fluffychat/pangea/user/models/profile_model.dart';
import 'package:fluffychat/pangea/user/models/activities_profile_model.dart';
import 'package:fluffychat/pangea/user/models/analytics_profile_model.dart';
import '../models/user_model.dart';
class LanguageUpdate {
@ -54,7 +57,8 @@ class UserController {
/// to be read in from client's account data each time it is accessed.
Profile? _cachedProfile;
PublicProfileModel? publicProfile;
AnalyticsProfileModel? analyticsProfile;
ActivitiesProfileModel? activitiesProfile;
/// Listens for account updates and updates the cached profile
StreamSubscription? _profileListener;
@ -146,6 +150,7 @@ class UserController {
_initializing = true;
try {
await GetStorage.init('activity_plan_by_id_storage');
await _initialize();
_addProfileListener();
_addAnalyticsRoomIdsToPublicProfile();
@ -184,21 +189,25 @@ class UserController {
if (client.userID == null) return;
try {
final resp = await client.getUserProfile(client.userID!);
publicProfile = PublicProfileModel.fromJson(resp.additionalProperties);
analyticsProfile =
AnalyticsProfileModel.fromJson(resp.additionalProperties);
activitiesProfile =
ActivitiesProfileModel.fromJson(resp.additionalProperties);
} catch (e) {
// getting a 404 error for some users without pre-existing profile
// still want to set other properties, so catch this error
publicProfile = PublicProfileModel();
analyticsProfile = AnalyticsProfileModel();
activitiesProfile = ActivitiesProfileModel.empty;
}
// Do not await. This function pulls level from analytics,
// so it waits for analytics to finish initializing. Analytics waits for user controller to
// finish initializing, so this would cause a deadlock.
if (publicProfile!.isEmpty) {
if (analyticsProfile!.isEmpty) {
_pangeaController.getAnalytics.initCompleter.future
.timeout(const Duration(seconds: 10))
.then((_) {
updatePublicProfile(
updateAnalyticsProfile(
level: _pangeaController.getAnalytics.constructListModel.level,
);
}).catchError((e, s) {
@ -206,7 +215,7 @@ class UserController {
e: e,
s: s,
data: {
"publicProfile": publicProfile?.toJson(),
"publicProfile": analyticsProfile?.toJson(),
"userId": client.userID,
},
level:
@ -231,85 +240,6 @@ class UserController {
await initialize();
}
Future<void> updatePublicProfile({
required int level,
LanguageModel? baseLanguage,
LanguageModel? targetLanguage,
}) async {
targetLanguage ??= _pangeaController.languageController.userL2;
baseLanguage ??= _pangeaController.languageController.userL1;
if (targetLanguage == null || publicProfile == null) return;
final analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(targetLanguage);
if (publicProfile!.targetLanguage == targetLanguage &&
publicProfile!.baseLanguage == baseLanguage &&
publicProfile!.languageAnalytics?[targetLanguage]?.level == level &&
publicProfile!.analyticsRoomIdByLanguage(targetLanguage) ==
analyticsRoom?.id) {
return;
}
publicProfile!.baseLanguage = baseLanguage;
publicProfile!.targetLanguage = targetLanguage;
publicProfile!.setLanguageInfo(
targetLanguage,
level,
analyticsRoom?.id,
);
await _savePublicProfile();
}
Future<void> _addAnalyticsRoomIdsToPublicProfile() async {
if (publicProfile?.languageAnalytics == null) return;
final analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
if (analyticsRooms.isEmpty) return;
for (final analyticsRoom in analyticsRooms) {
final lang = analyticsRoom.madeForLang?.split("-").first;
if (lang == null || publicProfile?.languageAnalytics == null) continue;
final langKey = publicProfile!.languageAnalytics!.keys.firstWhereOrNull(
(l) => l.langCodeShort == lang,
);
if (langKey == null) continue;
if (publicProfile!.languageAnalytics![langKey]!.analyticsRoomId ==
analyticsRoom.id) {
continue;
}
publicProfile!.setLanguageInfo(
langKey,
publicProfile!.languageAnalytics![langKey]!.level,
analyticsRoom.id,
);
}
await _savePublicProfile();
}
Future<void> addXPOffset(int offset) async {
final targetLanguage = _pangeaController.languageController.userL2;
if (targetLanguage == null || publicProfile == null) return;
publicProfile!.addXPOffset(
targetLanguage,
offset,
_pangeaController.matrixState.client
.analyticsRoomLocal(targetLanguage)
?.id,
);
await _savePublicProfile();
}
Future<void> _savePublicProfile() async => client.setUserProfile(
client.userID!,
PangeaEventTypes.profileAnalytics,
publicProfile!.toJson(),
);
/// Returns a boolean value indicating whether a new JWT (JSON Web Token) is needed.
bool needNewJWT(String token) => Jwt.isExpired(token);
@ -421,14 +351,171 @@ class UserController {
return email?.address;
}
Future<PublicProfileModel> getPublicProfile(String userId) async {
Future<void> _savePublicProfileUpdate(
String type,
Map<String, dynamic> content,
) async =>
client.setUserProfile(
client.userID!,
type,
content,
);
Future<void> updateAnalyticsProfile({
required int level,
LanguageModel? baseLanguage,
LanguageModel? targetLanguage,
}) async {
targetLanguage ??= _pangeaController.languageController.userL2;
baseLanguage ??= _pangeaController.languageController.userL1;
if (targetLanguage == null || analyticsProfile == null) return;
final analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(targetLanguage);
if (analyticsProfile!.targetLanguage == targetLanguage &&
analyticsProfile!.baseLanguage == baseLanguage &&
analyticsProfile!.languageAnalytics?[targetLanguage]?.level == level &&
analyticsProfile!.analyticsRoomIdByLanguage(targetLanguage) ==
analyticsRoom?.id) {
return;
}
analyticsProfile!.baseLanguage = baseLanguage;
analyticsProfile!.targetLanguage = targetLanguage;
analyticsProfile!.setLanguageInfo(
targetLanguage,
level,
analyticsRoom?.id,
);
await _savePublicProfileUpdate(
PangeaEventTypes.profileAnalytics,
analyticsProfile!.toJson(),
);
}
Future<void> _addAnalyticsRoomIdsToPublicProfile() async {
if (analyticsProfile?.languageAnalytics == null) return;
final analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
if (analyticsRooms.isEmpty) return;
for (final analyticsRoom in analyticsRooms) {
final lang = analyticsRoom.madeForLang?.split("-").first;
if (lang == null || analyticsProfile?.languageAnalytics == null) continue;
final langKey =
analyticsProfile!.languageAnalytics!.keys.firstWhereOrNull(
(l) => l.langCodeShort == lang,
);
if (langKey == null) continue;
if (analyticsProfile!.languageAnalytics![langKey]!.analyticsRoomId ==
analyticsRoom.id) {
continue;
}
analyticsProfile!.setLanguageInfo(
langKey,
analyticsProfile!.languageAnalytics![langKey]!.level,
analyticsRoom.id,
);
}
await _savePublicProfileUpdate(
PangeaEventTypes.profileAnalytics,
analyticsProfile!.toJson(),
);
}
Future<void> addXPOffset(int offset) async {
final targetLanguage = _pangeaController.languageController.userL2;
if (targetLanguage == null || analyticsProfile == null) return;
analyticsProfile!.addXPOffset(
targetLanguage,
offset,
_pangeaController.matrixState.client
.analyticsRoomLocal(targetLanguage)
?.id,
);
await _savePublicProfileUpdate(
PangeaEventTypes.profileAnalytics,
analyticsProfile!.toJson(),
);
}
Future<void> addBookmarkedActivity({
required String activityId,
}) async {
if (activitiesProfile == null) {
throw Exception("Activities profile is not initialized");
}
activitiesProfile!.addBookmark(activityId);
await _savePublicProfileUpdate(
PangeaEventTypes.profileActivities,
activitiesProfile!.toJson(),
);
}
Future<List<ActivityPlanModel>> getBookmarkedActivities() async {
if (activitiesProfile == null) {
throw Exception("Activities profile is not initialized");
}
return activitiesProfile!.getBookmarkedActivities();
}
List<ActivityPlanModel> getBookmarkedActivitiesSync() {
if (activitiesProfile == null) {
throw Exception("Activities profile is not initialized");
}
return activitiesProfile!.getBookmarkedActivitiesSync();
}
Future<void> updateBookmarkedActivity({
required String activityId,
required String newActivityId,
}) async {
if (activitiesProfile == null) {
throw Exception("Activities profile is not initialized");
}
activitiesProfile!.removeBookmark(activityId);
activitiesProfile!.addBookmark(newActivityId);
await _savePublicProfileUpdate(
PangeaEventTypes.profileActivities,
activitiesProfile!.toJson(),
);
}
Future<void> removeBookmarkedActivity({
required String activityId,
}) async {
if (activitiesProfile == null) {
throw Exception("Activities profile is not initialized");
}
activitiesProfile!.removeBookmark(activityId);
await _savePublicProfileUpdate(
PangeaEventTypes.profileActivities,
activitiesProfile!.toJson(),
);
}
bool isBookmarked(String id) => activitiesProfile?.isBookmarked(id) ?? false;
Future<AnalyticsProfileModel> getPublicAnalyticsProfile(
String userId,
) async {
try {
if (userId == BotName.byEnvironment) {
return PublicProfileModel();
return AnalyticsProfileModel();
}
final resp = await client.getUserProfile(userId);
return PublicProfileModel.fromJson(resp.additionalProperties);
return AnalyticsProfileModel.fromJson(resp.additionalProperties);
} catch (e, s) {
ErrorHandler.logError(
e: e,
@ -437,7 +524,7 @@ class UserController {
userId: userId,
},
);
return PublicProfileModel();
return AnalyticsProfileModel();
}
}
}

View file

@ -0,0 +1,55 @@
import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart';
import 'package:fluffychat/pangea/activity_suggestions/activity_plan_repo.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
class ActivitiesProfileModel {
final List<String> _bookmarkedActivities;
ActivitiesProfileModel({
required List<String> bookmarkedActivities,
}) : _bookmarkedActivities = bookmarkedActivities;
static ActivitiesProfileModel get empty => ActivitiesProfileModel(
bookmarkedActivities: [],
);
bool isBookmarked(String id) => _bookmarkedActivities.contains(id);
void addBookmark(String activityId) {
if (!_bookmarkedActivities.contains(activityId)) {
_bookmarkedActivities.add(activityId);
}
}
void removeBookmark(String activityId) {
_bookmarkedActivities.remove(activityId);
}
Future<List<ActivityPlanModel>> getBookmarkedActivities() => Future.wait(
_bookmarkedActivities.map((id) => ActivityPlanRepo.get(id)).toList(),
);
List<ActivityPlanModel> getBookmarkedActivitiesSync() => _bookmarkedActivities
.map((id) => ActivityPlanRepo.getCached(id))
.whereType<ActivityPlanModel>()
.toList();
static ActivitiesProfileModel fromJson(Map<String, dynamic> json) {
if (!json.containsKey(PangeaEventTypes.profileActivities)) {
return ActivitiesProfileModel.empty;
}
final profileJson = json[PangeaEventTypes.profileActivities];
return ActivitiesProfileModel(
bookmarkedActivities:
List<String>.from(profileJson[ModelKey.bookmarkedActivities] ?? []),
);
}
Map<String, dynamic> toJson() {
return {
ModelKey.bookmarkedActivities: _bookmarkedActivities,
};
}
}

View file

@ -3,20 +3,20 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart';
class PublicProfileModel {
class AnalyticsProfileModel {
LanguageModel? baseLanguage;
LanguageModel? targetLanguage;
Map<LanguageModel, LanguageAnalyticsProfileEntry>? languageAnalytics;
PublicProfileModel({
AnalyticsProfileModel({
this.baseLanguage,
this.targetLanguage,
this.languageAnalytics,
});
factory PublicProfileModel.fromJson(Map<String, dynamic> json) {
factory AnalyticsProfileModel.fromJson(Map<String, dynamic> json) {
if (!json.containsKey(PangeaEventTypes.profileAnalytics)) {
return PublicProfileModel();
return AnalyticsProfileModel();
}
final profileJson = json[PangeaEventTypes.profileAnalytics];
@ -47,7 +47,7 @@ class PublicProfileModel {
}
}
final profile = PublicProfileModel(
final profile = AnalyticsProfileModel(
baseLanguage: baseLanguage,
targetLanguage: targetLanguage,
languageAnalytics: languageAnalytics,