added learning summary to chat list, removed references to summary analytics

This commit is contained in:
ggurdin 2024-07-29 14:52:09 -04:00
parent ba6d395f32
commit c5187c7639
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
35 changed files with 2145 additions and 2587 deletions

View file

@ -4113,5 +4113,8 @@
"createSpace": "Create space",
"createChat": "Create chat",
"error520Title": "Please try again.",
"error520Desc": "Sorry, we could not understand your message..."
"error520Desc": "Sorry, we could not understand your message...",
"wordsUsed": "Words Used",
"errorTypes": "Error Types",
"level": "Level"
}

View file

@ -26,9 +26,7 @@ import 'package:fluffychat/pages/settings_notifications/settings_notifications.d
import 'package:fluffychat/pages/settings_password/settings_password.dart';
import 'package:fluffychat/pages/settings_security/settings_security.dart';
import 'package:fluffychat/pages/settings_style/settings_style.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/guard/p_vguard.dart';
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
import 'package:fluffychat/pangea/pages/find_partner/find_partner.dart';
import 'package:fluffychat/pangea/pages/p_user_age/p_user_age.dart';
import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart';
@ -162,17 +160,17 @@ abstract class AppRoutes {
),
routes: [
// #Pangea
GoRoute(
path: 'mylearning',
pageBuilder: (context, state) => defaultPageBuilder(
context,
state,
const StudentAnalyticsPage(
selectedView: BarChartViewSelection.messages,
),
),
redirect: loggedOutRedirect,
),
// GoRoute(
// path: 'mylearning',
// pageBuilder: (context, state) => defaultPageBuilder(
// context,
// state,
// const StudentAnalyticsPage(
// selectedView: BarChartViewSelection.messages,
// ),
// ),
// redirect: loggedOutRedirect,
// ),
// GoRoute(
// path: 'analytics',
// pageBuilder: (context, state) => defaultPageBuilder(

View file

@ -475,7 +475,7 @@ class ChatController extends State<ChatPageWithRoom>
// Pangea#
if (kIsWeb && !Matrix.of(context).webHasFocus) return;
// #Pangea
} catch (err, s) {
} catch (err) {
return;
}
// Pangea#

View file

