resolve merge conflicts

This commit is contained in:
ggurdin 2026-01-15 16:04:16 -05:00
commit 2b21329266
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
10 changed files with 145 additions and 32 deletions

View file

@ -85,6 +85,7 @@ class AnalyticsDataService {
void dispose() {
_syncController?.dispose();
updateDispatcher.dispose();
updateService.dispose();
_closeDatabase();
}

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart';
import 'package:fluffychat/pangea/analytics_data/analytics_update_dispatcher.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
@ -52,6 +53,13 @@ class AnalyticsSyncController {
if (constructEvents.isEmpty) return;
await dataService.updateServerAnalytics(constructEvents);
// Server updates do not usually need to update the UI, since usually they are only
// transfering local data to the server. However, if a user if using multiple devices,
// we do need to update the UI when new data comes from the server.
dataService.updateDispatcher.sendConstructAnalyticsUpdate(
AnalyticsUpdate([]),
);
}
Future<void> waitForSync(String analyticsRoomId) async {

View file

@ -23,9 +23,19 @@ class AnalyticsUpdateService {
final AnalyticsDataService dataService;
AnalyticsUpdateService(this.dataService);
AnalyticsUpdateService(this.dataService) {
_periodicTimer = Timer.periodic(
const Duration(minutes: 5),
(_) => sendLocalAnalyticsToAnalyticsRoom(),
);
}
Completer<void>? _updateCompleter;
Timer? _periodicTimer;
void dispose() {
_periodicTimer?.cancel();
}
LanguageModel? get _l2 => MatrixState.pangeaController.userController.userL2;
@ -50,8 +60,9 @@ class AnalyticsUpdateService {
Future<void> addAnalytics(
String? targetID,
List<OneConstructUse> newConstructs,
) async {
List<OneConstructUse> newConstructs, {
bool forceUpdate = false,
}) async {
await dataService.updateDispatcher.sendConstructAnalyticsUpdate(
AnalyticsUpdate(
newConstructs,
@ -63,7 +74,9 @@ class AnalyticsUpdateService {
final lastUpdated = await dataService.getLastUpdatedAnalytics();
final difference = DateTime.now().difference(lastUpdated ?? DateTime.now());
if (localConstructCount > _maxMessagesCached || difference.inMinutes > 10) {
if (forceUpdate ||
localConstructCount > _maxMessagesCached ||
difference.inMinutes > 10) {
sendLocalAnalyticsToAnalyticsRoom();
}
}

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:diacritic/diacritic.dart';
import 'package:go_router/go_router.dart';
import 'package:fluffychat/l10n/l10n.dart';
@ -106,7 +107,11 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
vocab = data.values.toList();
vocab!.sort(
(a, b) => a.lemma.toLowerCase().compareTo(b.lemma.toLowerCase()),
(a, b) {
final normalizedA = removeDiacritics(a.lemma).toLowerCase();
final normalizedB = removeDiacritics(b.lemma).toLowerCase();
return normalizedA.compareTo(normalizedB);
},
);
} finally {
if (mounted) setState(() {});

View file

@ -77,7 +77,7 @@ class VocabAnalyticsListTile extends StatelessWidget {
},
),
Container(
alignment: Alignment.topCenter,
alignment: Alignment.center,
padding: const EdgeInsets.only(top: 4),
height: (maxWidth - padding * 2) * 0.4,
child: ShrinkableText(

View file

@ -253,7 +253,11 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
setState(() {});
final bonus = _sessionLoader.value!.state.allBonusUses;
await _analyticsService.updateService.addAnalytics(null, bonus);
await _analyticsService.updateService.addAnalytics(
null,
bonus,
forceUpdate: true,
);
await _saveSession();
}

View file

@ -91,11 +91,17 @@ class AnalyticsPracticeSessionRepo {
return dateA.compareTo(dateB);
});
return constructs
.where((construct) => construct.lemma.isNotEmpty)
.take(AnalyticsPracticeConstants.practiceGroupSize)
.map((construct) => construct.id)
.toList();
final Set<String> seemLemmas = {};
final targets = <ConstructIdentifier>[];
for (final construct in constructs) {
if (seemLemmas.contains(construct.lemma)) continue;
seemLemmas.add(construct.lemma);
targets.add(construct.id);
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
break;
}
}
return targets;
}
static Future<Map<PangeaToken, MorphFeaturesEnum>> _fetchMorphs() async {

View file

@ -13,6 +13,7 @@ import 'package:fluffychat/pangea/course_plans/courses/course_plan_model.dart';
import 'package:fluffychat/pangea/course_plans/courses/course_plan_room_extension.dart';
import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/spaces/client_spaces_extension.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
enum SelectedCourseMode { launch, addToSpace, join }
@ -74,7 +75,9 @@ class SelectedCourseController extends State<SelectedCourse>
case SelectedCourseMode.addToSpace:
return L10n.of(context).addCoursePlan;
case SelectedCourseMode.join:
return L10n.of(context).joinWithClassCode;
return widget.roomChunk?.joinRule == JoinRules.knock.name
? L10n.of(context).knock
: L10n.of(context).join;
}
}
@ -152,16 +155,36 @@ class SelectedCourseController extends State<SelectedCourse>
}
final client = Matrix.of(context).client;
final roomId = await client.joinRoom(
widget.roomChunk!.roomId,
);
final room = client.getRoomById(roomId);
if (room == null || room.membership != Membership.join) {
await client.waitForRoomInSync(roomId, join: true);
final r = client.getRoomById(widget.roomChunk!.roomId);
if (r != null && r.membership == Membership.join) {
if (mounted) {
context.go("/rooms/spaces/${r.id}/details");
}
return;
}
if (client.getRoomById(roomId) == null) {
final knock = widget.roomChunk!.joinRule == JoinRules.knock.name;
final roomId = widget.roomChunk != null && knock
? await client.knockRoom(widget.roomChunk!.roomId)
: await client.joinRoom(widget.roomChunk!.roomId);
Room? room = client.getRoomById(roomId);
if (!knock && room == null) {
await client.waitForRoomInSync(roomId);
room = client.getRoomById(roomId);
}
if (knock && room == null) {
Navigator.of(context).pop();
await showOkAlertDialog(
context: context,
title: L10n.of(context).youHaveKnocked,
message: L10n.of(context).pleaseWaitUntilInvited,
);
return;
}
if (room == null) {
throw Exception("Failed to join room");
}

View file

@ -283,7 +283,8 @@ class SelectedCourseView extends StatelessWidget {
),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(Icons.map_outlined),
Text(

View file

@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/course_plans/courses/get_localized_courses_req
import 'package:fluffychat/pangea/languages/language_model.dart';
import 'package:fluffychat/pangea/spaces/public_course_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PublicCoursesPage extends StatefulWidget {
@ -52,10 +53,10 @@ class PublicCoursesPageState extends State<PublicCoursesPage> {
_loadCourses();
}
void setTargetLanguageFilter(LanguageModel? language, {bool reload = true}) {
void setTargetLanguageFilter(LanguageModel? language) {
if (targetLanguageFilter?.langCodeShort == language?.langCodeShort) return;
setState(() => targetLanguageFilter = language);
if (reload) _loadCourses();
_loadCourses();
}
List<PublicCoursesChunk> get filteredCourses {
@ -67,8 +68,7 @@ class PublicCoursesPageState extends State<PublicCoursesPage> {
r.id == c.room.roomId &&
r.membership == Membership.join,
) &&
coursePlans.containsKey(c.courseId) &&
c.room.joinRule == 'public',
coursePlans.containsKey(c.courseId),
)
.toList();
@ -83,16 +83,20 @@ class PublicCoursesPageState extends State<PublicCoursesPage> {
).toList();
}
// sort by join rule, with knock rooms at the end
filtered.sort((a, b) {
final aKnock = a.room.joinRule == JoinRules.knock.name;
final bKnock = b.room.joinRule == JoinRules.knock.name;
if (aKnock && !bKnock) return 1;
if (!aKnock && bKnock) return -1;
return 0;
});
return filtered;
}
Future<void> _loadCourses() async {
Future<void> _loadPublicSpaces() async {
try {
setState(() {
loading = true;
error = null;
});
final resp = await Matrix.of(context).client.requestPublicCourses(
since: nextBatch,
);
@ -114,6 +118,21 @@ class PublicCoursesPageState extends State<PublicCoursesPage> {
},
);
}
}
Future<void> _loadCourses() async {
setState(() {
loading = true;
error = null;
});
await _loadPublicSpaces();
int timesLoaded = 0;
while (error == null && timesLoaded < 5 && nextBatch != null) {
await _loadPublicSpaces();
timesLoaded++;
}
try {
final resp = await CoursePlansRepo.search(
@ -333,6 +352,39 @@ class PublicCoursesPageState extends State<PublicCoursesPage> {
style: theme.textTheme.bodyMedium,
),
],
const SizedBox(height: 12.0),
HoverBuilder(
builder: (context, hovered) =>
ElevatedButton(
onPressed: () => context.go(
'/${widget.route}/course/public/$courseId',
extra: roomChunk,
),
style: ElevatedButton.styleFrom(
backgroundColor: theme
.colorScheme.primaryContainer
.withAlpha(hovered ? 255 : 200),
foregroundColor: theme
.colorScheme.onPrimaryContainer,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(12.0),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
roomChunk.joinRule ==
JoinRules.knock.name
? L10n.of(context).knock
: L10n.of(context).join,
),
],
),
),
),
],
),
),