allow users to block constructs from their analytics

This commit is contained in:
ggurdin 2025-12-04 14:07:38 -05:00
parent 4d58b66bf1
commit 1e6cabc5d8
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
10 changed files with 103 additions and 129 deletions

View file

@ -469,7 +469,7 @@ class ChatController extends State<ChatPageWithRoom>
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
OverlayUtil.showPointsGained(update.targetID!, update.points, context);
}
}

View file

@ -135,10 +135,10 @@ class ConstructListModel {
level = calculateLevelWithXp(totalXP);
}
void deleteLemma(String lemma, int offset) {
_uses.removeWhere((use) => use.lemma == lemma);
void deleteConstruct(ConstructIdentifier constructId, int offset) {
_uses.removeWhere((use) => use.identifier == constructId);
_constructMap.removeWhere(
(key, value) => value.lemma == lemma,
(key, value) => value.id == constructId,
);
updateConstructs([], offset);
}

View file

@ -85,6 +85,12 @@ class GetAnalyticsController extends BaseController {
.putAnalytics.analyticsUpdateStream.stream
.listen(_onAnalyticsUpdate);
_pangeaController.putAnalytics.savedActivitiesNotifier
.addListener(_onActivityAnalyticsUpdate);
_pangeaController.putAnalytics.blockedConstructsNotifier
.addListener(_onBlockedConstructsUpdate);
// When a newly-joined space comes through in a sync
// update, add the analytics rooms to the space
_joinSpaceSubscription ??= _client.onSync.stream
@ -103,10 +109,10 @@ class GetAnalyticsController extends BaseController {
];
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
final blockedLemmas = analyticsRoom?.analyticsSettings?.blockedLemmas;
final blockedLemmas = analyticsRoom?.analyticsSettings?.blockedConstructs;
if (blockedLemmas != null && blockedLemmas.isNotEmpty) {
allUses.removeWhere(
(use) => blockedLemmas.contains(use.identifier.lemma),
(use) => blockedLemmas.contains(use.identifier),
);
}
@ -124,11 +130,7 @@ class GetAnalyticsController extends BaseController {
data: {},
);
} finally {
_updateAnalyticsStream(
type: AnalyticsUpdateType.init,
points: 0,
newConstructs: [],
);
_updateAnalyticsStream(AnalyticsStreamUpdate());
if (!initCompleter.isCompleted) initCompleter.complete();
_initializing = false;
}
@ -143,22 +145,16 @@ class GetAnalyticsController extends BaseController {
_joinSpaceSubscription?.cancel();
_joinSpaceSubscription = null;
initCompleter = Completer<void>();
_pangeaController.putAnalytics.savedActivitiesNotifier
.removeListener(_onActivityAnalyticsUpdate);
_pangeaController.putAnalytics.blockedConstructsNotifier
.removeListener(_onBlockedConstructsUpdate);
_cache.clear();
}
Future<void> _onAnalyticsUpdate(
AnalyticsUpdate analyticsUpdate,
) async {
final validTypes = [AnalyticsUpdateType.local, AnalyticsUpdateType.server];
if (!validTypes.contains(analyticsUpdate.type)) {
_updateAnalyticsStream(
type: analyticsUpdate.type,
points: 0,
newConstructs: [],
);
return;
}
if (analyticsUpdate.isLogout) return;
final oldLevel = constructListModel.level;
@ -173,9 +169,6 @@ class GetAnalyticsController extends BaseController {
)
.toSet();
final prevUnlockedVocab =
constructListModel.unlockedLemmas(ConstructTypeEnum.vocab).toSet();
constructListModel.updateConstructs(analyticsUpdate.newConstructs, offset);
final newUnlockedMorphs = constructListModel
@ -186,11 +179,6 @@ class GetAnalyticsController extends BaseController {
.toSet()
.difference(prevUnlockedMorphs);
final newUnlockedVocab = constructListModel
.unlockedLemmas(ConstructTypeEnum.vocab)
.toSet()
.difference(prevUnlockedVocab);
if (analyticsUpdate.type == AnalyticsUpdateType.server) {
await _getConstructs(forceUpdate: true);
}
@ -206,13 +194,13 @@ class GetAnalyticsController extends BaseController {
_onUnlockMorphLemmas(newUnlockedMorphs);
}
_updateAnalyticsStream(
type: analyticsUpdate.type,
points: analyticsUpdate.newConstructs.fold<int>(
0,
(previousValue, element) => previousValue + element.xp,
AnalyticsStreamUpdate(
points: analyticsUpdate.newConstructs.fold<int>(
0,
(previousValue, element) => previousValue + element.xp,
),
targetID: analyticsUpdate.targetID,
),
targetID: analyticsUpdate.targetID,
newConstructs: [...newUnlockedMorphs, ...newUnlockedVocab],
);
// Update public profile each time that new analytics are added.
// If the level hasn't changed, this will not send an update to the server.
@ -223,20 +211,8 @@ class GetAnalyticsController extends BaseController {
);
}
void _updateAnalyticsStream({
required AnalyticsUpdateType type,
required int points,
required List<ConstructIdentifier> newConstructs,
String? targetID,
}) =>
analyticsStream.add(
AnalyticsStreamUpdate(
type: type,
points: points,
newConstructs: newConstructs,
targetID: targetID,
),
);
void _updateAnalyticsStream(AnalyticsStreamUpdate update) =>
analyticsStream.add(update);
void _onLevelUp(final int lowerLevel, final int upperLevel) {
setState({
@ -267,6 +243,21 @@ class GetAnalyticsController extends BaseController {
setState({'unlocked_constructs': filtered});
}
void _onActivityAnalyticsUpdate() =>
_updateAnalyticsStream(AnalyticsStreamUpdate());
void _onBlockedConstructsUpdate() {
final constructId =
_pangeaController.putAnalytics.blockedConstructsNotifier.value;
if (constructId == null) return;
constructListModel.deleteConstruct(
constructId,
_pangeaController.userController.analyticsProfile?.xpOffset ?? 0,
);
_updateAnalyticsStream(AnalyticsStreamUpdate());
}
/// A local cache of eventIds and construct uses for messages sent since the last update.
/// It's a map of eventIDs to a list of OneConstructUses. Not just a list of OneConstructUses
/// because, with practice activity constructs, we might need to add to the list for a given
@ -484,19 +475,6 @@ class GetAnalyticsController extends BaseController {
return newConstructCount;
}
// Future<GenerateConstructSummaryResult?>
// _generateLevelUpAnalyticsAndSaveToStateEvent(
// final int lowerLevel,
// final int upperLevel,
// ) async {
// // generate level up analytics as a construct summary
// ConstructSummary summary;
// try {
// final int maxXP = constructListModel.calculateXpWithLevel(upperLevel);
// final int minXP = constructListModel.calculateXpWithLevel(lowerLevel);
// int diffXP = maxXP - minXP;
// if (diffXP < 0) diffXP = 0;
ConstructSummary? getConstructSummaryFromStateEvent() {
try {
final Room? analyticsRoom = _client.analyticsRoomLocal(_l2!);
@ -662,15 +640,11 @@ class AnalyticsCacheEntry {
}
class AnalyticsStreamUpdate {
final AnalyticsUpdateType type;
final int points;
final List<ConstructIdentifier> newConstructs;
final String? targetID;
AnalyticsStreamUpdate({
required this.type,
required this.points,
required this.newConstructs,
this.points = 0,
this.targetID,
});
}

View file

@ -6,14 +6,17 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_model.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum AnalyticsUpdateType { server, local, activities, init, delete }
enum AnalyticsUpdateType { server, local }
/// handles the processing of analytics for
/// 1) messages sent by the user and
@ -23,6 +26,10 @@ class PutAnalyticsController {
StreamController<AnalyticsUpdate> analyticsUpdateStream =
StreamController.broadcast();
ValueNotifier<List<String>> savedActivitiesNotifier = ValueNotifier([]);
ValueNotifier<ConstructIdentifier?> blockedConstructsNotifier =
ValueNotifier(null);
StreamSubscription? _languageStream;
Timer? _updateTimer;
@ -50,9 +57,16 @@ class PutAnalyticsController {
}
void initialize() {
final Room? analyticsRoom = _client.analyticsRoomLocal(
_pangeaController.languageController.userL2!,
);
if (analyticsRoom != null) {
savedActivitiesNotifier.value = analyticsRoom.activityRoomIds;
}
_languageStream ??= _pangeaController.userController.languageStream.stream
.listen(_onUpdateLanguages);
_refreshAnalyticsIfOutdated();
}
@ -281,22 +295,31 @@ class PutAnalyticsController {
);
if (analyticsRoom == null) return;
await analyticsRoom.addActivityRoomId(roomId);
analyticsUpdateStream.add(
AnalyticsUpdate(
AnalyticsUpdateType.activities,
[],
),
);
savedActivitiesNotifier.value = analyticsRoom.activityRoomIds;
}
Future<void> onBlockLemma() async {
analyticsUpdateStream.add(
AnalyticsUpdate(
AnalyticsUpdateType.delete,
[],
),
Future<void> blockConstruct(ConstructIdentifier constructId) async {
if (_pangeaController.matrixState.client.userID == null) return;
if (_pangeaController.languageController.userL2 == null) return;
final Room? analyticsRoom = await _client.getMyAnalyticsRoom(
_pangeaController.languageController.userL2!,
);
if (analyticsRoom == null) return;
final current = analyticsRoom.analyticsSettings ??
const AnalyticsSettingsModel(blockedConstructs: {});
final blockedConstructs = current.blockedConstructs;
final updated = current.copyWith(
blockedConstructs: {
...blockedConstructs,
constructId,
},
);
await analyticsRoom.setAnalyticsSettings(updated);
blockedConstructsNotifier.value = constructId;
}
}

View file

@ -8,10 +8,8 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
import 'package:fluffychat/pangea/analytics_page/analytics_page_constants.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart';
import 'package:fluffychat/pangea/analytics_summary/learning_progress_indicators.dart';
import 'package:fluffychat/pangea/analytics_summary/level_dialog_content.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
@ -43,7 +41,8 @@ class AnalyticsPage extends StatelessWidget {
if (resp != OkCancelResult.ok) return;
final res = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.blockLemma(construct!.lemma),
future: () =>
MatrixState.pangeaController.putAnalytics.blockConstruct(construct!),
);
if (!res.isError) {
@ -70,12 +69,9 @@ class AnalyticsPage extends StatelessWidget {
)
: null,
body: SafeArea(
child: StreamBuilder(
stream: MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.where(
(u) => u.type == AnalyticsUpdateType.init,
),
child: FutureBuilder(
future:
MatrixState.pangeaController.getAnalytics.initCompleter.future,
builder: (context, snapshot) {
return Padding(
padding: const EdgeInsetsGeometry.all(16.0),

View file

@ -1,9 +1,7 @@
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_settings/analytics_settings_model.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/widgets/matrix.dart';
extension AnalyticsSettingsRoomExtension on Room {
AnalyticsSettingsModel? get analyticsSettings {
@ -23,24 +21,3 @@ extension AnalyticsSettingsRoomExtension on Room {
);
}
}
extension AnalyticsSettingsClientExtension on Client {
Future<void> blockLemma(String lemma) async {
final l2 = MatrixState.pangeaController.languageController.userL2!;
final analyticsRoom = await getMyAnalyticsRoom(l2);
if (analyticsRoom == null) {
throw Exception("Could not get or create analytics room");
}
final current = analyticsRoom.analyticsSettings;
final blockedLemmas = current?.blockedLemmas ?? {};
final updated = current?.copyWith(
blockedLemmas: {
...blockedLemmas,
lemma,
},
);
await analyticsRoom.setAnalyticsSettings(updated!);
}
}

View file

@ -1,34 +1,36 @@
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
class AnalyticsSettingsModel {
final Set<String> blockedLemmas;
final Set<ConstructIdentifier> blockedConstructs;
const AnalyticsSettingsModel({
required this.blockedLemmas,
required this.blockedConstructs,
});
AnalyticsSettingsModel copyWith({
Set<String>? blockedLemmas,
Set<ConstructIdentifier>? blockedConstructs,
}) {
return AnalyticsSettingsModel(
blockedLemmas: blockedLemmas ?? this.blockedLemmas,
blockedConstructs: blockedConstructs ?? this.blockedConstructs,
);
}
factory AnalyticsSettingsModel.fromJson(Map<String, dynamic> json) {
final blockedLemmas = <String>{};
if (json['blocked_lemmas'] != null) {
final lemmas = json['blocked_lemmas'] as List<dynamic>;
final blockedConstructs = <ConstructIdentifier>{};
if (json['blocked_constructs'] != null) {
final lemmas = json['blocked_constructs'] as List<dynamic>;
for (final lemma in lemmas) {
blockedLemmas.add(lemma as String);
blockedConstructs.add(ConstructIdentifier.fromJson(lemma));
}
}
return AnalyticsSettingsModel(
blockedLemmas: blockedLemmas,
blockedConstructs: blockedConstructs,
);
}
Map<String, dynamic> toJson() {
return {
'blocked_lemmas': blockedLemmas.toList(),
'blocked_constructs': blockedConstructs.map((c) => c.toJson()).toList(),
};
}
}

View file

@ -51,11 +51,11 @@ class LearningProgressIndicatorsState
// if getAnalytics has already finished initializing,
// the data is loaded and should be displayed.
if (MatrixState.pangeaController.getAnalytics.initCompleter.isCompleted) {
updateData(null);
updateData();
}
_analyticsSubscription = MatrixState
.pangeaController.getAnalytics.analyticsStream.stream
.listen(updateData);
.listen((_) => updateData());
// rebuild when target language changes
_languageSubscription = MatrixState
@ -71,10 +71,11 @@ class LearningProgressIndicatorsState
_analyticsSubscription = null;
_languageSubscription?.cancel();
_languageSubscription = null;
super.dispose();
}
void updateData(AnalyticsStreamUpdate? _) {
void updateData() {
if (_loading) _loading = false;
if (mounted) setState(() {});
}

View file

@ -289,6 +289,7 @@ class OverlayUtil {
static void showPointsGained(
String targetId,
int points,
BuildContext context,
) {
showOverlay(
@ -297,7 +298,7 @@ class OverlayUtil {
targetAnchor: Alignment.bottomCenter,
context: context,
child: PointsGainedAnimation(
points: 2,
points: points,
targetID: targetId,
),
transformTargetId: targetId,

View file

@ -81,7 +81,7 @@ class LemmaHighlightEmojiRowState extends State<LemmaHighlightEmojiRow>
void _onAnalyticsUpdate(AnalyticsStreamUpdate update) {
if (update.targetID != null) {
OverlayUtil.showPointsGained(update.targetID!, context);
OverlayUtil.showPointsGained(update.targetID!, update.points, context);
}
}