diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 598c2b562..b5944b175 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -973,33 +973,13 @@ class ChatController extends State // There's a listen in my_analytics_controller that decides when to auto-update // analytics based on when / how many messages the logged in user send. This // stream sends the data for newly sent messages. - final metadata = ConstructUseMetaData( - roomId: roomId, - timeStamp: DateTime.now(), - eventId: msgEventId, + _sendMessageAnalytics( + msgEventId, + originalSent: originalSent, + tokensSent: tokensSent, + choreo: choreo, ); - if (msgEventId != null && originalSent != null && tokensSent != null) { - final List constructs = [ - ...originalSent.vocabAndMorphUses( - choreo: choreo, - tokens: tokensSent.tokens, - metadata: metadata, - ), - ]; - - _showAnalyticsFeedback(constructs, msgEventId); - - pangeaController.putAnalytics.setState( - AnalyticsStream( - eventId: msgEventId, - targetID: msgEventId, - roomId: room.id, - constructs: constructs, - ), - ); - } - if (previousEdit != null) { pangeaEditingEvent = previousEdit; } @@ -2091,6 +2071,48 @@ class ChatController extends State } } + Future _sendMessageAnalytics( + String? eventId, { + PangeaRepresentation? originalSent, + PangeaMessageTokens? tokensSent, + ChoreoRecord? choreo, + }) async { + // There's a listen in my_analytics_controller that decides when to auto-update + // analytics based on when / how many messages the logged in user send. This + // stream sends the data for newly sent messages. + if (originalSent?.langCode.split("-").first != + choreographer.l2Lang?.langCodeShort) { + return; + } + + final metadata = ConstructUseMetaData( + roomId: roomId, + timeStamp: DateTime.now(), + eventId: eventId, + ); + + if (eventId != null && originalSent != null && tokensSent != null) { + final List constructs = [ + ...originalSent.vocabAndMorphUses( + choreo: choreo, + tokens: tokensSent.tokens, + metadata: metadata, + ), + ]; + + _showAnalyticsFeedback(constructs, eventId); + + pangeaController.putAnalytics.setState( + AnalyticsStream( + eventId: eventId, + targetID: eventId, + roomId: room.id, + constructs: constructs, + ), + ); + } + } + Future _sendVoiceMessageAnalytics(String? eventId) async { if (eventId == null) { ErrorHandler.logError( diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 853da6a85..5a43aed3d 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -12,9 +12,9 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/chat_settings/pages/pangea_chat_details.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/download/download_room_extension.dart'; +import 'package:fluffychat/pangea/download/download_type_enum.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/file_selector.dart'; @@ -238,7 +238,26 @@ class ChatDetailsController extends State { ], ); if (type == null) return; - downloadChat(room, type, context); + + try { + await room.download(type, context); + } on EmptyChatException { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context).emptyChatDownloadWarning, + ), + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}", + ), + ), + ); + } } Future setBotOptions(BotOptionsModel botOptions) async { diff --git a/lib/pangea/activity_sessions/activity_analytics_chip.dart b/lib/pangea/activity_sessions/activity_analytics_chip.dart new file mode 100644 index 000000000..afeb7d792 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_analytics_chip.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class ActivityAnalyticsChip extends StatelessWidget { + final IconData icon; + final String text; + const ActivityAnalyticsChip( + this.icon, + this.text, { + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12.0), + Text( + text, + style: const TextStyle( + fontSize: 12.0, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_finished_status_message.dart b/lib/pangea/activity_sessions/activity_finished_status_message.dart index 6025e3e70..7feb2fbcd 100644 --- a/lib/pangea/activity_sessions/activity_finished_status_message.dart +++ b/lib/pangea/activity_sessions/activity_finished_status_message.dart @@ -7,10 +7,13 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_participant_indicator.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_results_carousel.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -103,6 +106,21 @@ class ActivityFinishedStatusMessage extends StatelessWidget { ), ), ), + if (summary.analytics != null) + Row( + spacing: 8.0, + mainAxisSize: MainAxisSize.min, + children: [ + ActivityAnalyticsChip( + ConstructTypeEnum.vocab.indicator.icon, + "${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.vocab)}", + ), + ActivityAnalyticsChip( + ConstructTypeEnum.morph.indicator.icon, + "${summary.analytics!.uniqueConstructCount(ConstructTypeEnum.morph)}", + ), + ], + ), const SizedBox(height: 16.0), if (_highlightedRole != null && userSummary != null) ClipRRect( @@ -114,9 +132,11 @@ class ActivityFinishedStatusMessage extends StatelessWidget { child: Column( children: [ ActivityResultsCarousel( + userId: _highlightedRole!.userId, selectedRole: _highlightedRole!, user: user, summary: userSummary, + analytics: summary.analytics, ), Wrap( alignment: WrapAlignment.center, diff --git a/lib/pangea/activity_sessions/activity_results_carousel.dart b/lib/pangea/activity_sessions/activity_results_carousel.dart index 13bdfe17e..677ccceb0 100644 --- a/lib/pangea/activity_sessions/activity_results_carousel.dart +++ b/lib/pangea/activity_sessions/activity_results_carousel.dart @@ -3,19 +3,27 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_analytics_chip.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; class ActivityResultsCarousel extends StatelessWidget { + final String userId; final ActivityRoleModel selectedRole; final ParticipantSummaryModel summary; + final ActivitySummaryAnalyticsModel? analytics; final User? user; const ActivityResultsCarousel({ super.key, + required this.userId, required this.selectedRole, required this.summary, + required this.analytics, this.user, }); @@ -48,25 +56,19 @@ class ActivityResultsCarousel extends StatelessWidget { spacing: 8.0, runSpacing: 8.0, children: [ - Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(20), + if (analytics != null) + ActivityAnalyticsChip( + ConstructTypeEnum.vocab.indicator.icon, + "${analytics!.uniqueConstructCountForUser(userId, ConstructTypeEnum.vocab)}", ), - child: Row( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.school, size: 12.0), - Text( - summary.cefrLevel, - style: const TextStyle( - fontSize: 12.0, - ), - ), - ], + if (analytics != null) + ActivityAnalyticsChip( + ConstructTypeEnum.morph.indicator.icon, + "${analytics!.uniqueConstructCountForUser(userId, ConstructTypeEnum.morph)}", ), + ActivityAnalyticsChip( + Icons.school, + summary.cefrLevel, ), ...summary.superlatives.map( (sup) => Container( diff --git a/lib/pangea/activity_sessions/activity_room_extension.dart b/lib/pangea/activity_sessions/activity_room_extension.dart index 2770c072c..1c6c32d81 100644 --- a/lib/pangea/activity_sessions/activity_room_extension.dart +++ b/lib/pangea/activity_sessions/activity_room_extension.dart @@ -8,15 +8,16 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_role_model.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_roles_model.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_model.dart'; -import 'package:fluffychat/pangea/activity_summary/activity_summary_repo.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_request_model.dart'; import 'package:fluffychat/pangea/bot/utils/bot_name.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; +import '../activity_summary/activity_summary_repo.dart'; extension ActivityRoomExtension on Room { Future sendActivityPlan( @@ -119,7 +120,9 @@ extension ActivityRoomExtension on Room { } Future fetchSummaries() async { - if (activitySummary?.summary != null) return; + if (activitySummary?.summary != null) { + return; + } await setActivitySummary( ActivitySummaryModel( @@ -128,15 +131,18 @@ extension ActivityRoomExtension on Room { ), ); - final events = await getAllEvents(this); + final events = await getAllEvents(); final List messages = []; + final ActivitySummaryAnalyticsModel analytics = + ActivitySummaryAnalyticsModel(); + + final timeline = this.timeline ?? await getTimeline(); for (final event in events) { if (event.type != EventTypes.Message || event.messageType != MessageTypes.Text) { continue; } - final timeline = this.timeline ?? await getTimeline(); final pangeaMessage = PangeaMessageEvent( event: event, timeline: timeline, @@ -155,6 +161,7 @@ extension ActivityRoomExtension on Room { ); messages.add(activityMessage); + analytics.addConstructs(pangeaMessage); } try { @@ -163,11 +170,15 @@ extension ActivityRoomExtension on Room { activity: activityPlan!, activityResults: messages, contentFeedback: [], + analytics: analytics, ), ); await setActivitySummary( - ActivitySummaryModel(summary: resp), + ActivitySummaryModel( + summary: resp, + analytics: analytics, + ), ); } catch (e, s) { ErrorHandler.logError( diff --git a/lib/pangea/activity_summary/activity_summary_analytics_model.dart b/lib/pangea/activity_summary/activity_summary_analytics_model.dart new file mode 100644 index 000000000..5b84b2f95 --- /dev/null +++ b/lib/pangea/activity_summary/activity_summary_analytics_model.dart @@ -0,0 +1,110 @@ +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; + +class ActivitySummaryAnalyticsModel { + final Map constructs = {}; + + ActivitySummaryAnalyticsModel(); + + Map uniqueConstructCountsByType() { + final Map> typeToIds = {}; + + for (final userAnalytics in constructs.values) { + for (final usage in userAnalytics.usages.values) { + final id = usage.identifier; + typeToIds.putIfAbsent(id.type, () => {}).add(id); + } + } + + return { + for (final entry in typeToIds.entries) entry.key: entry.value.length, + }; + } + + int uniqueConstructCount(ConstructTypeEnum type) => + uniqueConstructCountsByType()[type] ?? 0; + + /// Unique constructs of a given type for a specific user + int uniqueConstructCountForUser(String userId, ConstructTypeEnum type) { + final userAnalytics = constructs[userId]; + if (userAnalytics == null) return 0; + return userAnalytics.constructsOfType(type).length; + } + + void addConstructs(PangeaMessageEvent event) { + final uses = event.originalSent?.vocabAndMorphUses(); + if (uses == null || uses.isEmpty) return; + + final user = + constructs[event.senderId] ??= UserConstructAnalytics(event.senderId); + + for (final use in uses) { + user.addUsage(use.identifier); + } + } + + factory ActivitySummaryAnalyticsModel.fromJson(Map json) { + final model = ActivitySummaryAnalyticsModel(); + + for (final userEntry in json.entries) { + final userId = userEntry.key; + final constructList = userEntry.value as List; + + final userAnalytics = UserConstructAnalytics(userId); + + for (final constructJson in constructList) { + final constructId = ConstructIdentifier.fromJson(constructJson); + final timesUsed = constructJson['times_used'] as int? ?? 0; + + final usage = ConstructUsage(constructId)..timesUsed = timesUsed; + userAnalytics.usages[constructId.string] = usage; + } + + model.constructs[userId] = userAnalytics; + } + + return model; + } + + Map toJson() => { + for (final entry in constructs.entries) + entry.key: entry.value.toJsonList(), + }; +} + +class ConstructUsage { + final ConstructIdentifier identifier; + int timesUsed; + + ConstructUsage(this.identifier) : timesUsed = 0; + + void increment() => timesUsed++; + + Map toJson() => { + ...identifier.toJson(), + 'times_used': timesUsed, + }; +} + +class UserConstructAnalytics { + final String userId; + final Map usages; + + UserConstructAnalytics(this.userId) : usages = {}; + + /// Unique constructs of a given type + Set constructsOfType(ConstructTypeEnum type) => + usages.values + .map((u) => u.identifier) + .where((id) => id.type == type) + .toSet(); + + void addUsage(ConstructIdentifier id) { + usages[id.string] ??= ConstructUsage(id); + usages[id.string]!.increment(); + } + + List> toJsonList() => + usages.values.map((u) => u.toJson()).toList(); +} diff --git a/lib/pangea/activity_summary/activity_summary_model.dart b/lib/pangea/activity_summary/activity_summary_model.dart index 5f1a3055f..b3e350556 100644 --- a/lib/pangea/activity_summary/activity_summary_model.dart +++ b/lib/pangea/activity_summary/activity_summary_model.dart @@ -1,14 +1,17 @@ +import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; class ActivitySummaryModel { final ActivitySummaryResponseModel? summary; final DateTime? requestedAt; final DateTime? errorAt; + final ActivitySummaryAnalyticsModel? analytics; ActivitySummaryModel({ this.summary, this.requestedAt, this.errorAt, + this.analytics, }); Map toJson() { @@ -16,6 +19,7 @@ class ActivitySummaryModel { "summary": summary?.toJson(), "requested_at": requestedAt?.toIso8601String(), "error_at": errorAt?.toIso8601String(), + "analytics": analytics?.toJson(), }; } @@ -29,6 +33,9 @@ class ActivitySummaryModel { : null, errorAt: json['error_at'] != null ? DateTime.parse(json['error_at']) : null, + analytics: json['analytics'] != null + ? ActivitySummaryAnalyticsModel.fromJson(json['analytics']) + : null, ); } diff --git a/lib/pangea/activity_summary/activity_summary_request_model.dart b/lib/pangea/activity_summary/activity_summary_request_model.dart index b0c3cb8ca..7ca143565 100644 --- a/lib/pangea/activity_summary/activity_summary_request_model.dart +++ b/lib/pangea/activity_summary/activity_summary_request_model.dart @@ -1,6 +1,7 @@ // Add this import for the participant summary model import 'package:fluffychat/pangea/activity_planner/activity_plan_model.dart'; +import 'package:fluffychat/pangea/activity_summary/activity_summary_analytics_model.dart'; import 'package:fluffychat/pangea/activity_summary/activity_summary_response_model.dart'; class ActivitySummaryResultsMessage { @@ -67,11 +68,13 @@ class ActivitySummaryRequestModel { final ActivityPlanModel activity; final List activityResults; final List contentFeedback; + final ActivitySummaryAnalyticsModel analytics; ActivitySummaryRequestModel({ required this.activity, required this.activityResults, required this.contentFeedback, + required this.analytics, }); Map toJson() { @@ -79,6 +82,7 @@ class ActivitySummaryRequestModel { 'activity': activity.toJson(), 'activity_results': activityResults.map((e) => e.toJson()).toList(), 'content_feedback': contentFeedback.map((e) => e.toJson()).toList(), + 'analytics': analytics.toJson(), }; } } diff --git a/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart b/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart index c17bdab1d..b18cb3f8c 100644 --- a/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart +++ b/lib/pangea/analytics_downloads/analytics_dowload_dialog.dart @@ -15,10 +15,11 @@ import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/download/download_file_util.dart'; +import 'package:fluffychat/pangea/download/download_type_enum.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; @@ -101,12 +102,12 @@ class AnalyticsDownloadDialogState extends State { "analytics_morph_${MatrixState.pangeaController.matrixState.client.userID?.localpart}_${DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now())}.csv"; final futures = [ - downloadFile( + DownloadUtil.downloadFile( vocabContent, vocabFileName, _downloadType, ), - downloadFile( + DownloadUtil.downloadFile( morphContent, morphFileName, _downloadType, @@ -123,7 +124,7 @@ class AnalyticsDownloadDialogState extends State { final fileName = "analytics_${MatrixState.pangeaController.matrixState.client.userID?.localpart}_${DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now())}.xlsx'}"; - await downloadFile( + await DownloadUtil.downloadFile( content, fileName, _downloadType, diff --git a/lib/pangea/chat_settings/utils/download_chat.dart b/lib/pangea/chat_settings/utils/download_chat.dart deleted file mode 100644 index f79bfdf4a..000000000 --- a/lib/pangea/chat_settings/utils/download_chat.dart +++ /dev/null @@ -1,338 +0,0 @@ -// ignore_for_file: implementation_imports - -import 'dart:async'; - -import 'package:flutter/material.dart'; - -import 'package:csv/csv.dart'; -import 'package:excel/excel.dart'; -import 'package:intl/intl.dart'; -import 'package:matrix/matrix.dart'; -import 'package:matrix/src/models/timeline_chunk.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; - -Future downloadChat( - Room room, - DownloadType type, - BuildContext context, -) async { - List allPangeaMessages; - - try { - final List allEvents = await getAllEvents(room); - final TimelineChunk chunk = TimelineChunk(events: allEvents); - final Timeline timeline = Timeline( - room: room, - chunk: chunk, - ); - - allPangeaMessages = getPangeaMessageEvents( - allEvents, - timeline, - room, - ); - } catch (err, s) { - ErrorHandler.logError( - e: err, - s: s, - data: { - "roomID": room.id, - }, - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "${L10n.of(context).oopsSomethingWentWrong} ${L10n.of(context).errorPleaseRefresh}", - ), - ), - ); - return; - } - - if (allPangeaMessages.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - L10n.of(context).emptyChatDownloadWarning, - ), - ), - ); - return; - } - - final String filename = getFilename(room, type); - - switch (type) { - case DownloadType.txt: - final String content = - getTxtContent(allPangeaMessages, context, filename, room); - downloadFile(content, filename, DownloadType.txt); - break; - case DownloadType.csv: - final String content = - getCSVContent(allPangeaMessages, context, filename); - downloadFile(content, filename, DownloadType.csv); - return; - case DownloadType.xlsx: - final List content = - getExcelContent(allPangeaMessages, context, filename); - downloadFile(content, filename, DownloadType.xlsx); - return; - } -} - -Future> getAllEvents(Room room) async { - final GetRoomEventsResponse initalResp = - await room.client.getRoomEvents(room.id, Direction.b); - if (initalResp.end == null) return []; - String? nextStartToken = initalResp.end; - List allMatrixEvents = initalResp.chunk; - while (nextStartToken != null) { - final GetRoomEventsResponse resp = await room.client.getRoomEvents( - room.id, - Direction.b, - from: nextStartToken, - ); - final chunkMessages = resp.chunk; - allMatrixEvents.addAll(chunkMessages); - resp.end != nextStartToken - ? nextStartToken = resp.end - : nextStartToken = null; - } - allMatrixEvents = allMatrixEvents.reversed.toList(); - final List allEvents = allMatrixEvents - .map((MatrixEvent message) => Event.fromMatrixEvent(message, room)) - .toList(); - return allEvents; -} - -List getPangeaMessageEvents( - List events, - Timeline timeline, - Room room, -) { - final List allPangeaMessages = events - .where( - (Event event) => - event.type == EventTypes.Message && - event.content['msgtype'] == MessageTypes.Text, - ) - .map( - (Event message) => PangeaMessageEvent( - event: message, - timeline: timeline, - ownMessage: false, - ), - ) - .cast() - .toList(); - return allPangeaMessages; -} - -String getSentText(PangeaMessageEvent message) => - message.originalSent?.text ?? message.body; - -bool usageIsAvailable(PangeaMessageEvent message) { - try { - return message.originalSent?.choreo != null; - } catch (err) { - return false; - } -} - -String getFilename(Room room, DownloadType type) { - final String roomName = room - .getLocalizedDisplayname() - .trim() - .replaceAll(RegExp(r'[^A-Za-z0-9\s]'), "") - .replaceAll(RegExp(r'\s+'), "-"); - final String timestamp = - DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now()); - final String extension = type == DownloadType.txt - ? 'txt' - : type == DownloadType.csv - ? 'csv' - : 'xlsx'; - return "$roomName-$timestamp.$extension"; -} - -String mimetype(DownloadType fileType) { - switch (fileType) { - case DownloadType.txt: - return 'text/plain'; - case DownloadType.csv: - return 'text/csv'; - case DownloadType.xlsx: - return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - } -} - -String getTxtContent( - List messages, - BuildContext context, - String filename, - Room room, -) { - String formattedInfo = ""; - for (final PangeaMessageEvent message in messages) { - final String timestamp = - DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs); - final String sender = message.senderId; - final String originalMsg = message.originalWrittenContent; - final String sentMsg = getSentText(message); - final bool usageAvailable = usageIsAvailable(message); - - if (!usageAvailable) { - formattedInfo += - "${L10n.of(context).sender}: $sender\n${L10n.of(context).time}: $timestamp\n${L10n.of(context).originalMessage}: $originalMsg\n${L10n.of(context).sentMessage}: $sentMsg\n${L10n.of(context).useType}: ${L10n.of(context).notAvailable}\n\n"; - continue; - } - - final bool includedIT = message.originalSent!.choreo!.includedIT; - final bool includedIGC = message.originalSent!.choreo!.includedIGC; - - formattedInfo += - "${L10n.of(context).sender}: $sender\n${L10n.of(context).time}: $timestamp\n${L10n.of(context).originalMessage}: $originalMsg\n${L10n.of(context).sentMessage}: $sentMsg\n${L10n.of(context).useType}: "; - if (includedIT && includedIGC) { - formattedInfo += L10n.of(context).taAndGaTooltip; - } else if (includedIT) { - formattedInfo += L10n.of(context).taTooltip; - } else if (includedIGC) { - formattedInfo += L10n.of(context).gaTooltip; - } else { - formattedInfo += L10n.of(context).waTooltip; - } - formattedInfo += "\n\n"; - } - formattedInfo = "${room.getLocalizedDisplayname()}\n\n$formattedInfo"; - return formattedInfo; -} - -String getCSVContent( - List messages, - BuildContext context, - String fileName, -) { - final List> csvData = [ - [ - L10n.of(context).sender, - L10n.of(context).time, - L10n.of(context).originalMessage, - L10n.of(context).sentMessage, - L10n.of(context).taTooltip, - L10n.of(context).gaTooltip, - ] - ]; - for (final PangeaMessageEvent message in messages) { - final String timestamp = - DateFormat('yyyy-MM-dd hh:mm:ss').format(message.originServerTs); - final String sender = message.senderId; - final String originalMsg = message.originalWrittenContent; - final String sentMsg = getSentText(message); - final bool usageAvailable = usageIsAvailable(message); - - if (!usageAvailable) { - csvData.add([ - sender, - timestamp, - originalMsg, - sentMsg, - L10n.of(context).notAvailable, - L10n.of(context).notAvailable, - ]); - continue; - } - - final bool includedIT = message.originalSent!.choreo!.includedIT; - final bool includedIGC = message.originalSent!.choreo!.includedIGC; - - csvData.add([ - sender, - timestamp, - originalMsg, - sentMsg, - includedIT.toString(), - includedIGC.toString(), - ]); - } - final String fileString = const ListToCsvConverter().convert(csvData); - return fileString; -} - -List getExcelContent( - List messages, - BuildContext context, - String filename, -) { - final excel = Excel.createExcel(); - final Sheet sheetObject = excel['Sheet1']; - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).sender); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).time); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).originalMessage); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).sentMessage); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).taTooltip); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: 0)) - .value = TextCellValue(L10n.of(context).gaTooltip); - - for (int i = 0; i < messages.length; i++) { - final PangeaMessageEvent message = messages[i]; - final String sender = message.senderId; - final String originalMsg = message.originalWrittenContent; - final String sentMsg = getSentText(message); - final bool usageAvailable = usageIsAvailable(message); - - bool includedIT = false; - bool includedIGC = false; - - if (usageAvailable) { - includedIT = message.originalSent!.choreo!.includedIT; - includedIGC = message.originalSent!.choreo!.includedIGC; - } - - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i + 2)) - .value = TextCellValue(sender); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: i + 2)) - .value = DateTimeCellValue( - year: message.originServerTs.year, - month: message.originServerTs.month, - day: message.originServerTs.day, - hour: message.originServerTs.hour, - minute: message.originServerTs.minute, - second: message.originServerTs.second, - ); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: i + 2)) - .value = TextCellValue(originalMsg); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: i + 2)) - .value = TextCellValue(sentMsg); - if (usageAvailable) { - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: i + 2)) - .value = TextCellValue(includedIT.toString()); - sheetObject - .cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: i + 2)) - .value = TextCellValue(includedIGC.toString()); - } - } - - final List? bytes = excel.encode(); - return bytes ?? []; -} diff --git a/lib/pangea/chat_settings/utils/download_file.dart b/lib/pangea/chat_settings/utils/download_file.dart deleted file mode 100644 index 86c6e4424..000000000 --- a/lib/pangea/chat_settings/utils/download_file.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -import 'package:open_file/open_file.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:universal_html/html.dart' as webfile; - -import 'package:fluffychat/pangea/chat_settings/utils/download_chat.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; - -enum DownloadType { txt, csv, xlsx } - -Future downloadFile( - dynamic contents, - String filename, - DownloadType fileType, -) async { - if (kIsWeb) { - final blob = webfile.Blob([contents], mimetype(fileType), 'native'); - webfile.AnchorElement( - href: webfile.Url.createObjectUrlFromBlob(blob).toString(), - ) - ..setAttribute("download", filename) - ..click(); - return; - } - if (await Permission.storage.request().isGranted) { - Directory? directory; - try { - if (Platform.isIOS) { - directory = await getApplicationDocumentsDirectory(); - } else { - directory = Directory('/storage/emulated/0/Download'); - if (!await directory.exists()) { - directory = await getExternalStorageDirectory(); - } - } - } catch (err, s) { - debugPrint("Failed to get download folder path"); - ErrorHandler.logError( - e: err, - s: s, - data: {}, - ); - } - if (directory != null) { - final File f = File("${directory.path}/$filename"); - File resp; - if (fileType == DownloadType.txt || fileType == DownloadType.csv) { - resp = await f.writeAsString(contents); - } else { - resp = await f.writeAsBytes(contents); - } - OpenFile.open(resp.path); - } - } -} diff --git a/lib/pangea/constructs/construct_identifier.dart b/lib/pangea/constructs/construct_identifier.dart index 5934117f3..360aaa9f9 100644 --- a/lib/pangea/constructs/construct_identifier.dart +++ b/lib/pangea/constructs/construct_identifier.dart @@ -112,7 +112,7 @@ class ConstructIdentifier { @override int get hashCode { - return lemma.hashCode ^ type.hashCode; + return lemma.hashCode ^ type.hashCode ^ category.hashCode; } String get string { diff --git a/lib/pangea/download/download_file_util.dart b/lib/pangea/download/download_file_util.dart new file mode 100644 index 000000000..3adab21ca --- /dev/null +++ b/lib/pangea/download/download_file_util.dart @@ -0,0 +1,59 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'package:open_file/open_file.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:universal_html/html.dart' as webfile; + +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/download/download_type_enum.dart'; + +class DownloadUtil { + static Future downloadFile( + dynamic contents, + String filename, + DownloadType fileType, + ) async { + if (kIsWeb) { + final blob = webfile.Blob([contents], fileType.mimetype, 'native'); + webfile.AnchorElement( + href: webfile.Url.createObjectUrlFromBlob(blob).toString(), + ) + ..setAttribute("download", filename) + ..click(); + return; + } + if (await Permission.storage.request().isGranted) { + Directory? directory; + try { + if (Platform.isIOS) { + directory = await getApplicationDocumentsDirectory(); + } else { + directory = Directory('/storage/emulated/0/Download'); + if (!await directory.exists()) { + directory = await getExternalStorageDirectory(); + } + } + } catch (err, s) { + debugPrint("Failed to get download folder path"); + ErrorHandler.logError( + e: err, + s: s, + data: {}, + ); + } + if (directory != null) { + final File f = File("${directory.path}/$filename"); + File resp; + if (fileType == DownloadType.txt || fileType == DownloadType.csv) { + resp = await f.writeAsString(contents); + } else { + resp = await f.writeAsBytes(contents); + } + OpenFile.open(resp.path); + } + } + } +} diff --git a/lib/pangea/download/download_room_extension.dart b/lib/pangea/download/download_room_extension.dart new file mode 100644 index 000000000..d9eae2236 --- /dev/null +++ b/lib/pangea/download/download_room_extension.dart @@ -0,0 +1,241 @@ +// ignore_for_file: implementation_imports + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:csv/csv.dart'; +import 'package:excel/excel.dart'; +import 'package:intl/intl.dart'; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/models/timeline_chunk.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/download/download_file_util.dart'; +import 'package:fluffychat/pangea/download/download_type_enum.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; + +class _EventDownloadInfo { + final String sender; + final DateTime timestamp; + final String originalMsg; + final String sentMsg; + final bool usageAvailable; + + final bool includedIT; + final bool includedIGC; + + _EventDownloadInfo({ + required this.sender, + required this.timestamp, + required this.originalMsg, + required this.sentMsg, + required this.usageAvailable, + required this.includedIT, + required this.includedIGC, + }); +} + +class EmptyChatException implements Exception {} + +extension DownloadExtension on Room { + Future download( + DownloadType type, + BuildContext context, + ) async { + List allPangeaMessages; + + final List allEvents = await getAllEvents(); + final TimelineChunk chunk = TimelineChunk(events: allEvents); + final Timeline timeline = Timeline( + room: this, + chunk: chunk, + ); + + allPangeaMessages = getPangeaMessageEvents( + allEvents, + timeline, + ); + + if (allPangeaMessages.isEmpty) { + throw EmptyChatException(); + } + + dynamic content; + final List<_EventDownloadInfo> eventInfo = allPangeaMessages.map((message) { + return _EventDownloadInfo( + sender: message.senderId, + timestamp: message.originServerTs, + originalMsg: message.originalWrittenContent, + sentMsg: message.originalSent?.text ?? message.body, + usageAvailable: message.originalSent?.choreo != null, + includedIT: message.originalSent?.choreo?.includedIT ?? false, + includedIGC: message.originalSent?.choreo?.includedIGC ?? false, + ); + }).toList(); + + switch (type) { + case DownloadType.txt: + content = _getTxtContent(eventInfo, context); + case DownloadType.csv: + content = _getCSVContent(eventInfo, context); + case DownloadType.xlsx: + content = _getExcelContent(eventInfo, context); + } + DownloadUtil.downloadFile(content, _getFilename(type), type); + } + + String _getFilename(DownloadType type) { + final String roomName = getLocalizedDisplayname() + .trim() + .replaceAll(RegExp(r'[^A-Za-z0-9\s]'), "") + .replaceAll(RegExp(r'\s+'), "-"); + + final String timestamp = + DateFormat('yyyy-MM-dd-hh:mm:ss').format(DateTime.now()); + + return "$roomName-$timestamp.${type.extension}"; + } + + String _getTxtContent( + List<_EventDownloadInfo> eventInfo, + BuildContext context, + ) { + String formattedInfo = ""; + final l10n = L10n.of(context); + for (final _EventDownloadInfo info in eventInfo) { + final String timestamp = + DateFormat('yyyy-MM-dd hh:mm:ss').format(info.timestamp); + + if (!info.usageAvailable) { + formattedInfo += + "${l10n.sender}: ${info.sender}\n${l10n.time}: $timestamp\n${l10n.originalMessage}: ${info.originalMsg}\n${l10n.sentMessage}: ${info.sentMsg}\n${l10n.useType}: ${l10n.notAvailable}\n\n"; + continue; + } + + formattedInfo += + "${l10n.sender}: ${info.sender}\n${l10n.time}: $timestamp\n${l10n.originalMessage}: ${info.originalMsg}\n${l10n.sentMessage}: ${info.sentMsg}\n${l10n.useType}: "; + + if (info.includedIT && info.includedIGC) { + formattedInfo += l10n.taAndGaTooltip; + } else if (info.includedIT) { + formattedInfo += l10n.taTooltip; + } else if (info.includedIGC) { + formattedInfo += l10n.gaTooltip; + } else { + formattedInfo += l10n.waTooltip; + } + formattedInfo += "\n\n"; + } + formattedInfo = "${getLocalizedDisplayname()}\n\n$formattedInfo"; + return formattedInfo; + } + + String _getCSVContent( + List<_EventDownloadInfo> eventInfo, + BuildContext context, + ) { + final l10n = L10n.of(context); + final List> csvData = [ + [ + l10n.sender, + l10n.time, + l10n.originalMessage, + l10n.sentMessage, + l10n.taTooltip, + l10n.gaTooltip, + ] + ]; + for (final _EventDownloadInfo info in eventInfo) { + final String timestamp = + DateFormat('yyyy-MM-dd hh:mm:ss').format(info.timestamp); + + if (!info.usageAvailable) { + csvData.add([ + info.sender, + timestamp, + info.originalMsg, + info.sentMsg, + l10n.notAvailable, + l10n.notAvailable, + ]); + continue; + } + + csvData.add([ + info.sender, + timestamp, + info.originalMsg, + info.sentMsg, + info.includedIT.toString(), + info.includedIGC.toString(), + ]); + } + final String fileString = const ListToCsvConverter().convert(csvData); + return fileString; + } + + List _getExcelContent( + List<_EventDownloadInfo> eventInfo, + BuildContext context, + ) { + final l10n = L10n.of(context); + final excel = Excel.createExcel(); + final Sheet sheetObject = excel['Sheet1']; + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: 0)) + .value = TextCellValue(l10n.sender); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: 0)) + .value = TextCellValue(l10n.time); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: 0)) + .value = TextCellValue(l10n.originalMessage); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: 0)) + .value = TextCellValue(l10n.sentMessage); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: 0)) + .value = TextCellValue(l10n.taTooltip); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: 0)) + .value = TextCellValue(l10n.gaTooltip); + + for (int i = 0; i < eventInfo.length; i++) { + final _EventDownloadInfo info = eventInfo[i]; + + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: i + 2)) + .value = TextCellValue(info.sender); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: i + 2)) + .value = DateTimeCellValue( + year: info.timestamp.year, + month: info.timestamp.month, + day: info.timestamp.day, + hour: info.timestamp.hour, + minute: info.timestamp.minute, + second: info.timestamp.second, + ); + + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: i + 2)) + .value = TextCellValue(info.originalMsg); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: i + 2)) + .value = TextCellValue(info.sentMsg); + if (info.usageAvailable) { + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: i + 2)) + .value = TextCellValue(info.includedIT.toString()); + sheetObject + .cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: i + 2)) + .value = TextCellValue(info.includedIGC.toString()); + } + } + + final List? bytes = excel.encode(); + return bytes ?? []; + } +} diff --git a/lib/pangea/download/download_type_enum.dart b/lib/pangea/download/download_type_enum.dart new file mode 100644 index 000000000..04d88a4d9 --- /dev/null +++ b/lib/pangea/download/download_type_enum.dart @@ -0,0 +1,27 @@ +enum DownloadType { + txt, + csv, + xlsx; + + String get mimetype { + switch (this) { + case DownloadType.txt: + return 'text/plain'; + case DownloadType.csv: + return 'text/csv'; + case DownloadType.xlsx: + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + } + + String get extension { + switch (this) { + case DownloadType.txt: + return 'txt'; + case DownloadType.csv: + return 'csv'; + case DownloadType.xlsx: + return 'xlsx'; + } + } +} diff --git a/lib/pangea/events/event_wrappers/pangea_representation_event.dart b/lib/pangea/events/event_wrappers/pangea_representation_event.dart index 21e20e652..741c60920 100644 --- a/lib/pangea/events/event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_representation_event.dart @@ -9,6 +9,7 @@ import 'package:matrix/matrix.dart'; import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/choreographer/event_wrappers/pangea_choreo_event.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart'; @@ -336,4 +337,22 @@ class RepresentationEvent { .toList() ?? []; } + + List vocabAndMorphUses() { + if (tokens == null || tokens!.isEmpty) { + return []; + } + + final metadata = ConstructUseMetaData( + roomId: parentMessageEvent.room.id, + timeStamp: parentMessageEvent.originServerTs, + eventId: parentMessageEvent.eventId, + ); + + return content.vocabAndMorphUses( + tokens: tokens!, + metadata: metadata, + choreo: choreo, + ); + } } diff --git a/lib/pangea/events/models/representation_content_model.dart b/lib/pangea/events/models/representation_content_model.dart index cd859c05b..4f56bfbd2 100644 --- a/lib/pangea/events/models/representation_content_model.dart +++ b/lib/pangea/events/models/representation_content_model.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart'; -import 'package:fluffychat/widgets/matrix.dart'; /// this class is contained within a [RepresentationEvent] /// this event is the child of a [EventTypes.Message] @@ -105,8 +104,6 @@ class PangeaRepresentation { ChoreoRecord? choreo, }) { final List uses = []; - final l2 = MatrixState.pangeaController.languageController.userL2; - if (langCode.split("-")[0] != l2?.langCodeShort) return uses; // missing vital info so return if (event?.roomId == null && metadata?.roomId == null) { diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 3c4626460..e35aeb077 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/extensions/join_rule_extension.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index 9dfbd7f2d..820a2ecd3 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -321,4 +321,55 @@ extension EventsRoomExtension on Room { return resp.chunk.map((e) => Event.fromMatrixEvent(e, this)).toList(); } + + Future> getAllEvents() async { + final GetRoomEventsResponse initalResp = + await client.getRoomEvents(id, Direction.b); + + if (initalResp.end == null) return []; + String? nextStartToken = initalResp.end; + List allMatrixEvents = initalResp.chunk; + while (nextStartToken != null) { + final GetRoomEventsResponse resp = await client.getRoomEvents( + id, + Direction.b, + from: nextStartToken, + ); + final chunkMessages = resp.chunk; + allMatrixEvents.addAll(chunkMessages); + resp.end != nextStartToken + ? nextStartToken = resp.end + : nextStartToken = null; + } + + allMatrixEvents = allMatrixEvents.reversed.toList(); + final List allEvents = allMatrixEvents + .map((MatrixEvent message) => Event.fromMatrixEvent(message, this)) + .toList(); + + return allEvents; + } + + List getPangeaMessageEvents( + List events, + Timeline timeline, + ) { + final List allPangeaMessages = events + .where( + (Event event) => + event.type == EventTypes.Message && + event.content['msgtype'] == MessageTypes.Text, + ) + .map( + (Event message) => PangeaMessageEvent( + event: message, + timeline: timeline, + ownMessage: client.userID == message.senderId, + ), + ) + .cast() + .toList(); + + return allPangeaMessages; + } } diff --git a/lib/pangea/spaces/widgets/download_space_analytics_dialog.dart b/lib/pangea/spaces/widgets/download_space_analytics_dialog.dart index 812913f93..244a26f28 100644 --- a/lib/pangea/spaces/widgets/download_space_analytics_dialog.dart +++ b/lib/pangea/spaces/widgets/download_space_analytics_dialog.dart @@ -12,9 +12,10 @@ import 'package:fluffychat/pangea/analytics_downloads/space_analytics_summary_mo import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/chat_settings/utils/download_file.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; +import 'package:fluffychat/pangea/download/download_file_util.dart'; +import 'package:fluffychat/pangea/download/download_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -143,7 +144,7 @@ class DownloadAnalyticsDialogState extends State { final fileName = "analytics_${widget.space.name}_${DateTime.now().toIso8601String()}.${_downloadType == DownloadType.xlsx ? 'xlsx' : 'csv'}"; - await downloadFile( + await DownloadUtil.downloadFile( content, fileName, DownloadType.csv,