@ -3,6 +3,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart';
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_body_text.dart';
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_header_wrapper.dart';
import 'package:fluffychat/pangea/widgets/chat_list/chat_list_item_wrapper.dart';
@ -164,6 +165,9 @@ class ChatListViewBody extends StatelessWidget {
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
),
// #Pangea
const LearningProgressIndicators(),
// Pangea#
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[

View file

@ -1,5 +1,4 @@
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/utils/find_conversation_partner_dialog.dart';
import 'package:fluffychat/pangea/utils/logout.dart';
import 'package:fluffychat/pangea/utils/space_code.dart';
@ -67,19 +66,19 @@ class ClientChooserButton extends StatelessWidget {
// ],
// ),
// ),
PopupMenuItem(
enabled: matrix.client.rooms.any(
(room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
),
value: SettingsAction.myAnalytics,
child: Row(
children: [
const Icon(Icons.analytics_outlined),
const SizedBox(width: 18),
Expanded(child: Text(L10n.of(context)!.myLearning)),
],
),
),
// PopupMenuItem(
// enabled: matrix.client.rooms.any(
// (room) => !room.isSpace && !room.isArchived && !room.isAnalyticsRoom,
// ),
// value: SettingsAction.myAnalytics,
// child: Row(
// children: [
// const Icon(Icons.analytics_outlined),
// const SizedBox(width: 18),
// Expanded(child: Text(L10n.of(context)!.myLearning)),
// ],
// ),
// ),
PopupMenuItem(
value: SettingsAction.newClass,
child: Row(
@ -404,9 +403,9 @@ class ClientChooserButton extends StatelessWidget {
// case SettingsAction.spaceAnalytics:
// context.go('/rooms/analytics');
// break;
case SettingsAction.myAnalytics:
context.go('/rooms/mylearning');
break;
// case SettingsAction.myAnalytics:
// context.go('/rooms/mylearning');
// break;
case SettingsAction.logout:
pLogoutAction(context);
break;
@ -497,7 +496,7 @@ enum SettingsAction {
learning,
joinWithClassCode,
// spaceAnalytics,
myAnalytics,
// myAnalytics,
findAConversationPartner,
logout,
newClass,

View file

@ -1,16 +1,9 @@
import 'dart:async';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@ -20,89 +13,87 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import '../constants/class_default_values.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
import '../models/analytics/chart_analytics_model.dart';
import 'base_controller.dart';
import 'pangea_controller.dart';
// controls the fetching of analytics data
class AnalyticsController extends BaseController {
late PangeaController _pangeaController;
final List<AnalyticsCacheModel> _cachedAnalyticsModels = [];
final List<ConstructCacheEntry> _cachedConstructs = [];
AnalyticsController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
///////// TIME SPANS //////////
String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
String get langCode =>
_pangeaController.languageController.userL2?.langCode ??
_pangeaController.pLanguageStore.targetOptions.first.langCode;
TimeSpan get currentAnalyticsTimeSpan {
try {
final String? str = _pangeaController.pStoreService.read(
_analyticsTimeSpanKey,
);
return str != null
? TimeSpan.values.firstWhere((e) {
final spanString = e.toString();
return spanString == str;
})
: ClassDefaultValues.defaultTimeSpan;
} catch (err) {
debugger(when: kDebugMode);
return ClassDefaultValues.defaultTimeSpan;
}
}
// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
await _pangeaController.pStoreService.save(
_analyticsTimeSpanKey,
timeSpan.toString(),
);
setState();
}
// TimeSpan get currentAnalyticsTimeSpan {
// try {
// final String? str = _pangeaController.pStoreService.read(
// _analyticsTimeSpanKey,
// );
// return str != null
// ? TimeSpan.values.firstWhere((e) {
// final spanString = e.toString();
// return spanString == str;
// })
// : ClassDefaultValues.defaultTimeSpan;
// } catch (err) {
// debugger(when: kDebugMode);
// return ClassDefaultValues.defaultTimeSpan;
// }
// }
///////// SPACE ANALYTICS LANGUAGES //////////
String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
// Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
// await _pangeaController.pStoreService.save(
// _analyticsTimeSpanKey,
// timeSpan.toString(),
// );
// setState();
// }
LanguageModel get currentAnalyticsLang {
try {
final String? str = _pangeaController.pStoreService.read(
_analyticsSpaceLangKey,
);
return str != null
? PangeaLanguage.byLangCode(str)
: _pangeaController.languageController.userL2 ??
_pangeaController.pLanguageStore.targetOptions.first;
} catch (err) {
debugger(when: kDebugMode);
return _pangeaController.pLanguageStore.targetOptions.first;
}
}
// String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
await _pangeaController.pStoreService.save(
_analyticsSpaceLangKey,
lang.langCode,
);
setState();
}
// LanguageModel get currentAnalyticsLang {
// try {
// final String? str = _pangeaController.pStoreService.read(
// _analyticsSpaceLangKey,
// );
// return str != null
// ? PangeaLanguage.byLangCode(str)
// : _pangeaController.languageController.userL2 ??
// _pangeaController.pLanguageStore.targetOptions.first;
// } catch (err) {
// debugger(when: kDebugMode);
// return _pangeaController.pLanguageStore.targetOptions.first;
// }
// }
/// given an analytics event type and the current analytics language,
/// get the last time the user updated their analytics
Future<DateTime?> myAnalyticsLastUpdated(String type) async {
final List<Room> analyticsRooms = _pangeaController
.matrixState.client.allMyAnalyticsRooms
.where((room) => room.isAnalyticsRoom)
.toList();
// Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
// await _pangeaController.pStoreService.save(
// _analyticsSpaceLangKey,
// lang.langCode,
// );
// setState();
// }
/// Get the last time the user updated their analytics.
/// Tries to get the last time the user updated analytics for their current L2.
/// If there isn't yet an analytics room reacted for their L2, checks if the
/// user has any other analytics rooms and returns the most recent update time.
Future<DateTime?> myAnalyticsLastUpdated() async {
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
final Map<String, DateTime> langCodeLastUpdates = {};
for (final Room analyticsRoom in analyticsRooms) {
final String? roomLang = analyticsRoom.madeForLang;
if (roomLang == null) continue;
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
type,
_pangeaController.matrixState.client.userID!,
);
if (lastUpdated != null) {
@ -121,25 +112,20 @@ class AnalyticsController extends BaseController {
);
}
/// check if any students have recently updated their analytics
/// if any have, then the cache needs to be updated
Future<DateTime?> spaceAnalyticsLastUpdated(
String type,
Room space,
) async {
// check if any students have recently updated their analytics
// if any have, then the cache needs to be updated
// TODO - figure out how to do this on a per-student basis
await space.requestParticipants();
final List<Future<DateTime?>> lastUpdatedFutures = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
.analyticsRoomLocal(langCode, student.id);
if (analyticsRoom == null) continue;
lastUpdatedFutures.add(
analyticsRoom.analyticsLastUpdated(
type,
student.id,
),
analyticsRoom.analyticsLastUpdated(student.id),
);
}
@ -155,372 +141,16 @@ class AnalyticsController extends BaseController {
return null;
}
// Map of space ids to the last fetched hierarchy. Used when filtering
// private chat analytics to determine which children are already visible
// in the chat list
final Map<String, List<String>> _lastFetchedHierarchies = {};
void setLatestHierarchy(String spaceId, GetSpaceHierarchyResponse resp) {
final List<String> roomIds = resp.rooms.map((room) => room.roomId).toList();
_lastFetchedHierarchies[spaceId] = roomIds;
}
Future<List<String>> getLatestSpaceHierarchy(String spaceId) async {
if (!_lastFetchedHierarchies.containsKey(spaceId)) {
final resp =
await _pangeaController.matrixState.client.getSpaceHierarchy(spaceId);
setLatestHierarchy(spaceId, resp);
}
return _lastFetchedHierarchies[spaceId] ?? [];
}
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
/// get all the summary analytics events for the current user
/// in the current language's analytics room
Future<List<SummaryAnalyticsEvent>> mySummaryAnalytics() async {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
if (analyticsRoom == null) return [];
final List<AnalyticsEvent>? roomEvents =
await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.summaryAnalytics,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
);
return roomEvents?.cast<SummaryAnalyticsEvent>() ?? [];
}
Future<List<SummaryAnalyticsEvent>> spaceMemberAnalytics(
Room space,
Future<List<ConstructAnalyticsEvent>> allMyConstructs(
TimeSpan timeSpan,
) async {
// gets all the summary analytics events for the students
// in a space since the current timespace's cut off date
// ensure that the participants of the space are loaded
await space.requestParticipants();
// TODO switch to using list of futures
final List<SummaryAnalyticsEvent> analyticsEvents = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
if (analyticsRoom != null) {
final List<AnalyticsEvent>? roomEvents =
await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.summaryAnalytics,
since: currentAnalyticsTimeSpan.cutOffDate,
userId: student.id,
);
analyticsEvents.addAll(
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
);
}
}
final List<String> spaceChildrenIds = space.allSpaceChildRoomIds;
// filter out the analyics events that don't belong to the space's children
final List<SummaryAnalyticsEvent> allAnalyticsEvents = [];
for (final analyticsEvent in analyticsEvents) {
analyticsEvent.content.messages.removeWhere(
(msg) => !spaceChildrenIds.contains(msg.chatId),
);
allAnalyticsEvents.add(analyticsEvent);
}
return allAnalyticsEvents;
}
ChartAnalyticsModel? getAnalyticsLocal({
TimeSpan? timeSpan,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
bool forceUpdate = false,
bool updateExpired = false,
DateTime? lastUpdated,
}) {
timeSpan ??= currentAnalyticsTimeSpan;
final int index = _cachedAnalyticsModels.indexWhere(
(e) =>
(e.timeSpan == timeSpan) &&
(e.defaultSelected.id == defaultSelected.id) &&
(e.defaultSelected.type == defaultSelected.type) &&
(e.selected?.id == selected?.id) &&
(e.selected?.type == selected?.type) &&
(e.langCode == currentAnalyticsLang.langCode),
);
if (index != -1) {
if ((updateExpired && _cachedAnalyticsModels[index].isExpired) ||
forceUpdate ||
_cachedAnalyticsModels[index].needsUpdate(lastUpdated)) {
_cachedAnalyticsModels.removeAt(index);
} else {
return _cachedAnalyticsModels[index].chartAnalyticsModel;
}
}
return null;
}
void cacheAnalytics({
required ChartAnalyticsModel chartAnalyticsModel,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
TimeSpan? timeSpan,
}) {
_cachedAnalyticsModels.add(
AnalyticsCacheModel(
timeSpan: timeSpan ?? currentAnalyticsTimeSpan,
chartAnalyticsModel: chartAnalyticsModel,
defaultSelected: defaultSelected,
selected: selected,
langCode: currentAnalyticsLang.langCode,
),
);
}
List<SummaryAnalyticsEvent> filterStudentAnalytics(
List<SummaryAnalyticsEvent> unfiltered,
String? studentId,
) {
final List<SummaryAnalyticsEvent> filtered =
List<SummaryAnalyticsEvent>.from(unfiltered);
filtered.removeWhere((e) => e.event.senderId != studentId);
return filtered;
}
Future<List<SummaryAnalyticsEvent>> filterRoomAnalytics(
List<SummaryAnalyticsEvent> unfiltered,
String? roomID,
) async {
List<SummaryAnalyticsEvent> filtered = [...unfiltered];
Room? room;
if (roomID != null) {
room = _pangeaController.matrixState.client.getRoomById(roomID);
if (room?.isSpace == true) {
return await filterSpaceAnalytics(unfiltered, roomID);
}
}
filtered = filtered
.where(
(e) => (e.content).messages.any((u) => u.chatId == roomID),
)
.toList();
filtered.forEachIndexed(
(i, _) => (filtered[i].content).messages.removeWhere(
(u) => u.chatId != roomID,
),
);
return filtered;
}
Future<List<SummaryAnalyticsEvent>> filterPrivateChatAnalytics(
List<SummaryAnalyticsEvent> unfiltered,
Room space,
) async {
final List<String> privateChatIds = space.allSpaceChildRoomIds;
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
for (final id in lastFetched) {
privateChatIds.removeWhere((e) => e == id);
}
List<SummaryAnalyticsEvent> filtered =
List<SummaryAnalyticsEvent>.from(unfiltered);
filtered = filtered.where((e) {
return (e.content).messages.any(
(u) => privateChatIds.contains(u.chatId),
);
}).toList();
filtered.forEachIndexed(
(i, _) => (filtered[i].content).messages.removeWhere(
(u) => !privateChatIds.contains(u.chatId),
),
);
return filtered;
}
Future<List<SummaryAnalyticsEvent>> filterSpaceAnalytics(
List<SummaryAnalyticsEvent> unfiltered,
String spaceId,
) async {
final List<String> chatIds = await getLatestSpaceHierarchy(spaceId);
List<SummaryAnalyticsEvent> filtered =
List<SummaryAnalyticsEvent>.from(unfiltered);
filtered = filtered
.where(
(e) => e.content.messages.any((u) => chatIds.contains(u.chatId)),
)
.toList();
filtered.forEachIndexed(
(i, _) => (filtered[i].content).messages.removeWhere(
(u) => !chatIds.contains(u.chatId),
),
);
return filtered;
}
Future<List<SummaryAnalyticsEvent>> filterAnalytics({
required List<SummaryAnalyticsEvent> unfilteredAnalytics,
required AnalyticsSelected defaultSelected,
Room? space,
AnalyticsSelected? selected,
}) async {
for (int i = 0; i < unfilteredAnalytics.length; i++) {
unfilteredAnalytics[i].content.messages.removeWhere(
(record) => record.time.isBefore(
currentAnalyticsTimeSpan.cutOffDate,
),
);
}
switch (selected?.type) {
case null:
return unfilteredAnalytics;
case AnalyticsEntryType.student:
if (defaultSelected.type != AnalyticsEntryType.space) {
throw Exception(
"student filtering not available for default filter ${defaultSelected.type}",
);
}
return filterStudentAnalytics(unfilteredAnalytics, selected?.id);
case AnalyticsEntryType.room:
return filterRoomAnalytics(unfilteredAnalytics, selected?.id);
case AnalyticsEntryType.privateChats:
if (defaultSelected.type == AnalyticsEntryType.student) {
throw "private chat filtering not available for my analytics";
}
if (space == null) {
throw "space is null in filterAnalytics with selected type privateChats";
}
return await filterPrivateChatAnalytics(
unfilteredAnalytics,
space,
);
case AnalyticsEntryType.space:
return await filterSpaceAnalytics(unfilteredAnalytics, selected!.id);
default:
throw Exception("invalid filter type - ${selected?.type}");
}
}
Future<ChartAnalyticsModel> getAnalytics({
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
bool forceUpdate = false,
}) async {
try {
await _pangeaController.matrixState.client.roomsLoading;
// if the user is looking at space analytics, then fetch the space
Room? space;
if (defaultSelected.type == AnalyticsEntryType.space) {
space = _pangeaController.matrixState.client.getRoomById(
defaultSelected.id,
);
if (space == null) {
ErrorHandler.logError(
m: "space not found in getAnalytics",
data: {
"defaultSelected": defaultSelected,
"selected": selected,
},
);
return ChartAnalyticsModel(
msgs: [],
timeSpan: currentAnalyticsTimeSpan,
);
}
}
DateTime? lastUpdated;
if (defaultSelected.type != AnalyticsEntryType.space) {
// if default selected view is my analytics, check for the last
// time the logged in user updated their analytics events
// this gets passed to getAnalyticsLocal to determine if the cached
// entry is out-of-date
lastUpdated = await myAnalyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
);
} else {
// else, get the last time a student in the space updated their analytics
lastUpdated = await spaceAnalyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
space!,
);
}
final ChartAnalyticsModel? local = getAnalyticsLocal(
defaultSelected: defaultSelected,
selected: selected,
forceUpdate: forceUpdate,
lastUpdated: lastUpdated,
);
if (local != null && !forceUpdate) {
debugPrint("returning local analytics");
return local;
}
debugPrint("fetching new analytics");
// get all the relevant summary analytics events for the current timespan
final List<SummaryAnalyticsEvent> summaryEvents =
defaultSelected.type == AnalyticsEntryType.space
? await spaceMemberAnalytics(space!)
: await mySummaryAnalytics();
// filter out the analytics events based on filters the user has chosen
final List<SummaryAnalyticsEvent> filteredAnalytics =
await filterAnalytics(
unfilteredAnalytics: summaryEvents,
defaultSelected: defaultSelected,
space: space,
selected: selected,
);
// then create and return the model to be displayed
final ChartAnalyticsModel newModel = ChartAnalyticsModel(
timeSpan: currentAnalyticsTimeSpan,
msgs: filteredAnalytics
.map((event) => event.content.messages)
.expand((msgs) => msgs)
.toList(),
);
cacheAnalytics(
chartAnalyticsModel: newModel,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: currentAnalyticsTimeSpan,
);
return newModel;
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return ChartAnalyticsModel(
msgs: [],
timeSpan: currentAnalyticsTimeSpan,
);
}
}
//////////////////////////// CONSTRUCTS ////////////////////////////
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode);
final Room? analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(langCode);
if (analyticsRoom == null) return [];
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.construct,
since: currentAnalyticsTimeSpan.cutOffDate,
since: timeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
))
?.cast<ConstructAnalyticsEvent>();
@ -541,17 +171,17 @@ class AnalyticsController extends BaseController {
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
Room space,
TimeSpan timeSpan,
) async {
await space.requestParticipants();
final List<ConstructAnalyticsEvent> constructEvents = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
.analyticsRoomLocal(langCode, student.id);
if (analyticsRoom != null) {
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
type: PangeaEventTypes.construct,
since: currentAnalyticsTimeSpan.cutOffDate,
since: timeSpan.cutOffDate,
userId: student.id,
))
?.cast<ConstructAnalyticsEvent>();
@ -600,8 +230,9 @@ class AnalyticsController extends BaseController {
Room space,
) async {
final List<String> privateChatIds = space.allSpaceChildRoomIds;
final List<String> lastFetched = await getLatestSpaceHierarchy(space.id);
for (final id in lastFetched) {
final resp = await space.client.getSpaceHierarchy(space.id);
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
for (final id in chatIds) {
privateChatIds.removeWhere((e) => e == id);
}
final List<ConstructAnalyticsEvent> filtered =
@ -618,7 +249,8 @@ class AnalyticsController extends BaseController {
List<ConstructAnalyticsEvent> unfilteredConstructs,
Room space,
) async {
final List<String> chatIds = await getLatestSpaceHierarchy(space.id);
final resp = await space.client.getSpaceHierarchy(space.id);
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
final List<ConstructAnalyticsEvent> filtered =
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
@ -633,10 +265,10 @@ class AnalyticsController extends BaseController {
List<ConstructAnalyticsEvent>? getConstructsLocal({
required TimeSpan timeSpan,
required ConstructTypeEnum constructType,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
DateTime? lastUpdated,
ConstructTypeEnum? constructType,
}) {
final index = _cachedConstructs.indexWhere(
(e) =>
@ -646,7 +278,7 @@ class AnalyticsController extends BaseController {
e.defaultSelected.type == defaultSelected.type &&
e.selected?.id == selected?.id &&
e.selected?.type == selected?.type &&
e.langCode == currentAnalyticsLang.langCode,
e.langCode == langCode,
);
if (index > -1) {
@ -661,29 +293,31 @@ class AnalyticsController extends BaseController {
}
void cacheConstructs({
required ConstructTypeEnum constructType,
required List<ConstructAnalyticsEvent> events,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
ConstructTypeEnum? constructType,
}) {
final entry = ConstructCacheEntry(
timeSpan: currentAnalyticsTimeSpan,
timeSpan: timeSpan,
type: constructType,
events: List.from(events),
defaultSelected: defaultSelected,
selected: selected,
langCode: currentAnalyticsLang.langCode,
langCode: langCode,
);
_cachedConstructs.add(entry);
}
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
required AnalyticsSelected defaultSelected,
required ConstructTypeEnum constructType,
required TimeSpan timeSpan,
ConstructTypeEnum? constructType,
AnalyticsSelected? selected,
}) async {
final List<ConstructAnalyticsEvent> unfilteredConstructs =
await allMyConstructs();
await allMyConstructs(timeSpan);
final Room? space = selected?.type == AnalyticsEntryType.space
? _pangeaController.matrixState.client.getRoomById(selected!.id)
@ -694,18 +328,21 @@ class AnalyticsController extends BaseController {
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
required ConstructTypeEnum constructType,
required Room space,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
ConstructTypeEnum? constructType,
}) async {
final List<ConstructAnalyticsEvent> unfilteredConstructs =
await allSpaceMemberConstructs(
space,
timeSpan,
);
return filterConstructs(
@ -713,12 +350,14 @@ class AnalyticsController extends BaseController {
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
Future<List<ConstructAnalyticsEvent>> filterConstructs({
required List<ConstructAnalyticsEvent> unfilteredConstructs,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
Room? space,
AnalyticsSelected? selected,
}) async {
@ -730,7 +369,7 @@ class AnalyticsController extends BaseController {
for (int i = 0; i < unfilteredConstructs.length; i++) {
final construct = unfilteredConstructs[i];
construct.content.uses.removeWhere(
(use) => use.timeStamp.isBefore(currentAnalyticsTimeSpan.cutOffDate),
(use) => use.timeStamp.isBefore(timeSpan.cutOffDate),
);
}
@ -760,11 +399,12 @@ class AnalyticsController extends BaseController {
}
Future<List<ConstructAnalyticsEvent>?> getConstructs({
required ConstructTypeEnum constructType,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
bool removeIT = true,
bool forceUpdate = false,
ConstructTypeEnum? constructType,
}) async {
debugPrint("getting constructs");
await _pangeaController.matrixState.client.roomsLoading;
@ -792,19 +432,16 @@ class AnalyticsController extends BaseController {
// time the logged in user updated their analytics events
// this gets passed to getAnalyticsLocal to determine if the cached
// entry is out-of-date
lastUpdated = await myAnalyticsLastUpdated(
PangeaEventTypes.construct,
);
lastUpdated = await myAnalyticsLastUpdated();
} else {
// else, get the last time a student in the space updated their analytics
lastUpdated = await spaceAnalyticsLastUpdated(
PangeaEventTypes.construct,
space!,
);
}
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
timeSpan: currentAnalyticsTimeSpan,
timeSpan: timeSpan,
constructType: constructType,
defaultSelected: defaultSelected,
selected: selected,
@ -821,12 +458,14 @@ class AnalyticsController extends BaseController {
constructType: constructType,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
)
: await getSpaceConstructs(
constructType: constructType,
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
if (removeIT) {
@ -846,6 +485,7 @@ class AnalyticsController extends BaseController {
events: filteredConstructs,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
@ -889,32 +529,15 @@ abstract class CacheEntry {
}
class ConstructCacheEntry extends CacheEntry {
final ConstructTypeEnum type;
final ConstructTypeEnum? type;
final List<ConstructAnalyticsEvent> events;
ConstructCacheEntry({
required this.type,
required this.events,
required super.timeSpan,
required super.langCode,
required super.defaultSelected,
this.type,
super.selected,
});
}
class AnalyticsCacheModel extends CacheEntry {
final ChartAnalyticsModel chartAnalyticsModel;
AnalyticsCacheModel({
required this.chartAnalyticsModel,
required super.timeSpan,
required super.langCode,
required super.defaultSelected,
super.selected,
});
@override
bool get isExpired =>
DateTime.now().difference(_createdAt).inMinutes >
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
}

View file

@ -4,18 +4,16 @@ import 'dart:developer';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
/// handles the processing of analytics for
/// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
@ -56,15 +54,14 @@ class MyAnalyticsController {
/// If analytics haven't been updated in the last day, update them
Future<DateTime?> _refreshAnalyticsIfOutdated() async {
DateTime? lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
DateTime? lastUpdated =
await _pangeaController.analytics.myAnalyticsLastUpdated();
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
if (lastUpdated?.isBefore(yesterday) ?? true) {
debugPrint("analytics out-of-date, updating");
await updateAnalytics();
lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
lastUpdated = await _pangeaController.analytics.myAnalyticsLastUpdated();
}
return lastUpdated;
}
@ -238,7 +235,6 @@ class MyAnalyticsController {
// get the last time analytics were updated for this room
final DateTime? l2AnalyticsLastUpdated =
await analyticsRoom.analyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
_client.userID!,
);
@ -302,16 +298,6 @@ class MyAnalyticsController {
final List<PangeaMessageEvent> allRecentMessages =
recentPangeaMessageEvents.expand((e) => e).toList();
final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(allRecentMessages);
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
);
}
// get constructs for messages
final List<OneConstructUse> recentConstructUses = [];
for (final PangeaMessageEvent message in allRecentMessages) {

View file

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum ProgressIndicatorEnum {
level,
wordsUsed,
errorTypes,
}
extension ProgressIndicatorsExtension on ProgressIndicatorEnum {
IconData get icon {
switch (this) {
case ProgressIndicatorEnum.wordsUsed:
return Icons.text_fields_outlined;
case ProgressIndicatorEnum.errorTypes:
return Icons.error_outline;
case ProgressIndicatorEnum.level:
return Icons.star;
}
}
static bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
Color color(BuildContext context) {
switch (this) {
case ProgressIndicatorEnum.wordsUsed:
return Theme.of(context).brightness == Brightness.dark
? const Color.fromARGB(255, 169, 183, 237)
: const Color.fromARGB(255, 38, 59, 141);
case ProgressIndicatorEnum.errorTypes:
return Theme.of(context).brightness == Brightness.dark
? const Color.fromARGB(255, 212, 144, 216)
: const Color.fromARGB(255, 163, 39, 169);
case ProgressIndicatorEnum.level:
return Theme.of(context).brightness == Brightness.dark
? const Color.fromARGB(255, 250, 220, 129)
: const Color.fromARGB(255, 255, 208, 67);
default:
return Theme.of(context).textTheme.bodyLarge!.color ?? Colors.blueGrey;
}
}
String tooltip(BuildContext context) {
switch (this) {
case ProgressIndicatorEnum.wordsUsed:
return L10n.of(context)!.wordsUsed;
case ProgressIndicatorEnum.errorTypes:
return L10n.of(context)!.errorTypes;
case ProgressIndicatorEnum.level:
return L10n.of(context)!.level;
}
}
}

View file

@ -1,9 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../models/analytics/chart_analytics_model.dart';
enum TimeSpan { day, week, month, sixmonths, year }
enum TimeSpan { day, week, month, sixmonths, year, forever }
extension TimeSpanFunctions on TimeSpan {
String string(BuildContext context) {
@ -35,19 +33,8 @@ extension TimeSpanFunctions on TimeSpan {
return 6;
case TimeSpan.year:
return 12;
}
}
Duration timeAgo(int index) {
switch (this) {
case TimeSpan.day:
return Duration(hours: index);
case TimeSpan.week:
case TimeSpan.month:
return Duration(days: index);
case TimeSpan.year:
case TimeSpan.sixmonths:
return Duration(days: index * 32);
case TimeSpan.forever:
return 0;
}
}
@ -65,44 +52,8 @@ extension TimeSpanFunctions on TimeSpan {
return DateTime.now().subtract(Duration(days: numberOfIntervals * 30));
case TimeSpan.year:
return DateTime.now().subtract(const Duration(days: 365));
case TimeSpan.forever:
return DateTime(2020);
}
}
String getMapKey(DateTime date) {
switch (this) {
case TimeSpan.day:
return date.hour.toString();
case TimeSpan.week:
return date.weekday.toString();
case TimeSpan.month:
return date.day.toString();
case TimeSpan.sixmonths:
case TimeSpan.year:
return date.month.toString();
}
}
/// Note: end is same as start!!
Map<String, TimeSeriesInterval> get emptyIntervals {
final DateTime now = DateTime.now();
final List<int> numbers =
List.generate(numberOfIntervals, (index) => index);
final Map<String, TimeSeriesInterval> map = {};
// debugger(when: kDebugMode);
for (final index in numbers) {
final timeAgos = timeAgo(index);
final DateTime end = now.subtract(timeAgos);
// debugger(when: end.isBefore(now.subtract(const Duration(days: 30))));
final String mapKey = getMapKey(end);
// debugger(when: mapKey.toString() == "5");
map[mapKey] = TimeSeriesInterval(
start: end,
end: end,
totals: TimeSeriesTotals.empty,
);
}
// debugger(when: kDebugMode);
return map;
}
}

View file

@ -152,7 +152,6 @@ extension AnalyticsClientExtension on Client {
final Map<String, DateTime?> lastUpdatedMap = {};
for (final analyticsRoom in allMyAnalyticsRooms) {
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
userID!,
);
lastUpdatedMap[analyticsRoom.id] = lastUpdated;

View file

@ -282,83 +282,6 @@ extension EventsRoomExtension on Room {
);
}
Future<List<RecentMessageRecord>> get _messageListForAllChildChats async {
try {
if (!isSpace) return [];
final List<Room> spaceChats = spaceChildren
.where((e) => e.roomId != null)
.map((e) => client.getRoomById(e.roomId!))
.where((element) => element != null)
.cast<Room>()
.where((element) => !element.isSpace)
.toList();
final List<Future<List<RecentMessageRecord>>> msgListFutures = [];
for (final chat in spaceChats) {
msgListFutures.add(chat._messageListForChat);
}
final List<List<RecentMessageRecord>> msgLists =
await Future.wait(msgListFutures);
final List<RecentMessageRecord> joined = [];
for (final msgList in msgLists) {
joined.addAll(msgList);
}
return joined;
} catch (err) {
// debugger(when: kDebugMode);
rethrow;
}
}
Future<List<RecentMessageRecord>> get _messageListForChat async {
try {
int numberOfSearches = 0;
if (isSpace) {
throw Exception(
"In messageListForChat with room that is not a chat",
);
}
final Timeline timeline = await getTimeline();
while (timeline.canRequestHistory && numberOfSearches < 50) {
await timeline.requestHistory(historyCount: 100);
numberOfSearches += 1;
}
if (timeline.canRequestHistory) {
debugger(when: kDebugMode);
}
final List<RecentMessageRecord> msgs = [];
for (final event in timeline.events) {
if (event.senderId == client.userID &&
event.type == EventTypes.Message &&
event.content['msgtype'] == MessageTypes.Text) {
final PangeaMessageEvent pMsgEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: true,
);
msgs.add(
RecentMessageRecord(
eventId: event.eventId,
chatId: id,
useType: pMsgEvent.msgUseType,
time: event.originServerTs,
),
);
}
}
return msgs;
} catch (err, s) {
if (kDebugMode) rethrow;
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return [];
}
}
// ConstructEvent? _vocabEventLocal(String lemma) {
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
@ -451,14 +374,11 @@ extension EventsRoomExtension on Room {
return false;
}
while (timeline.canRequestHistory &&
!reachedEnd() &&
numberOfSearches < 10) {
while (timeline.canRequestHistory && numberOfSearches < 10) {
await timeline.requestHistory(historyCount: 100);
numberOfSearches += 1;
if (reachedEnd()) {
break;
}
if (!timeline.canRequestHistory) break;
if (reachedEnd()) break;
}
final List<Event> fetchedEvents = timeline.events

View file

@ -9,12 +9,8 @@ import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
@ -80,22 +76,15 @@ extension PangeaRoom on Room {
void inviteSpaceTeachersToAnalyticsRooms() =>
_inviteSpaceTeachersToAnalyticsRooms();
Future<AnalyticsEvent?> getLastAnalyticsEvent(
String type,
String userId,
) async =>
await _getLastAnalyticsEvent(type, userId);
Future<DateTime?> analyticsLastUpdated(String type, String userId) async {
return await _analyticsLastUpdated(type, userId);
Future<DateTime?> analyticsLastUpdated(String userId) async {
return await _analyticsLastUpdated(userId);
}
Future<List<AnalyticsEvent>?> getAnalyticsEvents({
required String type,
Future<List<ConstructAnalyticsEvent>?> getAnalyticsEvents({
required String userId,
DateTime? since,
}) async =>
await _getAnalyticsEvents(type: type, since: since, userId: userId);
await _getAnalyticsEvents(since: since, userId: userId);
String? get madeForLang => _madeForLang;

View file

@ -140,52 +140,36 @@ extension AnalyticsRoomExtension on Room {
);
}
Future<AnalyticsEvent?> _getLastAnalyticsEvent(
String type,
Future<ConstructAnalyticsEvent?> _getLastAnalyticsEvent(
String userId,
) async {
final List<Event> events = await getEventsBySender(
type: type,
type: PangeaEventTypes.construct,
sender: userId,
count: 10,
);
if (events.isEmpty) return null;
final Event event = events.first;
AnalyticsEvent? analyticsEvent;
switch (type) {
case PangeaEventTypes.summaryAnalytics:
analyticsEvent = SummaryAnalyticsEvent(event: event);
case PangeaEventTypes.construct:
analyticsEvent = ConstructAnalyticsEvent(event: event);
}
return analyticsEvent;
return ConstructAnalyticsEvent(event: event);
}
Future<DateTime?> _analyticsLastUpdated(String type, String userId) async {
final lastEvent = await _getLastAnalyticsEvent(type, userId);
Future<DateTime?> _analyticsLastUpdated(String userId) async {
final lastEvent = await _getLastAnalyticsEvent(userId);
return lastEvent?.event.originServerTs;
}
Future<List<AnalyticsEvent>?> _getAnalyticsEvents({
required String type,
Future<List<ConstructAnalyticsEvent>?> _getAnalyticsEvents({
required String userId,
DateTime? since,
}) async {
final List<Event> events = await getEventsBySender(
type: type,
type: PangeaEventTypes.construct,
sender: userId,
since: since,
);
final List<AnalyticsEvent> analyticsEvents = [];
final List<ConstructAnalyticsEvent> analyticsEvents = [];
for (final Event event in events) {
switch (type) {
case PangeaEventTypes.summaryAnalytics:
analyticsEvents.add(SummaryAnalyticsEvent(event: event));
break;
case PangeaEventTypes.construct:
analyticsEvents.add(ConstructAnalyticsEvent(event: event));
break;
}
analyticsEvents.add(ConstructAnalyticsEvent(event: event));
}
return analyticsEvents;
@ -203,18 +187,6 @@ extension AnalyticsRoomExtension on Room {
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
}
Future<void> sendSummaryAnalyticsEvent(
List<RecentMessageRecord> records,
) async {
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: records,
);
await sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
}
/// Sends construct events to the server.
///
/// The [uses] parameter is a list of [OneConstructUse] objects representing the

View file

@ -1,29 +0,0 @@
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:matrix/matrix.dart';
// superclass for all analytics events
abstract class AnalyticsEvent {
late Event _event;
AnalyticsModel? contentCache;
AnalyticsEvent({required Event event}) {
_event = event;
}
Event get event => _event;
AnalyticsModel get content {
switch (_event.type) {
case PangeaEventTypes.summaryAnalytics:
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
break;
case PangeaEventTypes.construct:
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
break;
}
return contentCache!;
}
}

View file

@ -1,23 +0,0 @@
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
abstract class AnalyticsModel {
static List<dynamic> formatAnalyticsContent(
List<PangeaMessageEvent> recentMsgs,
String type,
) {
switch (type) {
case PangeaEventTypes.summaryAnalytics:
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
case PangeaEventTypes.construct:
final List<OneConstructUse> uses = [];
for (final msg in recentMsgs) {
uses.addAll(msg.allConstructUses);
}
return uses;
}
return [];
}
}

View file

@ -1,149 +1,149 @@
import 'dart:developer';
// import 'dart:developer';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:flutter/foundation.dart';
// import 'package:fluffychat/pangea/enum/time_span.dart';
// import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
// import 'package:flutter/foundation.dart';
import '../../enum/use_type.dart';
// import '../../enum/use_type.dart';
class TimeSeriesTotals {
int ta;
int ga;
int wa;
int un;
// class TimeSeriesTotals {
// int ta;
// int ga;
// int wa;
// int un;
int get all => ta + ga + wa + un;
// int get all => ta + ga + wa + un;
TimeSeriesTotals({
required this.ta,
required this.ga,
required this.wa,
required this.un,
});
// TimeSeriesTotals({
// required this.ta,
// required this.ga,
// required this.wa,
// required this.un,
// });
Map<String, dynamic> toJson() => {
UseType.ta.string: ta,
UseType.ga.string: ga,
UseType.wa.string: wa,
UseType.un.string: un,
};
// Map<String, dynamic> toJson() => {
// UseType.ta.string: ta,
// UseType.ga.string: ga,
// UseType.wa.string: wa,
// UseType.un.string: un,
// };
factory TimeSeriesTotals.fromJson(json) => TimeSeriesTotals(
ta: json[UseType.ta.string],
ga: json[UseType.ga.string],
wa: json[UseType.wa.string],
un: json[UseType.un.string],
);
// factory TimeSeriesTotals.fromJson(json) => TimeSeriesTotals(
// ta: json[UseType.ta.string],
// ga: json[UseType.ga.string],
// wa: json[UseType.wa.string],
// un: json[UseType.un.string],
// );
static get empty => TimeSeriesTotals(ta: 0, ga: 0, wa: 0, un: 0);
// static get empty => TimeSeriesTotals(ta: 0, ga: 0, wa: 0, un: 0);
int get taPercent => all != 0 ? (ta / all * 100).round() : 0;
int get gaPercent => all != 0 ? (ga / all * 100).round() : 0;
int get waPercent => all != 0 ? (wa / all * 100).round() : 0;
int get unPercent => all != 0 ? (un / all * 100).round() : 0;
// int get taPercent => all != 0 ? (ta / all * 100).round() : 0;
// int get gaPercent => all != 0 ? (ga / all * 100).round() : 0;
// int get waPercent => all != 0 ? (wa / all * 100).round() : 0;
// int get unPercent => all != 0 ? (un / all * 100).round() : 0;
void increment(RecentMessageRecord msg) {
switch (msg.useType) {
case UseType.ta:
ta += 1;
break;
case UseType.wa:
wa += 1;
break;
case UseType.ga:
ga += 1;
break;
case UseType.un:
un += 1;
break;
default:
debugger(when: kDebugMode);
debugPrint("message with bad type ${msg.toJson()}");
}
}
}
// void increment(RecentMessageRecord msg) {
// switch (msg.useType) {
// case UseType.ta:
// ta += 1;
// break;
// case UseType.wa:
// wa += 1;
// break;
// case UseType.ga:
// ga += 1;
// break;
// case UseType.un:
// un += 1;
// break;
// default:
// debugger(when: kDebugMode);
// debugPrint("message with bad type ${msg.toJson()}");
// }
// }
// }
class TimeSeriesInterval {
DateTime start;
DateTime end;
TimeSeriesTotals totals;
// class TimeSeriesInterval {
// DateTime start;
// DateTime end;
// TimeSeriesTotals totals;
TimeSeriesInterval({
required this.start,
required this.end,
required this.totals,
});
// TimeSeriesInterval({
// required this.start,
// required this.end,
// required this.totals,
// });
Map<String, dynamic> toJson() => {
"strt": start.toIso8601String(),
"end": end.toIso8601String(),
"totals": totals.toJson(),
};
// Map<String, dynamic> toJson() => {
// "strt": start.toIso8601String(),
// "end": end.toIso8601String(),
// "totals": totals.toJson(),
// };
factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
start: DateTime.parse(json["strt"]),
end: DateTime.parse(json["end"]),
totals: TimeSeriesTotals.fromJson(json["totals"]),
);
}
// factory TimeSeriesInterval.fromJson(json) => TimeSeriesInterval(
// start: DateTime.parse(json["strt"]),
// end: DateTime.parse(json["end"]),
// totals: TimeSeriesTotals.fromJson(json["totals"]),
// );
// }
class ChartAnalyticsModel {
final TimeSpan timeSpan;
final TimeSeriesTotals totals = TimeSeriesTotals.empty;
final List<RecentMessageRecord> msgs;
final String? chatId;
// class ChartAnalyticsModel {
// final TimeSpan timeSpan;
// final TimeSeriesTotals totals = TimeSeriesTotals.empty;
// final List<RecentMessageRecord> msgs;
// final String? chatId;
late DateTime fetchedAt;
late List<TimeSeriesInterval> timeSeries;
DateTime? lastMessage;
// late DateTime fetchedAt;
// late List<TimeSeriesInterval> timeSeries;
// DateTime? lastMessage;
ChartAnalyticsModel({
required this.timeSpan,
required this.msgs,
this.chatId,
}) {
fetchedAt = DateTime.now();
calculate();
}
// ChartAnalyticsModel({
// required this.timeSpan,
// required this.msgs,
// this.chatId,
// }) {
// fetchedAt = DateTime.now();
// calculate();
// }
bool get isEmpty => (totals.ga + totals.ta + totals.wa == 0);
// bool get isEmpty => (totals.ga + totals.ta + totals.wa == 0);
void calculate() {
final Map<String, TimeSeriesInterval> intervals = timeSpan.emptyIntervals;
final DateTime cutOff = timeSpan.cutOffDate;
// void calculate() {
// final Map<String, TimeSeriesInterval> intervals = timeSpan.emptyIntervals;
// final DateTime cutOff = timeSpan.cutOffDate;
final filtered = msgs.where(
(msg) =>
(chatId == null || msg.chatId == chatId) && msg.time.isAfter(cutOff),
);
// final filtered = msgs.where(
// (msg) =>
// (chatId == null || msg.chatId == chatId) && msg.time.isAfter(cutOff),
// );
//remove msgs with duplicate ids
final Map<String, RecentMessageRecord> unique = {};
for (final msg in filtered) {
if (unique[msg.eventId] == null) {
unique[msg.eventId] = msg;
}
}
// //remove msgs with duplicate ids
// final Map<String, RecentMessageRecord> unique = {};
// for (final msg in filtered) {
// if (unique[msg.eventId] == null) {
// unique[msg.eventId] = msg;
// }
// }
for (final msg in unique.values) {
final String key = timeSpan.getMapKey(msg.time);
if (intervals[key] == null) {
debugger(when: kDebugMode);
} else {
intervals[key]!.totals.increment(msg);
totals.increment(msg);
lastMessage = msg.time;
}
}
timeSeries = intervals.values.toList().reversed.toList();
}
// for (final msg in unique.values) {
// final String key = timeSpan.getMapKey(msg.time);
// if (intervals[key] == null) {
// debugger(when: kDebugMode);
// } else {
// intervals[key]!.totals.increment(msg);
// totals.increment(msg);
// lastMessage = msg.time;
// }
// }
// timeSeries = intervals.values.toList().reversed.toList();
// }
DateTime? get lastMessageTime {
if (msgs.isEmpty) {
return null;
}
return msgs.map((msg) => msg.time).reduce(
(compare, recent) => compare.isAfter(recent) ? compare : recent,
);
}
}
// DateTime? get lastMessageTime {
// if (msgs.isEmpty) {
// return null;
// }
// return msgs.map((msg) => msg.time).reduce(
// (compare, recent) => compare.isAfter(recent) ? compare : recent,
// );
// }
// }

View file

@ -1,11 +1,13 @@
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:matrix/matrix.dart';
import '../../constants/pangea_event_types.dart';
class ConstructAnalyticsEvent extends AnalyticsEvent {
ConstructAnalyticsEvent({required Event event}) : super(event: event) {
class ConstructAnalyticsEvent {
late Event _event;
ConstructAnalyticsModel? contentCache;
ConstructAnalyticsEvent({required Event event}) {
_event = event;
if (event.type != PangeaEventTypes.construct) {
throw Exception(
"${event.type} should not be used to make a ConstructAnalyticsEvent",
@ -13,7 +15,8 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
}
}
@override
Event get event => _event;
ConstructAnalyticsModel get content {
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
return contentCache as ConstructAnalyticsModel;

View file

@ -1,14 +1,13 @@
import 'dart:developer';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../../enum/construct_type_enum.dart';
class ConstructAnalyticsModel extends AnalyticsModel {
class ConstructAnalyticsModel {
List<OneConstructUse> uses;
ConstructAnalyticsModel({

View file

@ -1,21 +0,0 @@
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:matrix/matrix.dart';
import '../../constants/pangea_event_types.dart';
class SummaryAnalyticsEvent extends AnalyticsEvent {
SummaryAnalyticsEvent({required Event event}) : super(event: event) {
if (event.type != PangeaEventTypes.summaryAnalytics) {
throw Exception(
"${event.type} should not be used to make a SummaryAnalyticsEvent",
);
}
}
@override
SummaryAnalyticsModel get content {
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
return contentCache as SummaryAnalyticsModel;
}
}

View file

@ -1,111 +0,0 @@
import 'dart:convert';
import 'package:fluffychat/pangea/enum/use_type.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
class SummaryAnalyticsModel extends AnalyticsModel {
late List<RecentMessageRecord> _messages;
SummaryAnalyticsModel({
required List<RecentMessageRecord> messages,
}) {
_messages = messages;
}
List<RecentMessageRecord> get messages => _messages;
static const _messagesKey = "msgs";
Map<String, dynamic> toJson() => {
_messagesKey: jsonEncode(_messages.map((e) => e.toJson()).toList()),
};
factory SummaryAnalyticsModel.fromJson(json) {
List<RecentMessageRecord> savedMessages = [];
try {
savedMessages = json[_messagesKey] != null
? (jsonDecode(json[_messagesKey] ?? "[]") as Iterable)
.map((e) => RecentMessageRecord.fromJson(e))
.toList()
.cast<RecentMessageRecord>()
: [];
} catch (err, stack) {
if (kDebugMode) rethrow;
ErrorHandler.logError(e: err, s: stack);
}
return SummaryAnalyticsModel(
messages: savedMessages,
);
}
static List<RecentMessageRecord> formatSummaryContent(
List<PangeaMessageEvent> recentMsgs,
) {
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
final List<RecentMessageRecord> records = filtered
.map(
(msg) => RecentMessageRecord(
eventId: msg.eventId,
chatId: msg.room.id,
useType: msg.msgUseType,
time: msg.originServerTs,
),
)
.toList();
return records;
}
}
class RecentMessageRecord {
String eventId;
String chatId;
UseType useType;
DateTime time;
RecentMessageRecord({
required this.eventId,
required this.chatId,
required this.useType,
required this.time,
});
factory RecentMessageRecord.fromJson(Map<String, dynamic> json) =>
RecentMessageRecord(
eventId: json[_eventIdKey],
chatId: json[_chatIdKey],
useType: _typeStringToEnum(json[_typeOfUseKey]),
time: DateTime.parse(json[_timeKey]),
);
Map<String, dynamic> toJson() => {
_eventIdKey: eventId,
_chatIdKey: chatId,
_typeOfUseKey: _typeEnumToString(useType),
_timeKey: time.toIso8601String(),
};
String _typeEnumToString(dynamic status) => status.toString().split('.').last;
static UseType _typeStringToEnum(String useType) {
final String lastPart = useType.toString().split('.').last;
switch (lastPart) {
case 'ta':
return UseType.ta;
case 'ga':
return UseType.ga;
case 'wa':
return UseType.wa;
default:
return UseType.un;
}
}
static const _eventIdKey = "m.id";
static const _chatIdKey = "c.id";
static const _typeOfUseKey = "typ";
static const _timeKey = "t";
}

View file

@ -1,156 +1,156 @@
import 'dart:async';
// import 'dart:async';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/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:matrix/matrix.dart';
// import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/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:matrix/matrix.dart';
import '../../../../utils/date_time_extension.dart';
import '../../../widgets/avatar.dart';
import '../../../widgets/matrix.dart';
import '../../models/analytics/chart_analytics_model.dart';
import 'base_analytics.dart';
import 'list_summary_analytics.dart';
// import '../../../../utils/date_time_extension.dart';
// import '../../../widgets/avatar.dart';
// import '../../../widgets/matrix.dart';
// import '../../models/analytics/chart_analytics_model.dart';
// import 'base_analytics.dart';
// import 'list_summary_analytics.dart';
class AnalyticsListTile extends StatefulWidget {
const AnalyticsListTile({
super.key,
required this.defaultSelected,
required this.selected,
required this.avatar,
required this.allowNavigateOnSelect,
required this.isSelected,
required this.onTap,
required this.pangeaController,
this.controller,
this.refreshStream,
});
// class AnalyticsListTile extends StatefulWidget {
// const AnalyticsListTile({
// super.key,
// required this.defaultSelected,
// required this.selected,
// required this.avatar,
// required this.allowNavigateOnSelect,
// required this.isSelected,
// required this.onTap,
// required this.pangeaController,
// this.controller,
// this.refreshStream,
// });
final void Function(AnalyticsSelected) onTap;
// final void Function(AnalyticsSelected) onTap;
final AnalyticsSelected defaultSelected;
final AnalyticsSelected selected;
// final AnalyticsSelected defaultSelected;
// final AnalyticsSelected selected;
final Uri? avatar;
// final Uri? avatar;
final bool allowNavigateOnSelect;
final bool isSelected;
// final bool allowNavigateOnSelect;
// final bool isSelected;
final PangeaController pangeaController;
final BaseAnalyticsController? controller;
final StreamController? refreshStream;
// final PangeaController pangeaController;
// final BaseAnalyticsController? controller;
// final StreamController? refreshStream;
@override
AnalyticsListTileState createState() => AnalyticsListTileState();
}
// @override
// AnalyticsListTileState createState() => AnalyticsListTileState();
// }
class AnalyticsListTileState extends State<AnalyticsListTile> {
ChartAnalyticsModel? tileData;
StreamSubscription? refreshSubscription;
// class AnalyticsListTileState extends State<AnalyticsListTile> {
// ChartAnalyticsModel? tileData;
// StreamSubscription? refreshSubscription;
@override
void initState() {
super.initState();
setTileData();
refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) {
setTileData(forceUpdate: forceUpdate);
});
}
// @override
// void initState() {
// super.initState();
// setTileData();
// refreshSubscription = widget.refreshStream?.stream.listen((forceUpdate) {
// setTileData(forceUpdate: forceUpdate);
// });
// }
@override
void didUpdateWidget(covariant AnalyticsListTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selected != widget.selected) {
setTileData();
}
}
// @override
// void didUpdateWidget(covariant AnalyticsListTile oldWidget) {
// super.didUpdateWidget(oldWidget);
// if (oldWidget.selected != widget.selected) {
// setTileData();
// }
// }
@override
void dispose() {
refreshSubscription?.cancel();
super.dispose();
}
// @override
// void dispose() {
// refreshSubscription?.cancel();
// super.dispose();
// }
Future<void> setTileData({forceUpdate = false}) async {
tileData = await MatrixState.pangeaController.analytics.getAnalytics(
defaultSelected: widget.defaultSelected,
selected: widget.selected,
forceUpdate: forceUpdate,
);
if (mounted) setState(() {});
}
// Future<void> setTileData({forceUpdate = false}) async {
// tileData = await MatrixState.pangeaController.analytics.getAnalytics(
// defaultSelected: widget.defaultSelected,
// selected: widget.selected,
// forceUpdate: forceUpdate,
// );
// if (mounted) setState(() {});
// }
@override
Widget build(BuildContext context) {
final Room? room =
Matrix.of(context).client.getRoomById(widget.selected.id);
return Material(
color: widget.isSelected
? Theme.of(context).colorScheme.secondaryContainer
: Colors.transparent,
child: ListTile(
leading: widget.selected.type == AnalyticsEntryType.privateChats
? CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: const Icon(Icons.forum),
)
: Avatar(
mxContent: widget.avatar,
name: widget.selected.displayName,
littleIcon: room?.roomTypeIcon,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
widget.selected.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
),
Tooltip(
message: L10n.of(context)!.timeOfLastMessage,
child: Text(
tileData?.lastMessageTime?.localizedTimeShort(context) ?? "",
style: TextStyle(
fontSize: 13,
color: Theme.of(context).textTheme.bodyMedium!.color,
),
),
),
],
),
subtitle: ListSummaryAnalytics(
chartAnalytics: tileData,
),
selected: widget.isSelected,
onTap: () {
if (widget.controller?.widget.selectedView == null) {
widget.onTap(widget.selected);
return;
}
if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
context.go('/rooms/analytics/${room!.id}');
return;
}
widget.onTap(widget.selected);
},
trailing: (room?.isSpace ?? false) &&
widget.selected.type != AnalyticsEntryType.privateChats &&
widget.allowNavigateOnSelect
? const Icon(Icons.chevron_right)
: null,
),
);
}
}
// @override
// Widget build(BuildContext context) {
// final Room? room =
// Matrix.of(context).client.getRoomById(widget.selected.id);
// return Material(
// color: widget.isSelected
// ? Theme.of(context).colorScheme.secondaryContainer
// : Colors.transparent,
// child: ListTile(
// leading: widget.selected.type == AnalyticsEntryType.privateChats
// ? CircleAvatar(
// backgroundColor: Theme.of(context).primaryColor,
// foregroundColor: Colors.white,
// radius: Avatar.defaultSize / 2,
// child: const Icon(Icons.forum),
// )
// : Avatar(
// mxContent: widget.avatar,
// name: widget.selected.displayName,
// littleIcon: room?.roomTypeIcon,
// ),
// title: Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Expanded(
// child: Text(
// widget.selected.displayName,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// softWrap: false,
// style: TextStyle(
// fontWeight: FontWeight.bold,
// color: Theme.of(context).textTheme.bodyLarge!.color,
// ),
// ),
// ),
// Tooltip(
// message: L10n.of(context)!.timeOfLastMessage,
// child: Text(
// tileData?.lastMessageTime?.localizedTimeShort(context) ?? "",
// style: TextStyle(
// fontSize: 13,
// color: Theme.of(context).textTheme.bodyMedium!.color,
// ),
// ),
// ),
// ],
// ),
// subtitle: ListSummaryAnalytics(
// chartAnalytics: tileData,
// ),
// selected: widget.isSelected,
// onTap: () {
// if (widget.controller?.widget.selectedView == null) {
// widget.onTap(widget.selected);
// return;
// }
// if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
// context.go('/rooms/analytics/${room!.id}');
// return;
// }
// widget.onTap(widget.selected);
// },
// trailing: (room?.isSpace ?? false) &&
// widget.selected.type != AnalyticsEntryType.privateChats &&
// widget.allowNavigateOnSelect
// ? const Icon(Icons.chevron_right)
// : null,
// ),
// );
// }
// }

View file

@ -1,214 +1,214 @@
import 'dart:async';
// import 'dart:async';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
// import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
// import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
// import 'package:fluffychat/pangea/models/language_model.dart';
// import 'package:fluffychat/pangea/pages/analytics/base_analytics_view.dart';
// import 'package:fluffychat/pangea/pages/analytics/student_analytics/student_analytics.dart';
// import 'package:flutter/material.dart';
// import 'package:future_loading_dialog/future_loading_dialog.dart';
// import 'package:matrix/matrix.dart';
import '../../../widgets/matrix.dart';
import '../../controllers/pangea_controller.dart';
import '../../enum/bar_chart_view_enum.dart';
import '../../enum/time_span.dart';
import '../../models/analytics/chart_analytics_model.dart';
// import '../../../widgets/matrix.dart';
// import '../../controllers/pangea_controller.dart';
// import '../../enum/bar_chart_view_enum.dart';
// import '../../enum/time_span.dart';
// import '../../models/analytics/chart_analytics_model.dart';
class BaseAnalyticsPage extends StatefulWidget {
final String pageTitle;
final List<TabData> tabs;
final BarChartViewSelection selectedView;
// class BaseAnalyticsPage extends StatefulWidget {
// final String pageTitle;
// final List<TabData> tabs;
// final BarChartViewSelection selectedView;
final AnalyticsSelected defaultSelected;
final AnalyticsSelected? alwaysSelected;
final StudentAnalyticsController? myAnalyticsController;
final List<LanguageModel> targetLanguages;
// final AnalyticsSelected defaultSelected;
// final AnalyticsSelected? alwaysSelected;
// final StudentAnalyticsController? myAnalyticsController;
// final List<LanguageModel> targetLanguages;
BaseAnalyticsPage({
super.key,
required this.pageTitle,
required this.tabs,
required this.alwaysSelected,
required this.defaultSelected,
required this.selectedView,
this.myAnalyticsController,
targetLanguages,
}) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
? targetLanguages
: MatrixState.pangeaController.pLanguageStore.targetOptions;
// BaseAnalyticsPage({
// super.key,
// required this.pageTitle,
// required this.tabs,
// required this.alwaysSelected,
// required this.defaultSelected,
// required this.selectedView,
// this.myAnalyticsController,
// targetLanguages,
// }) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
// ? targetLanguages
// : MatrixState.pangeaController.pLanguageStore.targetOptions;
@override
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
}
// @override
// State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
// }
class BaseAnalyticsController extends State<BaseAnalyticsPage> {
final PangeaController pangeaController = MatrixState.pangeaController;
AnalyticsSelected? selected;
String? currentLemma;
ChartAnalyticsModel? chartData;
StreamController refreshStream = StreamController.broadcast();
BarChartViewSelection currentView = BarChartViewSelection.messages;
// class BaseAnalyticsController extends State<BaseAnalyticsPage> {
// final PangeaController pangeaController = MatrixState.pangeaController;
// AnalyticsSelected? selected;
// String? currentLemma;
// ChartAnalyticsModel? chartData;
// StreamController refreshStream = StreamController.broadcast();
// BarChartViewSelection currentView = BarChartViewSelection.messages;
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
// bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
Room? get activeSpace {
if (widget.defaultSelected.type == AnalyticsEntryType.space) {
return Matrix.of(context).client.getRoomById(widget.defaultSelected.id);
}
return null;
}
// Room? get activeSpace {
// if (widget.defaultSelected.type == AnalyticsEntryType.space) {
// return Matrix.of(context).client.getRoomById(widget.defaultSelected.id);
// }
// return null;
// }
@override
void initState() {
super.initState();
currentView = widget.selectedView;
if (widget.defaultSelected.type == AnalyticsEntryType.student) {
runFirstRefresh();
}
setChartData();
}
// @override
// void initState() {
// super.initState();
// currentView = widget.selectedView;
// if (widget.defaultSelected.type == AnalyticsEntryType.student) {
// runFirstRefresh();
// }
// setChartData();
// }
@override
void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) {
// when a user is a parent space's analytics and clicks on a subspace
super.didUpdateWidget(oldWidget);
if (oldWidget.defaultSelected.id != widget.defaultSelected.id) {
setChartData();
refreshStream.add(false);
}
}
// @override
// void didUpdateWidget(covariant BaseAnalyticsPage oldWidget) {
// // when a user is a parent space's analytics and clicks on a subspace
// super.didUpdateWidget(oldWidget);
// if (oldWidget.defaultSelected.id != widget.defaultSelected.id) {
// setChartData();
// refreshStream.add(false);
// }
// }
Future<void> runFirstRefresh() async {
final analyticsRooms =
pangeaController.matrixState.client.allMyAnalyticsRooms;
// Future<void> runFirstRefresh() async {
// final analyticsRooms =
// pangeaController.matrixState.client.allMyAnalyticsRooms;
final List<AnalyticsEvent> analyticsEvent = [];
for (final analyticsRoom in analyticsRooms) {
final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent(
PangeaEventTypes.summaryAnalytics,
Matrix.of(context).client.userID!,
);
final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent(
PangeaEventTypes.construct,
Matrix.of(context).client.userID!,
);
if (lastSummaryEvent != null) {
analyticsEvent.add(lastSummaryEvent);
}
if (lastConstructEvent != null) {
analyticsEvent.add(lastConstructEvent);
}
}
// final List<AnalyticsEvent> analyticsEvent = [];
// for (final analyticsRoom in analyticsRooms) {
// final lastSummaryEvent = await analyticsRoom.getLastAnalyticsEvent(
// PangeaEventTypes.summaryAnalytics,
// Matrix.of(context).client.userID!,
// );
// final lastConstructEvent = await analyticsRoom.getLastAnalyticsEvent(
// PangeaEventTypes.construct,
// Matrix.of(context).client.userID!,
// );
// if (lastSummaryEvent != null) {
// analyticsEvent.add(lastSummaryEvent);
// }
// if (lastConstructEvent != null) {
// analyticsEvent.add(lastConstructEvent);
// }
// }
if (analyticsEvent.isNotEmpty) return;
onRefresh();
}
// if (analyticsEvent.isNotEmpty) return;
// onRefresh();
// }
Future<void> onRefresh() async {
// postframe callback to avoid calling this function during build
WidgetsBinding.instance.addPostFrameCallback((_) async {
await showFutureLoadingDialog(
context: context,
future: () async {
debugPrint("updating analytics");
await pangeaController.myAnalytics.updateAnalytics();
await setChartData(forceUpdate: true);
refreshStream.add(true);
},
);
});
}
// Future<void> onRefresh() async {
// // postframe callback to avoid calling this function during build
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// await showFutureLoadingDialog(
// context: context,
// future: () async {
// debugPrint("updating analytics");
// await pangeaController.myAnalytics.updateAnalytics();
// await setChartData(forceUpdate: true);
// refreshStream.add(true);
// },
// );
// });
// }
Future<ChartAnalyticsModel> fetchChartData(
AnalyticsSelected? params, {
forceUpdate = false,
}) async {
final ChartAnalyticsModel data =
await pangeaController.analytics.getAnalytics(
defaultSelected: widget.defaultSelected,
selected: params,
forceUpdate: forceUpdate,
);
// Future<ChartAnalyticsModel> fetchChartData(
// AnalyticsSelected? params, {
// forceUpdate = false,
// }) async {
// final ChartAnalyticsModel data =
// await pangeaController.analytics.getAnalytics(
// defaultSelected: widget.defaultSelected,
// selected: params,
// forceUpdate: forceUpdate,
// );
return data;
}
// return data;
// }
Future<void> setChartData({forceUpdate = false}) async {
final ChartAnalyticsModel newData = await fetchChartData(
selected,
forceUpdate: forceUpdate,
);
setState(() => chartData = newData);
}
// Future<void> setChartData({forceUpdate = false}) async {
// final ChartAnalyticsModel newData = await fetchChartData(
// selected,
// forceUpdate: forceUpdate,
// );
// setState(() => chartData = newData);
// }
TimeSpan get currentTimeSpan =>
pangeaController.analytics.currentAnalyticsTimeSpan;
// TimeSpan get currentTimeSpan =>
// pangeaController.analytics.currentAnalyticsTimeSpan;
Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
setState(() {
debugPrint("selectedParam.id is ${selectedParam.id}");
currentLemma = null;
selected = isSelected(selectedParam.id) ? null : selectedParam;
});
await setChartData();
refreshStream.add(false);
Future.delayed(Duration.zero, () => setState(() {}));
}
// Future<void> toggleSelection(AnalyticsSelected selectedParam) async {
// setState(() {
// debugPrint("selectedParam.id is ${selectedParam.id}");
// currentLemma = null;
// selected = isSelected(selectedParam.id) ? null : selectedParam;
// });
// await setChartData();
// refreshStream.add(false);
// Future.delayed(Duration.zero, () => setState(() {}));
// }
Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
await setChartData();
refreshStream.add(false);
}
// Future<void> toggleTimeSpan(BuildContext context, TimeSpan timeSpan) async {
// await pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
// await setChartData();
// refreshStream.add(false);
// }
Future<void> toggleSpaceLang(LanguageModel lang) async {
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
await setChartData();
refreshStream.add(false);
}
// Future<void> toggleSpaceLang(LanguageModel lang) async {
// await pangeaController.analytics.setCurrentAnalyticsLang(lang);
// await setChartData();
// refreshStream.add(false);
// }
Future<void> toggleView(BarChartViewSelection view) async {
currentView = view;
await setChartData();
refreshStream.add(false);
}
// Future<void> toggleView(BarChartViewSelection view) async {
// currentView = view;
// await setChartData();
// refreshStream.add(false);
// }
void setCurrentLemma(String? lemma) {
currentLemma = lemma;
setState(() {});
refreshStream.add(false);
}
// void setCurrentLemma(String? lemma) {
// currentLemma = lemma;
// setState(() {});
// refreshStream.add(false);
// }
@override
Widget build(BuildContext context) {
return BaseAnalyticsView(controller: this);
}
}
// @override
// Widget build(BuildContext context) {
// return BaseAnalyticsView(controller: this);
// }
// }
class TabData {
AnalyticsEntryType type;
IconData icon;
List<TabItem> items;
bool allowNavigateOnSelect;
// class TabData {
// AnalyticsEntryType type;
// IconData icon;
// List<TabItem> items;
// bool allowNavigateOnSelect;
TabData({
required this.type,
required this.items,
required this.icon,
this.allowNavigateOnSelect = true,
});
}
// TabData({
// required this.type,
// required this.items,
// required this.icon,
// this.allowNavigateOnSelect = true,
// });
// }
class TabItem {
Uri? avatar;
String displayName;
String id;
// class TabItem {
// Uri? avatar;
// String displayName;
// String id;
TabItem({required this.avatar, required this.displayName, required this.id});
}
// TabItem({required this.avatar, required this.displayName, required this.id});
// }
enum AnalyticsEntryType { student, room, space, privateChats }

View file

@ -1,243 +1,243 @@
import 'dart:math';
// import 'dart:math';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
import 'package:fluffychat/pangea/pages/analytics/construct_list.dart';
import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart';
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
// import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
// import 'package:fluffychat/pangea/enum/time_span.dart';
// import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
// import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
// import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart';
// import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
// import 'package:fluffychat/pangea/pages/analytics/construct_list.dart';
// import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart';
// import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
// import 'package:fluffychat/widgets/layouts/max_width_body.dart';
// import 'package:flutter/gestures.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
// import 'package:go_router/go_router.dart';
class BaseAnalyticsView extends StatelessWidget {
const BaseAnalyticsView({
super.key,
required this.controller,
});
// class BaseAnalyticsView extends StatelessWidget {
// const BaseAnalyticsView({
// super.key,
// required this.controller,
// });
final BaseAnalyticsController controller;
// final BaseAnalyticsController controller;
Widget chartView(BuildContext context) {
switch (controller.currentView) {
case BarChartViewSelection.messages:
return MessagesBarChart(
chartAnalytics: controller.chartData,
);
case BarChartViewSelection.grammar:
return ConstructList(
constructType: ConstructTypeEnum.grammar,
defaultSelected: controller.widget.defaultSelected,
selected: controller.selected,
controller: controller,
pangeaController: controller.pangeaController,
refreshStream: controller.refreshStream,
);
}
}
// Widget chartView(BuildContext context) {
// switch (controller.currentView) {
// case BarChartViewSelection.messages:
// return MessagesBarChart(
// chartAnalytics: controller.chartData,
// );
// case BarChartViewSelection.grammar:
// return ConstructList(
// constructType: ConstructTypeEnum.grammar,
// defaultSelected: controller.widget.defaultSelected,
// selected: controller.selected,
// controller: controller,
// pangeaController: controller.pangeaController,
// refreshStream: controller.refreshStream,
// );
// }
// }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: RichText(
text: TextSpan(
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 18,
fontWeight: FontWeight.w700,
),
children: [
TextSpan(
text: controller.widget.pageTitle,
style: const TextStyle(decoration: TextDecoration.underline),
recognizer: TapGestureRecognizer()
..onTap = () {
final String route =
"/rooms/${controller.widget.defaultSelected.type.route}";
context.go(route);
},
),
if (controller.activeSpace != null)
const TextSpan(
text: " > ",
),
if (controller.activeSpace != null)
TextSpan(
text: controller.activeSpace!.getLocalizedDisplayname(),
),
const TextSpan(
text: " > ",
),
TextSpan(
text: controller.currentView.string(context),
),
],
),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
body: MaxWidthBody(
withScrolling: false,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TimeSpanMenuButton(
value: controller.currentTimeSpan,
onChange: (TimeSpan value) =>
controller.toggleTimeSpan(context, value),
),
AnalyticsViewButton(
value: controller.currentView,
onChange: controller.toggleView,
),
AnalyticsLanguageButton(
value: controller
.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages: controller.widget.targetLanguages,
),
],
),
const SizedBox(
height: 10,
),
Expanded(
flex: 1,
child: chartView(context),
),
Expanded(
flex: 1,
child: DefaultTabController(
length: 2,
child: Column(
children: [
TabBar(
tabs: [
...controller.widget.tabs.map(
(tab) => Tab(
icon: Icon(
tab.icon,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
],
),
Expanded(
child: SingleChildScrollView(
child: SizedBox(
height: max(
controller.widget.tabs[0].items.length + 1,
controller.widget.tabs[1].items.length,
) *
72,
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...controller.widget.tabs[0].items.map(
(item) => AnalyticsListTile(
refreshStream: controller.refreshStream,
avatar: item.avatar,
defaultSelected:
controller.widget.defaultSelected,
selected: AnalyticsSelected(
item.id,
controller.widget.tabs[0].type,
item.displayName,
),
isSelected:
controller.isSelected(item.id),
onTap: (_) => controller.toggleSelection(
AnalyticsSelected(
item.id,
controller.widget.tabs[0].type,
item.displayName,
),
),
allowNavigateOnSelect: controller
.widget.tabs[0].allowNavigateOnSelect,
pangeaController:
controller.pangeaController,
controller: controller,
),
),
if (controller.widget.defaultSelected.type ==
AnalyticsEntryType.space)
AnalyticsListTile(
refreshStream: controller.refreshStream,
defaultSelected:
controller.widget.defaultSelected,
avatar: null,
selected: AnalyticsSelected(
controller.widget.defaultSelected.id,
AnalyticsEntryType.privateChats,
L10n.of(context)!.allPrivateChats,
),
allowNavigateOnSelect: false,
isSelected: controller.isSelected(
controller.widget.defaultSelected.id,
),
onTap: controller.toggleSelection,
pangeaController:
controller.pangeaController,
controller: controller,
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: controller.widget.tabs[1].items
.map(
(item) => AnalyticsListTile(
refreshStream: controller.refreshStream,
avatar: item.avatar,
defaultSelected:
controller.widget.defaultSelected,
selected: AnalyticsSelected(
item.id,
controller.widget.tabs[1].type,
item.displayName,
),
isSelected:
controller.isSelected(item.id),
onTap: controller.toggleSelection,
allowNavigateOnSelect: controller.widget
.tabs[1].allowNavigateOnSelect,
pangeaController:
controller.pangeaController,
controller: controller,
),
)
.toList(),
),
],
),
),
),
),
],
),
),
),
],
),
),
);
}
}
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// centerTitle: true,
// title: RichText(
// text: TextSpan(
// style: TextStyle(
// color: Theme.of(context).textTheme.bodyLarge!.color,
// fontSize: 18,
// fontWeight: FontWeight.w700,
// ),
// children: [
// TextSpan(
// text: controller.widget.pageTitle,
// style: const TextStyle(decoration: TextDecoration.underline),
// recognizer: TapGestureRecognizer()
// ..onTap = () {
// final String route =
// "/rooms/${controller.widget.defaultSelected.type.route}";
// context.go(route);
// },
// ),
// if (controller.activeSpace != null)
// const TextSpan(
// text: " > ",
// ),
// if (controller.activeSpace != null)
// TextSpan(
// text: controller.activeSpace!.getLocalizedDisplayname(),
// ),
// const TextSpan(
// text: " > ",
// ),
// TextSpan(
// text: controller.currentView.string(context),
// ),
// ],
// ),
// overflow: TextOverflow.ellipsis,
// textAlign: TextAlign.center,
// ),
// ),
// body: MaxWidthBody(
// withScrolling: false,
// child: Column(
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// TimeSpanMenuButton(
// value: controller.currentTimeSpan,
// onChange: (TimeSpan value) =>
// controller.toggleTimeSpan(context, value),
// ),
// AnalyticsViewButton(
// value: controller.currentView,
// onChange: controller.toggleView,
// ),
// AnalyticsLanguageButton(
// value: controller
// .pangeaController.analytics.currentAnalyticsLang,
// onChange: (lang) => controller.toggleSpaceLang(lang),
// languages: controller.widget.targetLanguages,
// ),
// ],
// ),
// const SizedBox(
// height: 10,
// ),
// Expanded(
// flex: 1,
// child: chartView(context),
// ),
// Expanded(
// flex: 1,
// child: DefaultTabController(
// length: 2,
// child: Column(
// children: [
// TabBar(
// tabs: [
// ...controller.widget.tabs.map(
// (tab) => Tab(
// icon: Icon(
// tab.icon,
// color: Theme.of(context)
// .colorScheme
// .onSurfaceVariant,
// ),
// ),
// ),
// ],
// ),
// Expanded(
// child: SingleChildScrollView(
// child: SizedBox(
// height: max(
// controller.widget.tabs[0].items.length + 1,
// controller.widget.tabs[1].items.length,
// ) *
// 72,
// child: TabBarView(
// physics: const NeverScrollableScrollPhysics(),
// children: [
// Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: [
// ...controller.widget.tabs[0].items.map(
// (item) => AnalyticsListTile(
// refreshStream: controller.refreshStream,
// avatar: item.avatar,
// defaultSelected:
// controller.widget.defaultSelected,
// selected: AnalyticsSelected(
// item.id,
// controller.widget.tabs[0].type,
// item.displayName,
// ),
// isSelected:
// controller.isSelected(item.id),
// onTap: (_) => controller.toggleSelection(
// AnalyticsSelected(
// item.id,
// controller.widget.tabs[0].type,
// item.displayName,
// ),
// ),
// allowNavigateOnSelect: controller
// .widget.tabs[0].allowNavigateOnSelect,
// pangeaController:
// controller.pangeaController,
// controller: controller,
// ),
// ),
// if (controller.widget.defaultSelected.type ==
// AnalyticsEntryType.space)
// AnalyticsListTile(
// refreshStream: controller.refreshStream,
// defaultSelected:
// controller.widget.defaultSelected,
// avatar: null,
// selected: AnalyticsSelected(
// controller.widget.defaultSelected.id,
// AnalyticsEntryType.privateChats,
// L10n.of(context)!.allPrivateChats,
// ),
// allowNavigateOnSelect: false,
// isSelected: controller.isSelected(
// controller.widget.defaultSelected.id,
// ),
// onTap: controller.toggleSelection,
// pangeaController:
// controller.pangeaController,
// controller: controller,
// ),
// ],
// ),
// Column(
// crossAxisAlignment: CrossAxisAlignment.stretch,
// children: controller.widget.tabs[1].items
// .map(
// (item) => AnalyticsListTile(
// refreshStream: controller.refreshStream,
// avatar: item.avatar,
// defaultSelected:
// controller.widget.defaultSelected,
// selected: AnalyticsSelected(
// item.id,
// controller.widget.tabs[1].type,
// item.displayName,
// ),
// isSelected:
// controller.isSelected(item.id),
// onTap: controller.toggleSelection,
// allowNavigateOnSelect: controller.widget
// .tabs[1].allowNavigateOnSelect,
// pangeaController:
// controller.pangeaController,
// controller: controller,
// ),
// )
// .toList(),
// ),
// ],
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// ),
// );
// }
// }

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
@ -22,7 +23,7 @@ class ConstructList extends StatefulWidget {
final ConstructTypeEnum constructType;
final AnalyticsSelected defaultSelected;
final AnalyticsSelected? selected;
final BaseAnalyticsController controller;
final TimeSpan timeSpan;
final PangeaController pangeaController;
final StreamController refreshStream;
@ -30,9 +31,9 @@ class ConstructList extends StatefulWidget {
super.key,
required this.constructType,
required this.defaultSelected,
required this.controller,
required this.pangeaController,
required this.refreshStream,
required this.timeSpan,
this.selected,
});
@ -53,11 +54,11 @@ class ConstructListState extends State<ConstructList> {
: Column(
children: [
ConstructListView(
controller: widget.controller,
pangeaController: widget.pangeaController,
defaultSelected: widget.defaultSelected,
selected: widget.selected,
refreshStream: widget.refreshStream,
timeSpan: widget.timeSpan,
),
],
);
@ -74,17 +75,17 @@ class ConstructListState extends State<ConstructList> {
// subtitle = total uses, equal to construct.content.uses.length
// list has a fixed height of 400 and is scrollable
class ConstructListView extends StatefulWidget {
final BaseAnalyticsController controller;
final PangeaController pangeaController;
final AnalyticsSelected defaultSelected;
final TimeSpan timeSpan;
final AnalyticsSelected? selected;
final StreamController refreshStream;
const ConstructListView({
super.key,
required this.controller,
required this.pangeaController,
required this.defaultSelected,
required this.timeSpan,
required this.refreshStream,
this.selected,
});
@ -101,6 +102,7 @@ class ConstructListViewState extends State<ConstructListView> {
bool fetchingConstructs = true;
bool fetchingUses = false;
StreamSubscription? refreshSubscription;
String? currentLemma;
@override
void initState() {
@ -112,6 +114,7 @@ class ConstructListViewState extends State<ConstructListView> {
defaultSelected: widget.defaultSelected,
selected: widget.selected,
forceUpdate: true,
timeSpan: widget.timeSpan,
)
.whenComplete(() => setState(() => fetchingConstructs = false))
.then((value) => setState(() => _constructs = value));
@ -127,6 +130,7 @@ class ConstructListViewState extends State<ConstructListView> {
defaultSelected: widget.defaultSelected,
selected: widget.selected,
forceUpdate: true,
timeSpan: widget.timeSpan,
)
.then(
(value) => setState(() {
@ -143,9 +147,14 @@ class ConstructListViewState extends State<ConstructListView> {
super.dispose();
}
void setCurrentLemma(String? lemma) {
currentLemma = lemma;
setState(() {});
}
int get lemmaIndex =>
constructs?.indexWhere(
(element) => element.lemma == widget.controller.currentLemma,
(element) => element.lemma == currentLemma,
) ??
-1;
@ -258,7 +267,7 @@ class ConstructListViewState extends State<ConstructListView> {
}
ConstructUses? get currentConstruct => constructs?.firstWhereOrNull(
(element) => element.lemma == widget.controller.currentLemma,
(element) => element.lemma == currentLemma,
);
// given the current lemma and list of message events, return a list of
@ -266,7 +275,7 @@ class ConstructListViewState extends State<ConstructListView> {
// this is because some message events may have has more than one PangeaMatch of a
// given lemma type.
List<MessageEventMatch> getMessageEventMatches() {
if (widget.controller.currentLemma == null) return [];
if (currentLemma == null) return [];
final List<MessageEventMatch> allMsgErrorSteps = [];
for (final msgEvent in _msgEvents) {
@ -277,7 +286,7 @@ class ConstructListViewState extends State<ConstructListView> {
}
// get all the pangea matches in that message which have that lemma
final List<PangeaMatch>? msgErrorSteps = msgEvent.errorSteps(
widget.controller.currentLemma!,
currentLemma!,
);
if (msgErrorSteps == null) continue;
@ -327,7 +336,7 @@ class ConstructListViewState extends State<ConstructListView> {
),
onTap: () async {
final String lemma = constructs![index].lemma;
widget.controller.setCurrentLemma(lemma);
setCurrentLemma(lemma);
fetchUses().then((_) => showConstructMessagesDialog());
},
);
@ -346,7 +355,7 @@ class ConstructMessagesDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (controller.widget.controller.currentLemma == null ||
if (controller.currentLemma == null ||
controller.constructs == null ||
controller.lemmaIndex < 0 ||
controller.lemmaIndex >= controller.constructs!.length) {
@ -359,7 +368,7 @@ class ConstructMessagesDialog extends StatelessWidget {
controller._msgEvents.length;
return AlertDialog(
title: Center(child: Text(controller.widget.controller.currentLemma!)),
title: Center(child: Text(controller.currentLemma!)),
content: SizedBox(
height: noData ? 90 : 250,
width: noData ? 200 : 400,
@ -380,7 +389,7 @@ class ConstructMessagesDialog extends StatelessWidget {
children: [
ConstructMessage(
msgEvent: event.msgEvent,
lemma: controller.widget.controller.currentLemma!,
lemma: controller.currentLemma!,
errorMessage: event.lemmaMatch,
),
if (index < msgEventMatches.length - 1)
@ -528,42 +537,37 @@ class ConstructMessageBubble extends StatelessWidget {
vertical: 8,
),
child: RichText(
text: (end == null)
? TextSpan(
text: errorText,
style: defaultStyle,
)
: TextSpan(
children: [
TextSpan(
text: errorText.substring(0, start),
style: defaultStyle,
),
TextSpan(
text: errorText.substring(start, end),
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.red.withOpacity(0.25),
decoration: TextDecoration.lineThrough,
decorationThickness: 2.5,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: replacementText,
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.green.withOpacity(0.25),
),
),
),
TextSpan(
text: errorText.substring(end),
style: defaultStyle,
),
],
text: TextSpan(
children: [
TextSpan(
text: errorText.substring(0, start),
style: defaultStyle,
),
TextSpan(
text: errorText.substring(start, end),
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.red.withOpacity(0.25),
decoration: TextDecoration.lineThrough,
decorationThickness: 2.5,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: replacementText,
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.green.withOpacity(0.25),
),
),
),
TextSpan(
text: errorText.substring(end),
style: defaultStyle,
),
],
),
),
),
),

View file

@ -1,101 +1,101 @@
import 'dart:math';
// import 'dart:math';
import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
// import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../enum/use_type.dart';
// import '../../enum/use_type.dart';
class ListSummaryAnalytics extends StatelessWidget {
final ChartAnalyticsModel? chartAnalytics;
// class ListSummaryAnalytics extends StatelessWidget {
// final ChartAnalyticsModel? chartAnalytics;
const ListSummaryAnalytics({super.key, this.chartAnalytics});
// const ListSummaryAnalytics({super.key, this.chartAnalytics});
TimeSeriesTotals? get totals => chartAnalytics?.totals;
// TimeSeriesTotals? get totals => chartAnalytics?.totals;
String spacer(int baseLength, int number) =>
" " * max(baseLength - number.toString().length, 0);
// String spacer(int baseLength, int number) =>
// " " * max(baseLength - number.toString().length, 0);
WidgetSpan spacerIconText(
String toolTip,
String space,
IconData icon,
int value,
Color? color, [
percentage = true,
]) =>
WidgetSpan(
child: Tooltip(
message: toolTip,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: space,
),
WidgetSpan(child: Icon(icon, size: 14, color: color)),
TextSpan(
text: " $value${percentage ? "%" : ""}",
style: TextStyle(color: color),
),
],
),
),
),
);
// WidgetSpan spacerIconText(
// String toolTip,
// String space,
// IconData icon,
// int value,
// Color? color, [
// percentage = true,
// ]) =>
// WidgetSpan(
// child: Tooltip(
// message: toolTip,
// child: RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: space,
// ),
// WidgetSpan(child: Icon(icon, size: 14, color: color)),
// TextSpan(
// text: " $value${percentage ? "%" : ""}",
// style: TextStyle(color: color),
// ),
// ],
// ),
// ),
// ),
// );
@override
Widget build(BuildContext context) {
if (totals == null) {
return const LinearProgressIndicator();
}
final l10n = L10n.of(context);
// @override
// Widget build(BuildContext context) {
// if (totals == null) {
// return const LinearProgressIndicator();
// }
// final l10n = L10n.of(context);
return RichText(
text: TextSpan(
children: [
spacerIconText(
L10n.of(context) != null
? L10n.of(context)!.totalMessages
: "Total messages sent",
"",
Icons.chat_bubble,
totals!.all,
Theme.of(context).textTheme.bodyLarge!.color,
false,
),
if (totals!.all != 0) ...[
spacerIconText(
l10n != null ? l10n.taTooltip : "With translation assistance",
spacer(8, totals!.all),
UseType.ta.iconData,
totals!.taPercent,
UseType.ta.color(context),
),
spacerIconText(
l10n != null ? l10n.gaTooltip : "With grammar assistance",
spacer(4, totals!.taPercent),
UseType.ga.iconData,
totals!.gaPercent,
UseType.ga.color(context),
),
spacerIconText(
l10n != null ? l10n.waTooltip : "Without assistance",
spacer(4, totals!.gaPercent),
UseType.wa.iconData,
totals!.waPercent,
UseType.wa.color(context),
),
spacerIconText(
l10n != null ? l10n.unTooltip : "Other",
spacer(4, totals!.waPercent),
UseType.un.iconData,
totals!.unPercent,
UseType.un.color(context),
),
],
],
),
);
}
}
// return RichText(
// text: TextSpan(
// children: [
// spacerIconText(
// L10n.of(context) != null
// ? L10n.of(context)!.totalMessages
// : "Total messages sent",
// "",
// Icons.chat_bubble,
// totals!.all,
// Theme.of(context).textTheme.bodyLarge!.color,
// false,
// ),
// if (totals!.all != 0) ...[
// spacerIconText(
// l10n != null ? l10n.taTooltip : "With translation assistance",
// spacer(8, totals!.all),
// UseType.ta.iconData,
// totals!.taPercent,
// UseType.ta.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.gaTooltip : "With grammar assistance",
// spacer(4, totals!.taPercent),
// UseType.ga.iconData,
// totals!.gaPercent,
// UseType.ga.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.waTooltip : "Without assistance",
// spacer(4, totals!.gaPercent),
// UseType.wa.iconData,
// totals!.waPercent,
// UseType.wa.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.unTooltip : "Other",
// spacer(4, totals!.waPercent),
// UseType.un.iconData,
// totals!.unPercent,
// UseType.un.color(context),
// ),
// ],
// ],
// ),
// );
// }
// }

View file

@ -1,402 +1,402 @@
import 'dart:developer';
// import 'dart:developer';
import 'package:fl_chart/fl_chart.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
// import 'package:fl_chart/fl_chart.dart';
// import 'package:fluffychat/config/themes.dart';
// import 'package:fluffychat/pangea/pages/analytics/bar_chart_placeholder_data.dart';
// import 'package:fluffychat/pangea/utils/error_handler.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:intl/intl.dart';
import '../../enum/time_span.dart';
import '../../enum/use_type.dart';
import '../../models/analytics/chart_analytics_model.dart';
import 'bar_chart_card.dart';
import 'messages_legend_widget.dart';
// import '../../enum/time_span.dart';
// import '../../enum/use_type.dart';
// import '../../models/analytics/chart_analytics_model.dart';
// import 'bar_chart_card.dart';
// import 'messages_legend_widget.dart';
class MessagesBarChart extends StatefulWidget {
final ChartAnalyticsModel? chartAnalytics;
// class MessagesBarChart extends StatefulWidget {
// final ChartAnalyticsModel? chartAnalytics;
const MessagesBarChart({
super.key,
required this.chartAnalytics,
});
// const MessagesBarChart({
// super.key,
// required this.chartAnalytics,
// });
@override
State<StatefulWidget> createState() => MessagesBarChartState();
}
// @override
// State<StatefulWidget> createState() => MessagesBarChartState();
// }
class MessagesBarChartState extends State<MessagesBarChart> {
final double barSpace = 16;
final List<List<TimeSeriesInterval>> intervalGroupings = [];
// class MessagesBarChartState extends State<MessagesBarChart> {
// final double barSpace = 16;
// final List<List<TimeSeriesInterval>> intervalGroupings = [];
@override
initState() {
super.initState();
}
// @override
// initState() {
// super.initState();
// }
@override
Widget build(BuildContext context) {
final flLine = FlLine(
color: Theme.of(context).dividerColor,
strokeWidth: 1,
);
// @override
// Widget build(BuildContext context) {
// final flLine = FlLine(
// color: Theme.of(context).dividerColor,
// strokeWidth: 1,
// );
final flTitlesData = FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
getTitlesWidget: bottomTitles,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: leftTitles,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
);
final barChartData = BarChartData(
alignment: BarChartAlignment.spaceEvenly,
barTouchData: BarTouchData(
enabled: false,
),
// barTouchData: barTouchData,
titlesData: flTitlesData,
gridData: FlGridData(
show: true,
// checkToShowHorizontalLine: (value) => value % 10 == 0,
checkToShowHorizontalLine: (value) => true,
getDrawingHorizontalLine: (value) => flLine,
checkToShowVerticalLine: (value) => false,
getDrawingVerticalLine: (value) => flLine,
),
borderData: FlBorderData(
show: false,
),
groupsSpace: barSpace,
barGroups: barChartGroupData,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
);
final barChart = BarChart(
barChartData,
swapAnimationDuration: const Duration(milliseconds: 250),
);
// final flTitlesData = FlTitlesData(
// show: true,
// bottomTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 28,
// getTitlesWidget: bottomTitles,
// ),
// ),
// leftTitles: AxisTitles(
// sideTitles: SideTitles(
// showTitles: true,
// reservedSize: 40,
// getTitlesWidget: leftTitles,
// ),
// ),
// topTitles: const AxisTitles(
// sideTitles: SideTitles(showTitles: false),
// ),
// rightTitles: const AxisTitles(
// sideTitles: SideTitles(showTitles: false),
// ),
// );
// final barChartData = BarChartData(
// alignment: BarChartAlignment.spaceEvenly,
// barTouchData: BarTouchData(
// enabled: false,
// ),
// // barTouchData: barTouchData,
// titlesData: flTitlesData,
// gridData: FlGridData(
// show: true,
// // checkToShowHorizontalLine: (value) => value % 10 == 0,
// checkToShowHorizontalLine: (value) => true,
// getDrawingHorizontalLine: (value) => flLine,
// checkToShowVerticalLine: (value) => false,
// getDrawingVerticalLine: (value) => flLine,
// ),
// borderData: FlBorderData(
// show: false,
// ),
// groupsSpace: barSpace,
// barGroups: barChartGroupData,
// backgroundColor: Theme.of(context).scaffoldBackgroundColor,
// );
// final barChart = BarChart(
// barChartData,
// swapAnimationDuration: const Duration(milliseconds: 250),
// );
return BarChartCard(
barChart: barChart,
loadingData: widget.chartAnalytics == null,
legend: const MessagesLegendsListWidget(),
);
}
// return BarChartCard(
// barChart: barChart,
// loadingData: widget.chartAnalytics == null,
// legend: const MessagesLegendsListWidget(),
// );
// }
bool showLabelBasedOnTimeSpan(
TimeSpan timeSpan,
TimeSeriesInterval current,
TimeSeriesInterval? last,
int labelIndex,
) {
switch (timeSpan) {
case TimeSpan.day:
return current.end.hour % 3 == 0;
case TimeSpan.month:
if (current.end.month != last?.end.month) {
return true;
}
double width = MediaQuery.of(context).size.width;
if (FluffyThemes.isColumnMode(context)) {
width = width - FluffyThemes.navRailWidth - FluffyThemes.columnWidth;
}
const int numDays = 28;
const int minSpacePerDay = 20;
final int availableSpaces = width ~/ minSpacePerDay;
final int showAtInterval = (numDays / availableSpaces).floor() + 1;
// bool showLabelBasedOnTimeSpan(
// TimeSpan timeSpan,
// TimeSeriesInterval current,
// TimeSeriesInterval? last,
// int labelIndex,
// ) {
// switch (timeSpan) {
// case TimeSpan.day:
// return current.end.hour % 3 == 0;
// case TimeSpan.month:
// if (current.end.month != last?.end.month) {
// return true;
// }
// double width = MediaQuery.of(context).size.width;
// if (FluffyThemes.isColumnMode(context)) {
// width = width - FluffyThemes.navRailWidth - FluffyThemes.columnWidth;
// }
// const int numDays = 28;
// const int minSpacePerDay = 20;
// final int availableSpaces = width ~/ minSpacePerDay;
// final int showAtInterval = (numDays / availableSpaces).floor() + 1;
final int lastDayOfCurrentMonth =
DateTime(current.end.year, current.end.month + 1, 0).day;
final bool isNextToMonth = labelIndex == 1 ||
current.end.day == 2 ||
current.end.day == lastDayOfCurrentMonth;
final bool shouldShowNextToMonth = showAtInterval <= 1;
return (current.end.day % showAtInterval == 0) &&
(!isNextToMonth || shouldShowNextToMonth);
case TimeSpan.week:
case TimeSpan.sixmonths:
case TimeSpan.year:
default:
return true;
}
}
// final int lastDayOfCurrentMonth =
// DateTime(current.end.year, current.end.month + 1, 0).day;
// final bool isNextToMonth = labelIndex == 1 ||
// current.end.day == 2 ||
// current.end.day == lastDayOfCurrentMonth;
// final bool shouldShowNextToMonth = showAtInterval <= 1;
// return (current.end.day % showAtInterval == 0) &&
// (!isNextToMonth || shouldShowNextToMonth);
// case TimeSpan.week:
// case TimeSpan.sixmonths:
// case TimeSpan.year:
// default:
// return true;
// }
// }
String getLabelBasedOnTimeSpan(
TimeSpan timeSpan,
TimeSeriesInterval current,
TimeSeriesInterval? last,
int labelIndex,
) {
final bool showLabel = showLabelBasedOnTimeSpan(
timeSpan,
current,
last,
labelIndex,
);
// String getLabelBasedOnTimeSpan(
// TimeSpan timeSpan,
// TimeSeriesInterval current,
// TimeSeriesInterval? last,
// int labelIndex,
// ) {
// final bool showLabel = showLabelBasedOnTimeSpan(
// timeSpan,
// current,
// last,
// labelIndex,
// );
if (widget.chartAnalytics == null || !showLabel) {
return "";
}
if (isInSameGroup(last, current, timeSpan)) {
return "-";
}
// if (widget.chartAnalytics == null || !showLabel) {
// return "";
// }
// if (isInSameGroup(last, current, timeSpan)) {
// return "-";
// }
switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) {
case TimeSpan.day:
return DateFormat(DateFormat.HOUR).format(current.end);
case TimeSpan.week:
return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end);
case TimeSpan.month:
return current.end.month != last?.end.month
? DateFormat(DateFormat.ABBR_MONTH).format(current.end)
: DateFormat(DateFormat.DAY).format(current.end);
case TimeSpan.sixmonths:
case TimeSpan.year:
return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end);
default:
return '';
}
}
// switch (widget.chartAnalytics?.timeSpan ?? TimeSpan.month) {
// case TimeSpan.day:
// return DateFormat(DateFormat.HOUR).format(current.end);
// case TimeSpan.week:
// return DateFormat(DateFormat.ABBR_WEEKDAY).format(current.end);
// case TimeSpan.month:
// return current.end.month != last?.end.month
// ? DateFormat(DateFormat.ABBR_MONTH).format(current.end)
// : DateFormat(DateFormat.DAY).format(current.end);
// case TimeSpan.sixmonths:
// case TimeSpan.year:
// return DateFormat(DateFormat.ABBR_STANDALONE_MONTH).format(current.end);
// default:
// return '';
// }
// }
Widget bottomTitles(double value, TitleMeta meta) {
if (widget.chartAnalytics == null) {
return Container();
}
String text;
final index = value.toInt();
final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month;
final TimeSeriesInterval? last =
index != 0 ? intervalGroupings[index - 1].last : null;
final TimeSeriesInterval current = intervalGroupings[index].last;
// Widget bottomTitles(double value, TitleMeta meta) {
// if (widget.chartAnalytics == null) {
// return Container();
// }
// String text;
// final index = value.toInt();
// final TimeSpan timeSpan = widget.chartAnalytics?.timeSpan ?? TimeSpan.month;
// final TimeSeriesInterval? last =
// index != 0 ? intervalGroupings[index - 1].last : null;
// final TimeSeriesInterval current = intervalGroupings[index].last;
text = getLabelBasedOnTimeSpan(timeSpan, current, last, index);
// text = getLabelBasedOnTimeSpan(timeSpan, current, last, index);
return SideTitleWidget(
axisSide: meta.axisSide,
child: Text(
text,
style: titleTextStyle(context),
),
);
}
// return SideTitleWidget(
// axisSide: meta.axisSide,
// child: Text(
// text,
// style: titleTextStyle(context),
// ),
// );
// }
TextStyle titleTextStyle(context) => TextStyle(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 10,
);
// TextStyle titleTextStyle(context) => TextStyle(
// color: Theme.of(context).textTheme.bodyLarge!.color,
// fontSize: 10,
// );
Widget leftTitles(double value, TitleMeta meta) {
Widget textWidget;
if (value != meta.max) {
textWidget = Text(meta.formattedValue, style: titleTextStyle(context));
} else {
textWidget = const Icon(Icons.chat_bubble, size: 14);
}
return SideTitleWidget(
axisSide: meta.axisSide,
child: textWidget,
);
}
// Widget leftTitles(double value, TitleMeta meta) {
// Widget textWidget;
// if (value != meta.max) {
// textWidget = Text(meta.formattedValue, style: titleTextStyle(context));
// } else {
// textWidget = const Icon(Icons.chat_bubble, size: 14);
// }
// return SideTitleWidget(
// axisSide: meta.axisSide,
// child: textWidget,
// );
// }
bool isInSameGroup(
TimeSeriesInterval? t1,
TimeSeriesInterval t2,
TimeSpan timeSpan,
) {
final DateTime? date1 = t1?.end;
final DateTime date2 = t2.end;
if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) {
return date1?.month == date2.month;
} else if (timeSpan == TimeSpan.week) {
return date1?.day == date2.day;
} else {
return false;
}
}
// bool isInSameGroup(
// TimeSeriesInterval? t1,
// TimeSeriesInterval t2,
// TimeSpan timeSpan,
// ) {
// final DateTime? date1 = t1?.end;
// final DateTime date2 = t2.end;
// if (timeSpan == TimeSpan.sixmonths || timeSpan == TimeSpan.year) {
// return date1?.month == date2.month;
// } else if (timeSpan == TimeSpan.week) {
// return date1?.day == date2.day;
// } else {
// return false;
// }
// }
void makeIntervalGroupings() {
intervalGroupings.clear();
try {
for (final timeSeriesInterval
in widget.chartAnalytics?.timeSeries ?? []) {
//Note: if we decide we'd like to do some sort of grouping in the future,
// this is where that could happen. Currently, we're just putting one
// BarChartRod in each BarChartGroup
final TimeSeriesInterval? last =
intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null;
// void makeIntervalGroupings() {
// intervalGroupings.clear();
// try {
// for (final timeSeriesInterval
// in widget.chartAnalytics?.timeSeries ?? []) {
// //Note: if we decide we'd like to do some sort of grouping in the future,
// // this is where that could happen. Currently, we're just putting one
// // BarChartRod in each BarChartGroup
// final TimeSeriesInterval? last =
// intervalGroupings.isNotEmpty ? intervalGroupings.last.last : null;
if (widget.chartAnalytics != null &&
isInSameGroup(
last,
timeSeriesInterval,
widget.chartAnalytics!.timeSpan,
)) {
intervalGroupings.last.add(timeSeriesInterval);
} else {
intervalGroupings.add([timeSeriesInterval]);
}
}
} catch (err, stack) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: stack);
}
}
// if (widget.chartAnalytics != null &&
// isInSameGroup(
// last,
// timeSeriesInterval,
// widget.chartAnalytics!.timeSpan,
// )) {
// intervalGroupings.last.add(timeSeriesInterval);
// } else {
// intervalGroupings.add([timeSeriesInterval]);
// }
// }
// } catch (err, stack) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(e: err, s: stack);
// }
// }
List<BarChartGroupData> get barChartGroupData {
if (widget.chartAnalytics == null) {
return BarChartPlaceHolderData.getRandomData(context);
}
// List<BarChartGroupData> get barChartGroupData {
// if (widget.chartAnalytics == null) {
// return BarChartPlaceHolderData.getRandomData(context);
// }
makeIntervalGroupings();
// makeIntervalGroupings();
final List<BarChartGroupData> chartData = [];
// final List<BarChartGroupData> chartData = [];
intervalGroupings.asMap().forEach((index, intervalGroup) {
chartData.add(
BarChartGroupData(
x: index,
barsSpace: barSpace,
// barRods: intervalGroup.map(constructBarChartRodData).toList(),
barRods: constructBarChartRodData(intervalGroup),
),
);
});
return chartData;
}
// intervalGroupings.asMap().forEach((index, intervalGroup) {
// chartData.add(
// BarChartGroupData(
// x: index,
// barsSpace: barSpace,
// // barRods: intervalGroup.map(constructBarChartRodData).toList(),
// barRods: constructBarChartRodData(intervalGroup),
// ),
// );
// });
// return chartData;
// }
// BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) {
// final double y1 = timeSeriesInterval.spanIT.toDouble();
// final double y2 =
// (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble();
// final double y3 = timeSeriesInterval.spanTotal.toDouble();
// return BarChartRodData(
// toY: y3,
// width: 10.toDouble(),
// rodStackItems: [
// BarChartRodStackItem(0, y1, UseType.ta.color(context)),
// BarChartRodStackItem(y1, y2, UseType.ga.color(context)),
// BarChartRodStackItem(y2, y3, UseType.wa.color(context)),
// ],
// borderRadius: BorderRadius.zero,
// );
// }
// // BarChartRodData constructBarChartRodData(TimeSeriesInterval timeSeriesInterval) {
// // final double y1 = timeSeriesInterval.spanIT.toDouble();
// // final double y2 =
// // (timeSeriesInterval.spanIT + timeSeriesInterval.spanIGC).toDouble();
// // final double y3 = timeSeriesInterval.spanTotal.toDouble();
// // return BarChartRodData(
// // toY: y3,
// // width: 10.toDouble(),
// // rodStackItems: [
// // BarChartRodStackItem(0, y1, UseType.ta.color(context)),
// // BarChartRodStackItem(y1, y2, UseType.ga.color(context)),
// // BarChartRodStackItem(y2, y3, UseType.wa.color(context)),
// // ],
// // borderRadius: BorderRadius.zero,
// // );
// // }
List<BarChartRodData> constructBarChartRodData(
List<TimeSeriesInterval> timeSeriesIntervalGroup,
) {
int y1 = 0;
int y2 = 0;
int y3 = 0;
int y4 = 0;
for (final e in timeSeriesIntervalGroup) {
y1 += e.totals.ta;
y2 += y1 + e.totals.ga;
y3 += y2 + e.totals.wa;
y4 += y3 + e.totals.un;
}
return [
BarChartRodData(
toY: y4.toDouble(),
width: 10.toDouble(),
rodStackItems: [
BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)),
BarChartRodStackItem(
y1.toDouble(),
y2.toDouble(),
UseType.ga.color(context),
),
BarChartRodStackItem(
y2.toDouble(),
y3.toDouble(),
UseType.wa.color(context),
),
BarChartRodStackItem(
y3.toDouble(),
y4.toDouble(),
UseType.un.color(context),
),
],
borderRadius: BorderRadius.zero,
),
];
}
// List<BarChartRodData> constructBarChartRodData(
// List<TimeSeriesInterval> timeSeriesIntervalGroup,
// ) {
// int y1 = 0;
// int y2 = 0;
// int y3 = 0;
// int y4 = 0;
// for (final e in timeSeriesIntervalGroup) {
// y1 += e.totals.ta;
// y2 += y1 + e.totals.ga;
// y3 += y2 + e.totals.wa;
// y4 += y3 + e.totals.un;
// }
// return [
// BarChartRodData(
// toY: y4.toDouble(),
// width: 10.toDouble(),
// rodStackItems: [
// BarChartRodStackItem(0, y1.toDouble(), UseType.ta.color(context)),
// BarChartRodStackItem(
// y1.toDouble(),
// y2.toDouble(),
// UseType.ga.color(context),
// ),
// BarChartRodStackItem(
// y2.toDouble(),
// y3.toDouble(),
// UseType.wa.color(context),
// ),
// BarChartRodStackItem(
// y3.toDouble(),
// y4.toDouble(),
// UseType.un.color(context),
// ),
// ],
// borderRadius: BorderRadius.zero,
// ),
// ];
// }
// BarTouchData get barTouchData => BarTouchData(
// touchTooltipData: BarTouchTooltipData(
// fitInsideVertically: true,
// tooltipBgColor: Colors.blueGrey,
// getTooltipItem: (group, groupIndex, rod, rodIndex) {
// return BarTooltipItem(
// "groupindex $groupIndex rodIndex $rodIndex",
// const TextStyle(
// color: Colors.white,
// fontWeight: FontWeight.bold,
// fontSize: 18,
// ),
// children: <TextSpan>[
// toolTipText(rod),
// ],
// );
// },
// ),
// // touchCallback: (FlTouchEvent event, barTouchResponse) {
// // setState(() {
// // if (!event.isInterestedForInteractions ||
// // barTouchResponse == null ||
// // barTouchResponse.spot == null) {
// // touchedIndex = -1;
// // return;
// // }
// // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
// // });
// // },
// );
// // BarTouchData get barTouchData => BarTouchData(
// // touchTooltipData: BarTouchTooltipData(
// // fitInsideVertically: true,
// // tooltipBgColor: Colors.blueGrey,
// // getTooltipItem: (group, groupIndex, rod, rodIndex) {
// // return BarTooltipItem(
// // "groupindex $groupIndex rodIndex $rodIndex",
// // const TextStyle(
// // color: Colors.white,
// // fontWeight: FontWeight.bold,
// // fontSize: 18,
// // ),
// // children: <TextSpan>[
// // toolTipText(rod),
// // ],
// // );
// // },
// // ),
// // // touchCallback: (FlTouchEvent event, barTouchResponse) {
// // // setState(() {
// // // if (!event.isInterestedForInteractions ||
// // // barTouchResponse == null ||
// // // barTouchResponse.spot == null) {
// // // touchedIndex = -1;
// // // return;
// // // }
// // // touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
// // // });
// // // },
// // );
// TextSpan toolTipText(BarChartRodData rodData) {
// double rodPercentage(int index) {
// return (rodData.rodStackItems[index].toY -
// rodData.rodStackItems[index].fromY) /
// rodData.toY *
// 100;
// }
// // TextSpan toolTipText(BarChartRodData rodData) {
// // double rodPercentage(int index) {
// // return (rodData.rodStackItems[index].toY -
// // rodData.rodStackItems[index].fromY) /
// // rodData.toY *
// // 100;
// // }
// return TextSpan(
// children: [
// const WidgetSpan(
// child: Icon(Icons.chat_bubble, size: 14),
// ),
// TextSpan(
// text: " ${rodData.toY}",
// ),
// TextSpan(
// text: "/nIT ${rodPercentage(0)}%",
// style: TextStyle(color: UseType.ta.color(context)),
// ),
// TextSpan(
// text: " IGC ${rodPercentage(1)}%",
// style: TextStyle(color: UseType.ga.color(context)),
// ),
// TextSpan(
// text: " Direct ${rodPercentage(2)}%",
// style: TextStyle(color: UseType.wa.color(context)),
// ),
// ],
// );
// }
}
// // return TextSpan(
// // children: [
// // const WidgetSpan(
// // child: Icon(Icons.chat_bubble, size: 14),
// // ),
// // TextSpan(
// // text: " ${rodData.toY}",
// // ),
// // TextSpan(
// // text: "/nIT ${rodPercentage(0)}%",
// // style: TextStyle(color: UseType.ta.color(context)),
// // ),
// // TextSpan(
// // text: " IGC ${rodPercentage(1)}%",
// // style: TextStyle(color: UseType.ga.color(context)),
// // ),
// // TextSpan(
// // text: " Direct ${rodPercentage(2)}%",
// // style: TextStyle(color: UseType.wa.color(context)),
// // ),
// // ],
// // );
// // }
// }

View file

@ -1,121 +1,114 @@
import 'dart:async';
import 'dart:developer';
// import 'dart:async';
// import 'dart:developer';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:matrix/matrix.dart';
// import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
// import 'package:fluffychat/pangea/models/language_model.dart';
// import 'package:fluffychat/pangea/utils/error_handler.dart';
// import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
// import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:go_router/go_router.dart';
// import 'package:matrix/matrix.dart';
import '../../../../widgets/matrix.dart';
import '../../../utils/sync_status_util_v2.dart';
import 'space_analytics_view.dart';
// import '../../../../widgets/matrix.dart';
// import '../../../utils/sync_status_util_v2.dart';
class SpaceAnalyticsPage extends StatefulWidget {
final BarChartViewSelection selectedView;
const SpaceAnalyticsPage({super.key, required this.selectedView});
// class SpaceAnalyticsPage extends StatefulWidget {
// final BarChartViewSelection selectedView;
// const SpaceAnalyticsPage({super.key, required this.selectedView});
@override
State<SpaceAnalyticsPage> createState() => SpaceAnalyticsV2Controller();
}
// @override
// State<SpaceAnalyticsPage> createState() => SpaceAnalyticsV2Controller();
// }
class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
bool _initialized = false;
// StreamSubscription<Event>? stateSub;
// Timer? refreshTimer;
// class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
// bool _initialized = false;
// // StreamSubscription<Event>? stateSub;
// // Timer? refreshTimer;
List<SpaceRoomsChunk> chats = [];
List<User> students = [];
String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
Room? _spaceRoom;
List<LanguageModel> targetLanguages = [];
// List<SpaceRoomsChunk> chats = [];
// List<User> students = [];
// String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
// Room? _spaceRoom;
// List<LanguageModel> targetLanguages = [];
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
context.go('/rooms');
}
getChatAndStudents();
});
}
// @override
// void initState() {
// super.initState();
// Future.delayed(Duration.zero, () async {
// if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
// context.go('/rooms');
// }
// getChatAndStudents();
// });
// }
Room? get spaceRoom {
if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
debugPrint("updating _spaceRoom");
_spaceRoom = spaceId != null
? Matrix.of(context).client.getRoomById(spaceId!)
: null;
if (_spaceRoom == null) {
context.go('/rooms/analytics');
return null;
}
getChatAndStudents().then((_) => setTargetLanguages());
}
return _spaceRoom;
}
// Room? get spaceRoom {
// if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
// debugPrint("updating _spaceRoom");
// _spaceRoom = spaceId != null
// ? Matrix.of(context).client.getRoomById(spaceId!)
// : null;
// if (_spaceRoom == null) {
// context.go('/rooms/analytics');
// return null;
// }
// getChatAndStudents().then((_) => setTargetLanguages());
// }
// return _spaceRoom;
// }
Future<void> getChatAndStudents() async {
try {
await spaceRoom?.requestParticipants();
// Future<void> getChatAndStudents() async {
// try {
// await spaceRoom?.requestParticipants();
if (spaceRoom != null) {
final response = await Matrix.of(context).client.getSpaceHierarchy(
spaceRoom!.id,
);
// if (spaceRoom != null) {
// final response = await Matrix.of(context).client.getSpaceHierarchy(
// spaceRoom!.id,
// );
// set the latest fetched full hierarchy in message analytics controller
// we want to avoid calling this endpoint again and again, so whenever the
// data is made available, set it in the controller
MatrixState.pangeaController.analytics
.setLatestHierarchy(_spaceRoom!.id, response);
// students = spaceRoom!.students;
// chats = response.rooms
// .where(
// (room) =>
// room.roomId != spaceRoom!.id &&
// room.roomType != PangeaRoomTypes.analytics,
// )
// .toList();
// chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
// }
students = spaceRoom!.students;
chats = response.rooms
.where(
(room) =>
room.roomId != spaceRoom!.id &&
room.roomType != PangeaRoomTypes.analytics,
)
.toList();
chats.sort((a, b) => a.roomType == 'm.space' ? -1 : 1);
}
// setState(() {
// _initialized = true;
// });
// } catch (err, s) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(e: err, s: s);
// }
// }
setState(() {
_initialized = true;
});
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
}
}
// Future<void> setTargetLanguages() async {
// // get a list of language models, sorted by the
// // number of students who are learning that language
// targetLanguages = await spaceRoom?.targetLanguages() ?? [];
// setState(() {});
// }
Future<void> setTargetLanguages() async {
// get a list of language models, sorted by the
// number of students who are learning that language
targetLanguages = await spaceRoom?.targetLanguages() ?? [];
setState(() {});
}
@override
Widget build(BuildContext context) {
if (!_initialized) return const PCircular();
return PLoadingStatusV2(
// if we everr want it rebuild the whole thing each time (and run initState again)
// but this is computationally expensive!
// key: UniqueKey(),
shimmerChild: const ListPlaceholder(),
// onFinish: () {
// getChatAndStudentAnalytics(context);
// },
child: SpaceAnalyticsView(this),
);
}
}
// @override
// Widget build(BuildContext context) {
// if (!_initialized) return const PCircular();
// return PLoadingStatusV2(
// // if we everr want it rebuild the whole thing each time (and run initState again)
// // but this is computationally expensive!
// // key: UniqueKey(),
// shimmerChild: const ListPlaceholder(),
// // onFinish: () {
// // getChatAndStudentAnalytics(context);
// // },
// child: SpaceAnalyticsView(this),
// );
// }
// }

View file

@ -1,66 +1,66 @@
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
// import 'package:fluffychat/widgets/matrix.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../base_analytics.dart';
import 'space_analytics.dart';
// import '../base_analytics.dart';
// import 'space_analytics.dart';
class SpaceAnalyticsView extends StatelessWidget {
final SpaceAnalyticsV2Controller controller;
const SpaceAnalyticsView(this.controller, {super.key});
// class SpaceAnalyticsView extends StatelessWidget {
// final SpaceAnalyticsV2Controller controller;
// const SpaceAnalyticsView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final String pageTitle = L10n.of(context)!.spaceAnalytics;
final TabData tab1 = TabData(
type: AnalyticsEntryType.room,
icon: Icons.chat_bubble_outline,
items: controller.chats
.map(
(room) => TabItem(
avatar: room.avatarUrl,
displayName: room.name ??
Matrix.of(context)
.client
.getRoomById(room.roomId)
?.getLocalizedDisplayname() ??
"",
id: room.roomId,
),
)
.toList(),
);
final TabData tab2 = TabData(
type: AnalyticsEntryType.student,
icon: Icons.people_outline,
items: controller.students
.map(
(s) => TabItem(
avatar: s.avatarUrl,
displayName: s.calcDisplayname(),
id: s.id,
),
)
.toList(),
);
// @override
// Widget build(BuildContext context) {
// final String pageTitle = L10n.of(context)!.spaceAnalytics;
// final TabData tab1 = TabData(
// type: AnalyticsEntryType.room,
// icon: Icons.chat_bubble_outline,
// items: controller.chats
// .map(
// (room) => TabItem(
// avatar: room.avatarUrl,
// displayName: room.name ??
// Matrix.of(context)
// .client
// .getRoomById(room.roomId)
// ?.getLocalizedDisplayname() ??
// "",
// id: room.roomId,
// ),
// )
// .toList(),
// );
// final TabData tab2 = TabData(
// type: AnalyticsEntryType.student,
// icon: Icons.people_outline,
// items: controller.students
// .map(
// (s) => TabItem(
// avatar: s.avatarUrl,
// displayName: s.calcDisplayname(),
// id: s.id,
// ),
// )
// .toList(),
// );
return controller.spaceId != null
? BaseAnalyticsPage(
selectedView: controller.widget.selectedView,
pageTitle: pageTitle,
tabs: [tab1, tab2],
alwaysSelected: AnalyticsSelected(
controller.spaceId!,
AnalyticsEntryType.space,
controller.spaceRoom?.name ?? "",
),
defaultSelected: AnalyticsSelected(
controller.spaceId!,
AnalyticsEntryType.space,
controller.spaceRoom?.name ?? "",
),
targetLanguages: controller.targetLanguages,
)
: const SizedBox();
}
}
// return controller.spaceId != null
// ? BaseAnalyticsPage(
// selectedView: controller.widget.selectedView,
// pageTitle: pageTitle,
// tabs: [tab1, tab2],
// alwaysSelected: AnalyticsSelected(
// controller.spaceId!,
// AnalyticsEntryType.space,
// controller.spaceRoom?.name ?? "",
// ),
// defaultSelected: AnalyticsSelected(
// controller.spaceId!,
// AnalyticsEntryType.space,
// controller.spaceRoom?.name ?? "",
// ),
// targetLanguages: controller.targetLanguages,
// )
// : const SizedBox();
// }
// }

View file

@ -1,100 +1,100 @@
import 'dart:async';
// import 'dart:async';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
// import 'package:fluffychat/pangea/enum/time_span.dart';
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
// import 'package:fluffychat/pangea/models/language_model.dart';
// import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
// import 'package:flutter/material.dart';
// import 'package:matrix/matrix.dart';
import '../../../../widgets/matrix.dart';
import '../../../controllers/pangea_controller.dart';
import '../../../utils/sync_status_util_v2.dart';
import '../../../widgets/common/list_placeholder.dart';
// import '../../../../widgets/matrix.dart';
// import '../../../controllers/pangea_controller.dart';
// import '../../../utils/sync_status_util_v2.dart';
// import '../../../widgets/common/list_placeholder.dart';
class AnalyticsSpaceList extends StatefulWidget {
const AnalyticsSpaceList({super.key});
// class AnalyticsSpaceList extends StatefulWidget {
// const AnalyticsSpaceList({super.key});
@override
State<AnalyticsSpaceList> createState() => AnalyticsSpaceListController();
}
// @override
// State<AnalyticsSpaceList> createState() => AnalyticsSpaceListController();
// }
class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
PangeaController pangeaController = MatrixState.pangeaController;
List<Room> spaces = [];
StreamSubscription? stateSub;
List<LanguageModel> targetLanguages = [];
// class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
// PangeaController pangeaController = MatrixState.pangeaController;
// List<Room> spaces = [];
// StreamSubscription? stateSub;
// List<LanguageModel> targetLanguages = [];
@override
void initState() {
super.initState();
setSpaceList().then((_) => setTargetLanguages());
// @override
// void initState() {
// super.initState();
// setSpaceList().then((_) => setTargetLanguages());
// reload dropdowns when their values change in analytics page
stateSub = pangeaController.analytics.stateStream.listen(
(_) => setState(() {}),
);
}
// // reload dropdowns when their values change in analytics page
// stateSub = pangeaController.analytics.stateStream.listen(
// (_) => setState(() {}),
// );
// }
@override
void dispose() {
stateSub?.cancel();
super.dispose();
}
// @override
// void dispose() {
// stateSub?.cancel();
// super.dispose();
// }
StreamController refreshStream = StreamController.broadcast();
// StreamController refreshStream = StreamController.broadcast();
Future<void> setSpaceList() async {
final spaceList = await Matrix.of(context).client.spacesImTeaching;
spaces = spaceList
.where(
(space) => !spaceList.any(
(parentSpace) => parentSpace.spaceChildren
.any((child) => child.roomId == space.id),
),
)
.toList();
setState(() {});
}
// Future<void> setSpaceList() async {
// final spaceList = Matrix.of(context).client.spacesImTeaching;
// spaces = spaceList
// .where(
// (space) => !spaceList.any(
// (parentSpace) => parentSpace.spaceChildren
// .any((child) => child.roomId == space.id),
// ),
// )
// .toList();
// setState(() {});
// }
Future<void> setTargetLanguages() async {
if (spaces.isEmpty) return;
final Map<LanguageModel, int> langCounts = {};
for (final Room space in spaces) {
final List<LanguageModel> targetLangs = await space.targetLanguages();
for (final LanguageModel lang in targetLangs) {
langCounts[lang] ??= 0;
langCounts[lang] = langCounts[lang]! + 1;
}
}
targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
..sort(
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
);
setState(() {});
}
// Future<void> setTargetLanguages() async {
// if (spaces.isEmpty) return;
// final Map<LanguageModel, int> langCounts = {};
// for (final Room space in spaces) {
// final List<LanguageModel> targetLangs = await space.targetLanguages();
// for (final LanguageModel lang in targetLangs) {
// langCounts[lang] ??= 0;
// langCounts[lang] = langCounts[lang]! + 1;
// }
// }
// targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
// ..sort(
// (a, b) => langCounts[b]!.compareTo(langCounts[a]!),
// );
// setState(() {});
// }
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
refreshStream.add(false);
setState(() {});
}
// void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
// pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
// refreshStream.add(false);
// setState(() {});
// }
Future<void> toggleSpaceLang(LanguageModel lang) async {
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
refreshStream.add(false);
setState(() {});
}
// Future<void> toggleSpaceLang(LanguageModel lang) async {
// await pangeaController.analytics.setCurrentAnalyticsLang(lang);
// refreshStream.add(false);
// setState(() {});
// }
@override
Widget build(BuildContext context) {
return PLoadingStatusV2(
shimmerChild: const ListPlaceholder(),
child: AnalyticsSpaceListView(this),
onFinish: () {
// getAllClassAnalytics(context);
},
);
}
}
// @override
// Widget build(BuildContext context) {
// return PLoadingStatusV2(
// shimmerChild: const ListPlaceholder(),
// child: AnalyticsSpaceListView(this),
// onFinish: () {
// // getAllClassAnalytics(context);
// },
// );
// }
// }

View file

@ -1,89 +1,89 @@
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
// import 'package:fluffychat/pangea/enum/time_span.dart';
// import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
// import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
// import 'package:fluffychat/pangea/pages/analytics/time_span_menu_button.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
// import 'package:go_router/go_router.dart';
import '../base_analytics.dart';
import 'space_list.dart';
// import '../base_analytics.dart';
// import 'space_list.dart';
class AnalyticsSpaceListView extends StatelessWidget {
final AnalyticsSpaceListController controller;
const AnalyticsSpaceListView(this.controller, {super.key});
// class AnalyticsSpaceListView extends StatelessWidget {
// final AnalyticsSpaceListController controller;
// const AnalyticsSpaceListView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
L10n.of(context)!.spaceAnalytics,
style: TextStyle(
color: Theme.of(context).textTheme.bodyLarge!.color,
fontSize: 18,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.clip,
textAlign: TextAlign.center,
),
leading: IconButton(
icon: const Icon(Icons.close_outlined),
onPressed: () => context.pop(),
),
),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TimeSpanMenuButton(
value: controller
.pangeaController.analytics.currentAnalyticsTimeSpan,
onChange: (TimeSpan value) => controller.toggleTimeSpan(
context,
value,
),
),
AnalyticsLanguageButton(
value:
controller.pangeaController.analytics.currentAnalyticsLang,
onChange: (lang) => controller.toggleSpaceLang(lang),
languages:
controller.pangeaController.pLanguageStore.targetOptions,
),
],
),
Flexible(
child: ListView.builder(
itemCount: controller.spaces.length,
itemBuilder: (context, i) => AnalyticsListTile(
defaultSelected: AnalyticsSelected(
controller.spaces[i].id,
AnalyticsEntryType.space,
controller.spaces[i].name,
),
avatar: controller.spaces[i].avatar,
selected: AnalyticsSelected(
controller.spaces[i].id,
AnalyticsEntryType.space,
controller.spaces[i].name,
),
onTap: (selected) {
context.go(
'/rooms/analytics/${selected.id}',
);
},
allowNavigateOnSelect: true,
isSelected: false,
pangeaController: controller.pangeaController,
refreshStream: controller.refreshStream,
),
),
),
],
),
);
}
}
// @override
// Widget build(BuildContext context) {
// return Scaffold(
// appBar: AppBar(
// centerTitle: true,
// title: Text(
// L10n.of(context)!.spaceAnalytics,
// style: TextStyle(
// color: Theme.of(context).textTheme.bodyLarge!.color,
// fontSize: 18,
// fontWeight: FontWeight.w700,
// ),
// overflow: TextOverflow.clip,
// textAlign: TextAlign.center,
// ),
// leading: IconButton(
// icon: const Icon(Icons.close_outlined),
// onPressed: () => context.pop(),
// ),
// ),
// body: Column(
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
// TimeSpanMenuButton(
// value: controller
// .pangeaController.analytics.currentAnalyticsTimeSpan,
// onChange: (TimeSpan value) => controller.toggleTimeSpan(
// context,
// value,
// ),
// ),
// AnalyticsLanguageButton(
// value:
// controller.pangeaController.analytics.currentAnalyticsLang,
// onChange: (lang) => controller.toggleSpaceLang(lang),
// languages:
// controller.pangeaController.pLanguageStore.targetOptions,
// ),
// ],
// ),
// Flexible(
// child: ListView.builder(
// itemCount: controller.spaces.length,
// itemBuilder: (context, i) => AnalyticsListTile(
// defaultSelected: AnalyticsSelected(
// controller.spaces[i].id,
// AnalyticsEntryType.space,
// controller.spaces[i].name,
// ),
// avatar: controller.spaces[i].avatar,
// selected: AnalyticsSelected(
// controller.spaces[i].id,
// AnalyticsEntryType.space,
// controller.spaces[i].name,
// ),
// onTap: (selected) {
// context.go(
// '/rooms/analytics/${selected.id}',
// );
// },
// allowNavigateOnSelect: true,
// isSelected: false,
// pangeaController: controller.pangeaController,
// refreshStream: controller.refreshStream,
// ),
// ),
// ),
// ],
// ),
// );
// }
// }

View file

@ -1,90 +1,90 @@
import 'dart:developer';
// import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
// import 'package:fluffychat/pangea/constants/language_constants.dart';
// import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
// import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
// import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
// import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
// import 'package:fluffychat/pangea/models/language_model.dart';
// import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter/material.dart';
// import 'package:matrix/matrix.dart';
import '../../../../widgets/matrix.dart';
import '../../../controllers/pangea_controller.dart';
import '../../../utils/sync_status_util_v2.dart';
import '../base_analytics.dart';
import 'student_analytics_view.dart';
// import '../../../../widgets/matrix.dart';
// import '../../../controllers/pangea_controller.dart';
// import '../../../utils/sync_status_util_v2.dart';
// import '../base_analytics.dart';
// import 'student_analytics_view.dart';
class StudentAnalyticsPage extends StatefulWidget {
final BarChartViewSelection selectedView;
const StudentAnalyticsPage({super.key, required this.selectedView});
// class StudentAnalyticsPage extends StatefulWidget {
// final BarChartViewSelection selectedView;
// const StudentAnalyticsPage({super.key, required this.selectedView});
@override
State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
}
// @override
// State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
// }
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
final PangeaController _pangeaController = MatrixState.pangeaController;
AnalyticsSelected? selected;
// class StudentAnalyticsController extends State<StudentAnalyticsPage> {
// final PangeaController _pangeaController = MatrixState.pangeaController;
// AnalyticsSelected? selected;
@override
void initState() {
super.initState();
}
// @override
// void initState() {
// super.initState();
// }
@override
void dispose() {
super.dispose();
}
// @override
// void dispose() {
// super.dispose();
// }
List<Room> _chats = [];
List<Room> get chats {
if (_chats.isEmpty) {
_pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
setState(() => _chats = result);
});
}
return _chats;
}
// List<Room> _chats = [];
// List<Room> get chats {
// if (_chats.isEmpty) {
// _pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
// setState(() => _chats = result);
// });
// }
// return _chats;
// }
List<Room> get spaces =>
_pangeaController.matrixState.client.spacesImAStudentIn;
// List<Room> get spaces =>
// _pangeaController.matrixState.client.spacesImAStudentIn;
String? get userId {
final id = _pangeaController.matrixState.client.userID;
debugger(when: kDebugMode && id == null);
return id;
}
// String? get userId {
// final id = _pangeaController.matrixState.client.userID;
// debugger(when: kDebugMode && id == null);
// return id;
// }
List<LanguageModel> get targetLanguages {
final LanguageModel? l2 =
_pangeaController.languageController.activeL2Model();
final List<LanguageModel> analyticsRoomLangs =
_pangeaController.matrixState.client.allMyAnalyticsRooms
.map((analyticsRoom) => analyticsRoom.madeForLang)
.where((langCode) => langCode != null)
.map((langCode) => PangeaLanguage.byLangCode(langCode!))
.where(
(langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
)
.toList();
if (l2 != null) {
analyticsRoomLangs.add(l2);
}
return analyticsRoomLangs.toSet().toList();
}
// List<LanguageModel> get targetLanguages {
// final LanguageModel? l2 =
// _pangeaController.languageController.activeL2Model();
// final List<LanguageModel> analyticsRoomLangs =
// _pangeaController.matrixState.client.allMyAnalyticsRooms
// .map((analyticsRoom) => analyticsRoom.madeForLang)
// .where((langCode) => langCode != null)
// .map((langCode) => PangeaLanguage.byLangCode(langCode!))
// .where(
// (langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
// )
// .toList();
// if (l2 != null) {
// analyticsRoomLangs.add(l2);
// }
// return analyticsRoomLangs.toSet().toList();
// }
@override
Widget build(BuildContext context) {
return PLoadingStatusV2(
// if we everr want it rebuild the whole thing each time (and run initState again)
// but this is computationally expensive!
// key: UniqueKey(),
shimmerChild: const ListPlaceholder(),
// onFinish: initialize,
child: StudentAnalyticsView(this),
);
}
}
// @override
// Widget build(BuildContext context) {
// return PLoadingStatusV2(
// // if we everr want it rebuild the whole thing each time (and run initState again)
// // but this is computationally expensive!
// // key: UniqueKey(),
// shimmerChild: const ListPlaceholder(),
// // onFinish: initialize,
// child: StudentAnalyticsView(this),
// );
// }
// }

View file

@ -1,66 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
import '../base_analytics.dart';
import 'student_analytics.dart';
// import '../../../../utils/matrix_sdk_extensions/matrix_locals.dart';
// import '../base_analytics.dart';
// import 'student_analytics.dart';
class StudentAnalyticsView extends StatelessWidget {
final StudentAnalyticsController controller;
const StudentAnalyticsView(this.controller, {super.key});
// class StudentAnalyticsView extends StatelessWidget {
// final StudentAnalyticsController controller;
// const StudentAnalyticsView(this.controller, {super.key});
@override
Widget build(BuildContext context) {
final String pageTitle = L10n.of(context)!.myLearning;
final TabData chatTabData = TabData(
type: AnalyticsEntryType.room,
icon: Icons.chat_bubble_outline,
items: (controller.chats)
.map(
(c) => TabItem(
avatar: c.avatar,
displayName:
c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
id: c.id,
),
)
.toList(),
allowNavigateOnSelect: false,
);
final TabData classTabData = TabData(
type: AnalyticsEntryType.space,
icon: Icons.workspaces,
items: (controller.spaces ?? [])
.map(
(c) => TabItem(
avatar: c.avatar,
displayName:
c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
id: c.id,
),
)
.toList(),
allowNavigateOnSelect: false,
);
// @override
// Widget build(BuildContext context) {
// final String pageTitle = L10n.of(context)!.myLearning;
// final TabData chatTabData = TabData(
// type: AnalyticsEntryType.room,
// icon: Icons.chat_bubble_outline,
// items: (controller.chats)
// .map(
// (c) => TabItem(
// avatar: c.avatar,
// displayName:
// c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
// id: c.id,
// ),
// )
// .toList(),
// allowNavigateOnSelect: false,
// );
// final TabData classTabData = TabData(
// type: AnalyticsEntryType.space,
// icon: Icons.workspaces,
// items: (controller.spaces ?? [])
// .map(
// (c) => TabItem(
// avatar: c.avatar,
// displayName:
// c.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
// id: c.id,
// ),
// )
// .toList(),
// allowNavigateOnSelect: false,
// );
return controller.userId != null
? BaseAnalyticsPage(
selectedView: controller.widget.selectedView,
pageTitle: pageTitle,
tabs: [chatTabData, classTabData],
alwaysSelected: AnalyticsSelected(
controller.userId!,
AnalyticsEntryType.student,
L10n.of(context)!.allChatsAndClasses,
),
myAnalyticsController: controller,
defaultSelected: AnalyticsSelected(
controller.userId!,
AnalyticsEntryType.student,
L10n.of(context)!.allChatsAndClasses,
),
targetLanguages: controller.targetLanguages,
)
: const SizedBox();
}
}
// return controller.userId != null
// ? BaseAnalyticsPage(
// selectedView: controller.widget.selectedView,
// pageTitle: pageTitle,
// tabs: [chatTabData, classTabData],
// alwaysSelected: AnalyticsSelected(
// controller.userId!,
// AnalyticsEntryType.student,
// L10n.of(context)!.allChatsAndClasses,
// ),
// myAnalyticsController: controller,
// defaultSelected: AnalyticsSelected(
// controller.userId!,
// AnalyticsEntryType.student,
// L10n.of(context)!.allChatsAndClasses,
// ),
// targetLanguages: controller.targetLanguages,
// )
// : const SizedBox();
// }
// }

View file

@ -0,0 +1,195 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
/// A summary of "My Analytics" shown at the top of the chat list
/// It shows a variety of progress indicators such as
/// messages sent, words used, and error types, which can
/// be clicked to access more fine-grained analytics data.
class LearningProgressIndicators extends StatefulWidget {
const LearningProgressIndicators({
super.key,
});
@override
LearningProgressIndicatorsState createState() =>
LearningProgressIndicatorsState();
}
class LearningProgressIndicatorsState
extends State<LearningProgressIndicators> {
final PangeaController _pangeaController = MatrixState.pangeaController;
int? wordsUsed;
int? errorTypes;
@override
void initState() {
super.initState();
setData();
}
AnalyticsSelected get defaultSelected => AnalyticsSelected(
_pangeaController.matrixState.client.userID!,
AnalyticsEntryType.student,
"",
);
Future<void> setData() async {
await getNumLemmasUsed();
setState(() {});
}
Future<void> getNumLemmasUsed() async {
final constructs = await _pangeaController.analytics.getConstructs(
defaultSelected: defaultSelected,
timeSpan: TimeSpan.forever,
);
if (constructs == null) {
errorTypes = 0;
wordsUsed = 0;
return;
}
final List<String> errorLemmas = [];
final List<String> vocabLemmas = [];
for (final event in constructs) {
for (final use in event.content.uses) {
if (use.lemma == null) continue;
switch (use.constructType) {
case ConstructTypeEnum.grammar:
errorLemmas.add(use.lemma!);
break;
case ConstructTypeEnum.vocab:
vocabLemmas.add(use.lemma!);
break;
default:
break;
}
}
}
errorTypes = errorLemmas.toSet().length;
wordsUsed = vocabLemmas.toSet().length;
}
int? getProgressPoints(ProgressIndicatorEnum indicator) {
switch (indicator) {
case ProgressIndicatorEnum.wordsUsed:
return wordsUsed;
case ProgressIndicatorEnum.errorTypes:
return errorTypes;
case ProgressIndicatorEnum.level:
return level;
}
}
int get xpPoints {
final points = [
wordsUsed ?? 0,
errorTypes ?? 0,
];
return points.reduce((a, b) => a + b);
}
int get level => xpPoints ~/ 100;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FutureBuilder(
future:
_pangeaController.matrixState.client.getProfileFromUserId(
_pangeaController.matrixState.client.userID!,
),
builder: (context, snapshot) {
final mxid = Matrix.of(context).client.userID ??
L10n.of(context)!.user;
return Avatar(
name: snapshot.data?.displayName ?? mxid.localpart ?? mxid,
mxContent: snapshot.data?.avatarUrl,
);
},
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: ProgressIndicatorEnum.values
.where(
(indicator) => indicator != ProgressIndicatorEnum.level,
)
.map(
(indicator) => ProgressIndicatorBadge(
points: getProgressPoints(indicator),
onTap: () {},
progressIndicator: indicator,
),
)
.toList(),
),
),
],
),
const SizedBox(height: 4),
SizedBox(
height: 35,
child: Stack(
alignment: Alignment.centerLeft,
children: [
Positioned(
right: 0,
child: Row(
children: [
SizedBox(
width: FluffyThemes.columnWidth - (36 * 2) - 25,
child: LinearProgressIndicator(
value: (xpPoints % 100) / 100,
color: Theme.of(context).colorScheme.primary,
backgroundColor:
Theme.of(context).colorScheme.onPrimary,
minHeight: 15,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
),
],
),
),
Positioned(
left: 0,
child: CircleAvatar(
backgroundColor: "$level $xpPoints".lightColorAvatar,
radius: 16,
child: Text(
"$level",
style: const TextStyle(color: Colors.white),
),
),
),
],
),
),
],
),
);
}
}

View file

@ -0,0 +1,51 @@
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
import 'package:flutter/material.dart';
/// A badge that represents one learning progress indicator (i.e., construct uses)
class ProgressIndicatorBadge extends StatelessWidget {
final int? points;
final VoidCallback onTap;
final ProgressIndicatorEnum progressIndicator;
const ProgressIndicatorBadge({
super.key,
required this.points,
required this.onTap,
required this.progressIndicator,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
child: Tooltip(
message: progressIndicator.tooltip(context),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
progressIndicator.icon,
color: progressIndicator.color(context),
),
const SizedBox(width: 5),
points != null
? Text(
points.toString(),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
)
: const CircularProgressIndicator.adaptive(),
],
),
),
),
),
);
}
}

View file

@ -139,7 +139,6 @@ abstract class ClientManager {
timeline: StateFilter(
notTypes: [
PangeaEventTypes.construct,
PangeaEventTypes.summaryAnalytics,
],
),
),