From 361690935dd2e0832e93f6354eaaa6a368eb9575 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sat, 20 Jan 2024 07:32:45 -0500 Subject: [PATCH 1/8] in the midst of it --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/chat_input_row.dart | 43 +++--- lib/pages/chat/chat_view.dart | 27 +++- .../widgets/language_display_toggle.dart | 69 ++++++++- lib/pangea/constants/model_keys.dart | 4 +- lib/pangea/constants/pangea_event_types.dart | 2 + lib/pangea/controllers/pangea_controller.dart | 3 + .../text_to_speech_controller.dart | 125 ++++++++++++++++ lib/pangea/models/pangea_audio_events.dart | 9 ++ lib/pangea/models/pangea_message_event.dart | 117 ++++++++++++++- lib/pangea/network/urls.dart | 2 + lib/pangea/repo/image_repo.dart | 72 +++++++++ lib/pangea/repo/text_to_speech_repo.dart | 66 +++++++++ lib/pangea/widgets/chat/message_actions.dart | 27 ++++ .../widgets/chat/text_to_speech_button.dart | 128 ++++++++++++++++ needed-translations.txt | 139 ++++++++++++------ 16 files changed, 759 insertions(+), 77 deletions(-) create mode 100644 lib/pangea/controllers/text_to_speech_controller.dart create mode 100644 lib/pangea/models/pangea_audio_events.dart create mode 100644 lib/pangea/repo/image_repo.dart create mode 100644 lib/pangea/repo/text_to_speech_repo.dart create mode 100644 lib/pangea/widgets/chat/message_actions.dart create mode 100644 lib/pangea/widgets/chat/text_to_speech_button.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a859c6229..05701909d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3956,5 +3956,6 @@ "inNoSpaces": "You are not a member of any classes or exchanges", "successfullySubscribed": "You have successfully subscribed!", "clickToManageSubscription": "Click here to manage your subscription.", - "emptyInviteWarning": "Add this chat to a class or exchange to invite other users." + "emptyInviteWarning": "Add this chat to a class or exchange to invite other users.", + "errorGettingAudio": "Error getting audio. Please refresh and try again." } \ No newline at end of file diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 6c29f4ddd..eef7aaca0 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,17 +1,17 @@ +import 'package:animations/animations.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_actions.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import 'package:animations/animations.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../config/themes.dart'; import 'chat.dart'; import 'input_bar.dart'; @@ -58,18 +58,21 @@ class ChatInputRow extends StatelessWidget { ), ) else - SizedBox( - height: 56, - child: TextButton( - onPressed: controller.forwardEventsAction, - child: Row( - children: [ - const Icon(Icons.keyboard_arrow_left_outlined), - Text(L10n.of(context)!.forward), - ], - ), - ), - ), + // #Pangea + PangeaMessageActions(chatController: controller), + // SizedBox( + // height: 56, + // child: TextButton( + // onPressed: controller.forwardEventsAction, + // child: Row( + // children: [ + // const Icon(Icons.keyboard_arrow_left_outlined), + // Text(L10n.of(context)!.forward), + // ], + // ), + // ), + // ), + // Pangea# controller.selectedEvents.length == 1 ? controller.selectedEvents.first .getDisplayEvent(controller.timeline!) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index f801df69e..c1b8e1bfd 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -29,7 +29,10 @@ import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; -enum _EventContextAction { info, report } +//#Pangea +// enum _EventContextAction { info, report } +enum _EventContextAction { info, forward, report } +//Pangea# class ChatView extends StatelessWidget { final ChatController controller; @@ -85,18 +88,34 @@ class ChatView extends StatelessWidget { case _EventContextAction.report: controller.reportEventAction(); break; + // #Pangea + case _EventContextAction.forward: + controller.forwardEventsAction(); + break; + // Pangea# } }, itemBuilder: (context) => [ // #Pangea + // PopupMenuItem( + // value: _EventContextAction.info, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // const Icon(Icons.info_outlined), + // const SizedBox(width: 12), + // Text(L10n.of(context)!.messageInfo), + // ], + // ), + // ), PopupMenuItem( - value: _EventContextAction.info, + value: _EventContextAction.forward, child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.info_outlined), + const Icon(Icons.forward), const SizedBox(width: 12), - Text(L10n.of(context)!.messageInfo), + Text(L10n.of(context)!.forward), ], ), ), diff --git a/lib/pangea/choreographer/widgets/language_display_toggle.dart b/lib/pangea/choreographer/widgets/language_display_toggle.dart index a399fda44..451e31782 100644 --- a/lib/pangea/choreographer/widgets/language_display_toggle.dart +++ b/lib/pangea/choreographer/widgets/language_display_toggle.dart @@ -1,5 +1,5 @@ +import 'package:fluffychat/pangea/widgets/flag.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../../config/app_config.dart'; @@ -55,3 +55,70 @@ class LanguageDisplayToggle extends StatelessWidget { // ); } } + +class LanguageToggleSwitch extends StatefulWidget { + final ChatController controller; + + const LanguageToggleSwitch({super.key, required this.controller}); + + @override + _LanguageToggleSwitchState createState() => _LanguageToggleSwitchState(); +} + +class _LanguageToggleSwitchState extends State { + @override + Widget build(BuildContext context) { + final borderRadius = + BorderRadius.circular(20.0); // Use the same radius as your LanguageFlag + + return Tooltip( + message: L10n.of(context)!.toggleLanguages, + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.transparent, // No background color + shape: RoundedRectangleBorder(borderRadius: borderRadius), + padding: EdgeInsets.zero, // Aligns with your custom padding + ), + onPressed: _toggleLanguage, // Use the onTap logic for onPressed + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .background, // Adapt to your app theme or custom color + borderRadius: borderRadius, + ), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + //trranslatte icon + Opacity( + opacity: isL1Selected ? 1.0 : 0.6, + child: LanguageFlag( + language: widget.controller.choreographer.l1Lang, + ), + ), + const SizedBox(width: 8.0), // Spacing between flags + Opacity( + opacity: isL1Selected ? 0.6 : 1.0, + child: LanguageFlag( + language: widget.controller.choreographer.l2Lang, + ), + ), + ], + ), + ), + ), + ); + } + + bool get isL1Selected => + widget.controller.choreographer.messageOptions.isTranslationOn; + + void _toggleLanguage() { + setState(() { + widget.controller.choreographer.messageOptions + .toggleSelectedDisplayLang(); + }); + } +} diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index acec26c80..094ccaa84 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -51,7 +51,7 @@ class ModelKey { static const String word = "word"; static const String lang = "lang"; static const String deepL = "deepl"; - static const String langCode = 'langCode'; + static const String langCode = 'lang_code'; static const String wordLang = "word_lang"; static const String lemma = "lemma"; static const String saveVocab = "save_vocab"; @@ -87,4 +87,6 @@ class ModelKey { static const String currentText = "current"; static const String bestContinuance = "best_continuance"; static const String feedbackLang = "feedback_lang"; + + static const String transcription = "transcription"; } diff --git a/lib/pangea/constants/pangea_event_types.dart b/lib/pangea/constants/pangea_event_types.dart index 68e849c88..b98492871 100644 --- a/lib/pangea/constants/pangea_event_types.dart +++ b/lib/pangea/constants/pangea_event_types.dart @@ -13,4 +13,6 @@ class PangeaEventTypes { static const vocab = "p.vocab"; static const roomInfo = "pangea.roomtopic"; + + static const audio = "p.audio"; } diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 2d612ecab..e60b44b40 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/message_data_controller.dart'; import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/permissions_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/controllers/user_controller.dart'; import 'package:fluffychat/pangea/controllers/word_net_controller.dart'; import 'package:fluffychat/pangea/guard/p_vguard.dart'; @@ -44,6 +45,7 @@ class PangeaController { late ITFeedbackController itFeedback; late InstructionsController instructions; late SubscriptionController subscriptionController; + late TextToSpeechController textToSpeech; ///store Services late PLocalStore pStoreService; @@ -89,6 +91,7 @@ class PangeaController { instructions = InstructionsController(this); subscriptionController = SubscriptionController(this); itFeedback = ITFeedbackController(this); + textToSpeech = TextToSpeechController(this); PAuthGaurd.pController = this; } diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart new file mode 100644 index 000000000..bcd2c556f --- /dev/null +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/model_keys.dart'; +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:http/http.dart'; + +import '../network/requests.dart'; + +class TextToSpeechRequest { + String text; + String langCode; + + TextToSpeechRequest({required this.text, required this.langCode}); + + Map toJson() => { + ModelKey.text: text, + ModelKey.langCode: langCode, + }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TextToSpeechRequest && + other.text == text && + other.langCode == langCode; + } + + @override + int get hashCode => text.hashCode ^ langCode.hashCode; +} + +class TextToSpeechResponse { + String audioContent; + String mediaType; + int durationMillis; + List waveform; + + TextToSpeechResponse({ + required this.audioContent, + required this.mediaType, + required this.durationMillis, + required this.waveform, + }); + + factory TextToSpeechResponse.fromJson( + Map json, + ) => + TextToSpeechResponse( + audioContent: json["audio_content"], + mediaType: json["media_type"], + durationMillis: json["duration_millis"], + waveform: List.from(json["wave_form"]), + ); +} + +class _TextToSpeechCacheItem { + Future data; + + _TextToSpeechCacheItem({ + required this.data, + }); +} + +class TextToSpeechController { + static final Map _cache = {}; + late final PangeaController _pangeaController; + + Timer? _cacheClearTimer; + + TextToSpeechController(PangeaController pangeaController) { + _pangeaController = pangeaController; + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 15); // Adjust the duration as needed + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } + + void dispose() { + _cacheClearTimer?.cancel(); + } + + Future get( + TextToSpeechRequest params, + ) async { + if (_cache.containsKey(params)) { + return _cache[params]!.data; + } else { + final Future response = _fetchResponse( + await _pangeaController.userController.accessToken, + params, + ); + _cache[params] = _TextToSpeechCacheItem(data: response); + return response; + } + } + + static Future _fetchResponse( + String accessToken, + TextToSpeechRequest params, + ) async { + final Requests request = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await request.post( + url: PApiUrls.textToSpeech, + body: params.toJson(), + ); + + final Map json = jsonDecode(res.body); + + return TextToSpeechResponse.fromJson(json); + } +} diff --git a/lib/pangea/models/pangea_audio_events.dart b/lib/pangea/models/pangea_audio_events.dart new file mode 100644 index 000000000..3583d021e --- /dev/null +++ b/lib/pangea/models/pangea_audio_events.dart @@ -0,0 +1,9 @@ +// relates to a pangea representation event +// the matrix even fits the form of a regular matrix audio event +// but with something to distinguish it as a pangea audio event + +import 'package:matrix/matrix.dart'; + +class PangeaAudioEvent { + Event? _event; +} diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index 9a8a45d44..0a5b7602a 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -1,8 +1,10 @@ +import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; @@ -81,6 +83,116 @@ class PangeaMessageEvent { return true; } + //get audio for text and language + //if no audio exists, create it + //if audio exists, return it + Future getAudioGlobal(String langCode) async { + // try { + final String text = representationByLanguage(langCode)?.text ?? body; + + final local = getAudioLocal(langCode, text); + + if (local != null) return Future.value(local.eventId); + + final TextToSpeechRequest params = TextToSpeechRequest( + text: text, + langCode: langCode, + ); + + final TextToSpeechResponse response = + await MatrixState.pangeaController.textToSpeech.get( + params, + ); + + if (response.mediaType != 'audio/ogg') { + throw Exception('Unexpected media type: ${response.mediaType}'); + } + + final audioBytes = base64.decode(response.audioContent); + + // from text, trim whitespace, remove special characters, and limit to 20 characters + // final fileName = + // text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20); + final fileName = "audio_for_${eventId}_$langCode"; + + final file = MatrixAudioFile( + bytes: audioBytes, + name: fileName, + ); + + return room.sendFileEvent( + file, + inReplyTo: _event, + extraContent: { + 'info': { + ...file.info, + 'duration': response.durationMillis, + }, + 'org.matrix.msc3245.voice': {}, + 'org.matrix.msc1767.audio': { + 'duration': response.durationMillis, + 'waveform': null, + // 'waveform': response.waveform, + }, + 'transcription': { + ModelKey.text: text, + ModelKey.langCode: langCode, + }, + }, + ).timeout( + Durations.long4, + onTimeout: () { + debugPrint("timeout in getAudioGlobal"); + return null; + }, + ).then((eventId) { + debugPrint("eventId in getAudioGlobal $eventId"); + return eventId; + }).catchError((err, s) { + debugPrint("error in getAudioGlobal"); + debugPrint(err); + debugPrint(s); + debugger(when: kDebugMode); + return null; + }); + + // } catch (err, s) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // e: err, + // s: s, + // ); + // return Future.value(null); + // } + } + + Event? getAudioLocal(String langCode, String text) { + return allAudio.firstWhereOrNull( + (element) { + // Safely access the transcription map + final transcription = + element.content.tryGet>(ModelKey.transcription); + if (transcription == null) { + // If transcription is null, this element does not match. + return false; + } + + // Safely get language code and text from the transcription + final elementLangCode = transcription.tryGet(ModelKey.langCode); + final elementText = transcription.tryGet(ModelKey.text); + + // Check if both language code and text match + return elementLangCode == langCode && elementText == text; + }, + ); + } + + // get audio events that are related to this event + Set get allAudio => _latestEdit.aggregatedEvents( + timeline, + EventTypes.Message, + ); + List? _representations; List get representations { if (_representations != null) return _representations!; @@ -188,11 +300,6 @@ class PangeaMessageEvent { RepresentationEvent? rep = representationByLanguage(langCode); - //if event is less than 1 minute old, then print new event - if (isNew) { - debugger(when: kDebugMode); - } - while ((isNew || eventId.contains("web")) && tries < 20) { if (rep != null) return rep; await Future.delayed(const Duration(milliseconds: 500)); diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 0b0d6ad59..912376e45 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -51,6 +51,8 @@ class PApiUrls { static String firstStep = "/it_initialstep"; static String subseqStep = "/it_step"; + static String textToSpeech = "$choreoBaseApi/text_to_speech"; + ///-------------------------------- revenue cat -------------------------- static String rcApiV1 = "https://api.revenuecat.com/v1"; static String rcApiV2 = diff --git a/lib/pangea/repo/image_repo.dart b/lib/pangea/repo/image_repo.dart new file mode 100644 index 000000000..624e06e22 --- /dev/null +++ b/lib/pangea/repo/image_repo.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + +import '../config/environment.dart'; +import '../network/requests.dart'; + +class GenerateImageeResponse { + final String imageUrl; + final String prompt; + + GenerateImageeResponse({ + required this.imageUrl, + required this.prompt, + }); + + factory GenerateImageeResponse.fromJson(Map json) { + return GenerateImageeResponse( + imageUrl: json['image_url'], + prompt: json['prompt'], + ); + } + + factory GenerateImageeResponse.error() { + return GenerateImageeResponse( + imageUrl: 'https://i.imgur.com/2L2JYqk.png', + prompt: 'Error', + ); + } +} + +class GenerateImageRequest { + String prompt; + + GenerateImageRequest({required this.prompt}); + + Map toJson() => { + 'prompt': prompt, + }; +} + +class ImageRepo { + static Future fetchImage( + GenerateImageRequest request) async { + final Requests req = + Requests(baseUrl: Environment.choreoApi); // Set your API base URL + final requestBody = request.toJson(); + + try { + final Response res = await req.post( + url: '/generate-image/', // Endpoint in your FastAPI server + body: requestBody, + ); + + if (res.statusCode == 200) { + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + return GenerateImageeResponse.fromJson( + decodedBody); // Convert response to ImageModel + } else { + throw Exception('Failed to load image'); + } + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack, data: requestBody); + return GenerateImageeResponse + .error(); // Return an error model or handle accordingly + } + } +} diff --git a/lib/pangea/repo/text_to_speech_repo.dart b/lib/pangea/repo/text_to_speech_repo.dart new file mode 100644 index 000000000..aafd299bd --- /dev/null +++ b/lib/pangea/repo/text_to_speech_repo.dart @@ -0,0 +1,66 @@ +// import 'dart:async'; +// import 'dart:convert'; + +// import 'package:fluffychat/pangea/config/environment.dart'; +// import 'package:fluffychat/pangea/constants/model_keys.dart'; +// import 'package:fluffychat/pangea/network/urls.dart'; +// import 'package:http/http.dart'; + +// import '../network/requests.dart'; + +// class TextToSpeechRequest { +// String text; +// String langCode; + +// TextToSpeechRequest({required this.text, required this.langCode}); + +// Map toJson() => { +// ModelKey.text: text, +// ModelKey.langCode: langCode, +// }; +// } + +// class TextToSpeechResponse { +// String audioContent; +// String mediaType; +// int durationMillis; +// List waveform; + +// TextToSpeechResponse({ +// required this.audioContent, +// required this.mediaType, +// required this.durationMillis, +// required this.waveform, +// }); + +// factory TextToSpeechResponse.fromJson( +// Map json, +// ) => +// TextToSpeechResponse( +// audioContent: json["audio_content"], +// mediaType: json["media_type"], +// durationMillis: json["duration_millis"], +// waveform: List.from(json["wave_form"]), +// ); +// } + +// class TextToSpeechService { +// static Future get({ +// required String accessToken, +// required TextToSpeechRequest params, +// }) async { +// final Requests request = Requests( +// choreoApiKey: Environment.choreoApiKey, +// accessToken: accessToken, +// ); + +// final Response res = await request.post( +// url: PApiUrls.textToSpeech, +// body: params.toJson(), +// ); + +// final Map json = jsonDecode(res.body); + +// return TextToSpeechResponse.fromJson(json); +// } +// } diff --git a/lib/pangea/widgets/chat/message_actions.dart b/lib/pangea/widgets/chat/message_actions.dart new file mode 100644 index 000000000..b8eebbb43 --- /dev/null +++ b/lib/pangea/widgets/chat/message_actions.dart @@ -0,0 +1,27 @@ +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart'; +import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart'; +import 'package:flutter/material.dart'; + +class PangeaMessageActions extends StatelessWidget { + final ChatController chatController; + + const PangeaMessageActions({super.key, required this.chatController}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + LanguageToggleSwitch(controller: chatController), + TextToSpeechButton( + controller: chatController, + ), + // IconButton( + // icon: Icon(Icons.mic), + // onPressed: chatController.onMicTap, + // ), + // Add more IconButton widgets here + ], + ); + } +} diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart new file mode 100644 index 000000000..c5c229a47 --- /dev/null +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -0,0 +1,128 @@ +import 'dart:developer'; + +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:matrix/matrix.dart'; + +class TextToSpeechButton extends StatefulWidget { + final ChatController controller; + + const TextToSpeechButton({ + super.key, + required this.controller, + }); + + @override + _TextToSpeechButtonState createState() => _TextToSpeechButtonState(); +} + +class _TextToSpeechButtonState extends State { + final AudioPlayer _audioPlayer = AudioPlayer(); + + @override + void dispose() { + _audioPlayer.dispose(); + super.dispose(); + } + + void _playSpeech() { + try { + final String langCode = widget.controller.choreographer.messageOptions + .selectedDisplayLang?.langCode ?? + widget.controller.choreographer.l2LangCode ?? + 'en'; + final Event event = widget.controller.selectedEvents.first; + + PangeaMessageEvent( + event: event, + timeline: widget.controller.timeline!, + ownMessage: event.senderId == Matrix.of(context).client.userID, + selected: true, + ).getAudioGlobal(langCode); + + // final String? text = PangeaMessageEvent( + // event: event, + // timeline: widget.controller.timeline!, + // ownMessage: event.senderId == Matrix.of(context).client.userID, + // selected: true, + // ).representationByLanguage(langCode)?.text; + + // if (text == null || text.isEmpty) { + // throw Exception("text is null or empty in text_to_speech_button.dart"); + // } + + // final TextToSpeechRequest params = TextToSpeechRequest( + // text: text, + // langCode: widget.controller.choreographer.messageOptions + // .selectedDisplayLang?.langCode ?? + // widget.controller.choreographer.l2LangCode ?? + // LanguageKeys.unknownLanguage, + // ); + + // final TextToSpeechResponse response = await TextToSpeechService.get( + // accessToken: + // await MatrixState.pangeaController.userController.accessToken, + // params: params, + // ); + + // if (response.mediaType != 'audio/ogg') { + // throw Exception('Unexpected media type: ${response.mediaType}'); + // } + + // // Decode the base64 audio content to bytes + // final audioBytes = base64.decode(response.audioContent); + + // final encoding = Uri.dataFromBytes(audioBytes); + // final uri = AudioSource.uri(encoding); + // // gets here without problems + + // await _audioPlayer.setAudioSource(uri); + // await _audioPlayer.play(); + + // final audioBytes = base64.decode(response.audioContent); + // final tempDir = await getTemporaryDirectory(); + // final file = File('${tempDir.path}/speech.ogg'); + // await file.writeAsBytes(audioBytes); + + // await _audioPlayer.setFilePath(file.path); + + // await _audioPlayer.play(); + } catch (e) { + debugger(when: kDebugMode); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + L10n.of(context)!.errorGettingAudio, + ), + ), + ); + ErrorHandler.logError( + e: Exception(), + s: StackTrace.current, + m: 'text is null or empty in text_to_speech_button.dart', + data: { + 'event': widget.controller.selectedEvents.first, + 'langCode': widget.controller.choreographer.messageOptions + .selectedDisplayLang?.langCode ?? + widget.controller.choreographer.l2LangCode ?? + LanguageKeys.unknownLanguage, + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: _playSpeech, + child: const Text('Convert to Speech'), + ); + } +} diff --git a/needed-translations.txt b/needed-translations.txt index cdb97658e..64a3f81b8 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -765,7 +765,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "bn": [ @@ -1539,7 +1540,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "bo": [ @@ -2313,7 +2315,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ca": [ @@ -3082,7 +3085,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "cs": [ @@ -3851,7 +3855,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "de": [ @@ -4620,7 +4625,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "el": [ @@ -5394,7 +5400,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "eo": [ @@ -6163,7 +6170,12 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" + ], + + "es": [ + "errorGettingAudio" ], "et": [ @@ -6932,7 +6944,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "eu": [ @@ -7701,7 +7714,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fa": [ @@ -8470,7 +8484,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fi": [ @@ -9239,7 +9254,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "fr": [ @@ -10008,7 +10024,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ga": [ @@ -10777,7 +10794,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "gl": [ @@ -11546,7 +11564,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "he": [ @@ -12315,7 +12334,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hi": [ @@ -13089,7 +13109,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hr": [ @@ -13858,7 +13879,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "hu": [ @@ -14627,7 +14649,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "id": [ @@ -15396,7 +15419,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ie": [ @@ -16167,7 +16191,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "it": [ @@ -16936,7 +16961,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ja": [ @@ -17705,7 +17731,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ko": [ @@ -18474,7 +18501,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "lt": [ @@ -19243,7 +19271,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "lv": [ @@ -20017,7 +20046,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "nb": [ @@ -20786,7 +20816,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "nl": [ @@ -21555,7 +21586,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pl": [ @@ -22324,7 +22356,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt": [ @@ -23098,7 +23131,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt_BR": [ @@ -23867,7 +23901,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "pt_PT": [ @@ -24636,7 +24671,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ro": [ @@ -25405,7 +25441,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ru": [ @@ -26174,7 +26211,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sk": [ @@ -26944,7 +26982,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sl": [ @@ -27716,7 +27755,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sr": [ @@ -28485,7 +28525,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "sv": [ @@ -29254,7 +29295,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "ta": [ @@ -30028,7 +30070,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "th": [ @@ -30802,7 +30845,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "tr": [ @@ -31571,7 +31615,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "uk": [ @@ -32340,7 +32385,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "vi": [ @@ -33112,7 +33158,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "zh": [ @@ -33881,7 +33928,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ], "zh_Hant": [ @@ -34650,6 +34698,7 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "errorGettingAudio" ] } From 847ad9f8c1b141d6fb5272dbe79740242424a264 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 24 Jan 2024 12:28:38 -0500 Subject: [PATCH 2/8] coming along, next is getAudioLocal --- lib/pages/chat/events/audio_player.dart | 29 +- .../text_to_speech_controller.dart | 33 + .../extensions/pangea_room_extension.dart | 40 +- lib/pangea/models/pangea_message_event.dart | 52 +- lib/pangea/widgets/chat/message_actions.dart | 29 +- .../widgets/chat/text_to_speech_button.dart | 159 +- .../filtered_timeline_extension.dart | 4 + needed-translations.txt | 2187 +++++++++++++++-- 8 files changed, 2195 insertions(+), 338 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 45f7de83f..5cf5a98a7 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -1,15 +1,16 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; +import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; +import 'package:fluffychat/utils/error_reporter.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:fluffychat/utils/error_reporter.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import '../../../utils/matrix_sdk_extensions/event_extension.dart'; class AudioPlayerWidget extends StatefulWidget { @@ -80,6 +81,7 @@ class AudioPlayerState extends State { }); _playAction(); } catch (e, s) { + debugger(); Logs().v('Could not download audio file', e, s); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -132,7 +134,26 @@ class AudioPlayerState extends State { if (audioFile != null) { audioPlayer.setFilePath(audioFile.path); } else { - await audioPlayer.setAudioSource(MatrixFileAudioSource(matrixFile!)); + final data = matrixFile!.bytes; + final mimeType = matrixFile!.mimeType; + //shouldn't have to be settting this here + //TODO: figure out why this is necessary + matrixFile = MatrixAudioFile( + bytes: matrixFile!.bytes, + name: matrixFile!.name, + mimeType: "audio/ogg", + ); + debugPrint("audioType is $mimeType"); + if (!TextToSpeechController.isOggFile(matrixFile!.bytes)) { + debugger(when: kDebugMode); + } else { + debugPrint("still an ogg file!"); + } + try { + await audioPlayer.setAudioSource(MatrixFileAudioSource(matrixFile!)); + } catch (e, s) { + debugger(when: kDebugMode); + } } audioPlayer.play().onError( ErrorReporter(context, 'Unable to play audio message') diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index bcd2c556f..4db6caa6e 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; @@ -122,4 +123,36 @@ class TextToSpeechController { return TextToSpeechResponse.fromJson(json); } + // if (json["wave_form"] == null) { + // json["wave_form"] = getWaveForm(); + // } + + // return TextToSpeechResponse( + // audioContent: String.fromCharCodes(base64Decode(json["audio_content"])), + // mediaType: json["media_type"], + // durationMillis: durationMillis(json["duration_millis"]), + // waveform: getWaveForm(json["audio_content"]), + // ); + // } + + // static List getWaveForm(audioContent) { + // return []; + // } + + // static int durationMillis(audioContent) { + // return 0; + // } + + static bool isOggFile(Uint8List bytes) { + // Check if the file has enough bytes for the header + if (bytes.length < 4) { + return false; + } + + // Check the magic number for OGG file + return bytes[0] == 0x4F && + bytes[1] == 0x67 && + bytes[2] == 0x67 && + bytes[3] == 0x53; + } } diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 624437666..308d4db0a 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -824,28 +824,28 @@ extension PangeaRoom on Room { Future setClassPowerlLevels() async { try { - if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { - return; - } - final currentPower = getState(EventTypes.RoomPowerLevels); - final Map? currentPowerContent = - currentPower!.content["events"] as Map?; - final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; - final studentAnalyticsPower = - currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary]; + // if (ownPowerLevel < ClassDefaultValues.powerLevelOfAdmin) { + // return; + // } + // final currentPower = getState(EventTypes.RoomPowerLevels); + // final Map? currentPowerContent = + // currentPower!.content["events"] as Map?; + // final spaceChildPower = currentPowerContent?[EventTypes.spaceChild]; + // final studentAnalyticsPower = + // currentPowerContent?[PangeaEventTypes.studentAnalyticsSummary]; - if (spaceChildPower == null || studentAnalyticsPower == null) { - currentPowerContent!["events"][EventTypes.spaceChild] = 0; - currentPowerContent["events"] - [PangeaEventTypes.studentAnalyticsSummary] = 0; + // if (spaceChildPower == null || studentAnalyticsPower == null) { + // currentPowerContent!["events"][EventTypes.spaceChild] = 0; + // currentPowerContent["events"] + // [PangeaEventTypes.studentAnalyticsSummary] = 0; - await client.setRoomStateWithKey( - id, - EventTypes.RoomPowerLevels, - currentPower.stateKey ?? "", - currentPowerContent, - ); - } + // await client.setRoomStateWithKey( + // id, + // EventTypes.RoomPowerLevels, + // currentPower.stateKey ?? "", + // currentPowerContent, + // ); + // } } catch (err, s) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: s, data: toJson()); diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index 0a5b7602a..71b3dd685 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -110,6 +110,12 @@ class PangeaMessageEvent { final audioBytes = base64.decode(response.audioContent); + if (!TextToSpeechController.isOggFile(audioBytes)) { + throw Exception("File is not a valid OGG format"); + } else { + debugPrint("File is a valid OGG format"); + } + // from text, trim whitespace, remove special characters, and limit to 20 characters // final fileName = // text.trim().replaceAll(RegExp('[^A-Za-z0-9]'), '').substring(0, 20); @@ -118,8 +124,13 @@ class PangeaMessageEvent { final file = MatrixAudioFile( bytes: audioBytes, name: fileName, + mimeType: response.mediaType, ); + if (file.mimeType != "audio/ogg") { + throw Exception("Unexpected mime type: ${file.mimeType}"); + } + return room.sendFileEvent( file, inReplyTo: _event, @@ -131,10 +142,9 @@ class PangeaMessageEvent { 'org.matrix.msc3245.voice': {}, 'org.matrix.msc1767.audio': { 'duration': response.durationMillis, - 'waveform': null, - // 'waveform': response.waveform, + 'waveform': response.waveform, }, - 'transcription': { + ModelKey.transcription: { ModelKey.text: text, ModelKey.langCode: langCode, }, @@ -170,28 +180,36 @@ class PangeaMessageEvent { return allAudio.firstWhereOrNull( (element) { // Safely access the transcription map - final transcription = - element.content.tryGet>(ModelKey.transcription); - if (transcription == null) { - // If transcription is null, this element does not match. - return false; - } + final transcription = element.content.tryGet(ModelKey.transcription); - // Safely get language code and text from the transcription - final elementLangCode = transcription.tryGet(ModelKey.langCode); - final elementText = transcription.tryGet(ModelKey.text); + return transcription != null; + // if (transcription == null) { + // // If transcription is null, this element does not match. + // return false; + // } - // Check if both language code and text match - return elementLangCode == langCode && elementText == text; + // // Safely get language code and text from the transcription + // final elementLangCode = transcription.tryGet(ModelKey.langCode); + // final elementText = transcription.tryGet(ModelKey.text); + + // // Check if both language code and text match + // return elementLangCode == langCode && elementText == text; }, ); } // get audio events that are related to this event - Set get allAudio => _latestEdit.aggregatedEvents( + Set get allAudio => _latestEdit + .aggregatedEvents( timeline, - EventTypes.Message, - ); + RelationshipTypes.reply, + ) + .where((element) { + return element.content.tryGet>( + ModelKey.transcription, + ) != + null; + }).toSet(); List? _representations; List get representations { diff --git a/lib/pangea/widgets/chat/message_actions.dart b/lib/pangea/widgets/chat/message_actions.dart index b8eebbb43..03d3f8e6e 100644 --- a/lib/pangea/widgets/chat/message_actions.dart +++ b/lib/pangea/widgets/chat/message_actions.dart @@ -10,18 +10,21 @@ class PangeaMessageActions extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - LanguageToggleSwitch(controller: chatController), - TextToSpeechButton( - controller: chatController, - ), - // IconButton( - // icon: Icon(Icons.mic), - // onPressed: chatController.onMicTap, - // ), - // Add more IconButton widgets here - ], - ); + return chatController.selectedEvents.length == 1 + ? Row( + children: [ + LanguageToggleSwitch(controller: chatController), + TextToSpeechButton( + controller: chatController, + selectedEvent: chatController.selectedEvents.first, + ), + // IconButton( + // icon: Icon(Icons.mic), + // onPressed: chatController.onMicTap, + // ), + // Add more IconButton widgets here + ], + ) + : const SizedBox(); } } diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart index c5c229a47..e591a448d 100644 --- a/lib/pangea/widgets/chat/text_to_speech_button.dart +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -1,7 +1,8 @@ import 'dart:developer'; +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -13,10 +14,12 @@ import 'package:matrix/matrix.dart'; class TextToSpeechButton extends StatefulWidget { final ChatController controller; + final Event selectedEvent; const TextToSpeechButton({ super.key, required this.controller, + required this.selectedEvent, }); @override @@ -25,6 +28,8 @@ class TextToSpeechButton extends StatefulWidget { class _TextToSpeechButtonState extends State { final AudioPlayer _audioPlayer = AudioPlayer(); + late PangeaMessageEvent _pangeaMessageEvent; + bool _isLoading = false; @override void dispose() { @@ -32,97 +37,103 @@ class _TextToSpeechButtonState extends State { super.dispose(); } - void _playSpeech() { + @override + void initState() { + super.initState(); + _pangeaMessageEvent = PangeaMessageEvent( + event: widget.selectedEvent, + timeline: widget.controller.timeline!, + ownMessage: + widget.selectedEvent.senderId == Matrix.of(context).client.userID, + selected: true, + ); + } + + Event? get localAudioEvent => + langCode != null && text != null && text!.isNotEmpty + ? _pangeaMessageEvent.getAudioLocal(langCode!, text!) + : null; + + String? get langCode => + widget.controller.choreographer.messageOptions.selectedDisplayLang + ?.langCode ?? + widget.controller.choreographer.l2LangCode; + + String? get text => langCode != null + ? _pangeaMessageEvent.representationByLanguage(langCode!)?.text + : null; + + Future _getAudio() async { try { - final String langCode = widget.controller.choreographer.messageOptions - .selectedDisplayLang?.langCode ?? - widget.controller.choreographer.l2LangCode ?? - 'en'; - final Event event = widget.controller.selectedEvents.first; + if (!mounted) return; + if (text == null || text!.isEmpty) return; + if (langCode == null || langCode!.isEmpty) return; - PangeaMessageEvent( - event: event, - timeline: widget.controller.timeline!, - ownMessage: event.senderId == Matrix.of(context).client.userID, - selected: true, - ).getAudioGlobal(langCode); - - // final String? text = PangeaMessageEvent( - // event: event, - // timeline: widget.controller.timeline!, - // ownMessage: event.senderId == Matrix.of(context).client.userID, - // selected: true, - // ).representationByLanguage(langCode)?.text; - - // if (text == null || text.isEmpty) { - // throw Exception("text is null or empty in text_to_speech_button.dart"); - // } - - // final TextToSpeechRequest params = TextToSpeechRequest( - // text: text, - // langCode: widget.controller.choreographer.messageOptions - // .selectedDisplayLang?.langCode ?? - // widget.controller.choreographer.l2LangCode ?? - // LanguageKeys.unknownLanguage, - // ); - - // final TextToSpeechResponse response = await TextToSpeechService.get( - // accessToken: - // await MatrixState.pangeaController.userController.accessToken, - // params: params, - // ); - - // if (response.mediaType != 'audio/ogg') { - // throw Exception('Unexpected media type: ${response.mediaType}'); - // } - - // // Decode the base64 audio content to bytes - // final audioBytes = base64.decode(response.audioContent); - - // final encoding = Uri.dataFromBytes(audioBytes); - // final uri = AudioSource.uri(encoding); - // // gets here without problems - - // await _audioPlayer.setAudioSource(uri); - // await _audioPlayer.play(); - - // final audioBytes = base64.decode(response.audioContent); - // final tempDir = await getTemporaryDirectory(); - // final file = File('${tempDir.path}/speech.ogg'); - // await file.writeAsBytes(audioBytes); - - // await _audioPlayer.setFilePath(file.path); - - // await _audioPlayer.play(); + setState(() => _isLoading = true); + await _pangeaMessageEvent.getAudioGlobal(langCode!); + setState(() => _isLoading = false); } catch (e) { + setState(() => _isLoading = false); debugger(when: kDebugMode); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - L10n.of(context)!.errorGettingAudio, - ), + content: Text(L10n.of(context)!.errorGettingAudio), ), ); ErrorHandler.logError( e: Exception(), s: StackTrace.current, m: 'text is null or empty in text_to_speech_button.dart', - data: { - 'event': widget.controller.selectedEvents.first, - 'langCode': widget.controller.choreographer.messageOptions - .selectedDisplayLang?.langCode ?? - widget.controller.choreographer.l2LangCode ?? - LanguageKeys.unknownLanguage, - }, + data: {'selectedEvent': widget.selectedEvent, 'langCode': langCode}, ); } } @override Widget build(BuildContext context) { - return ElevatedButton( - onPressed: _playSpeech, - child: const Text('Convert to Speech'), + if (_isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final playButton = InkWell( + borderRadius: BorderRadius.circular(64), + onTap: text == null || text!.isEmpty ? null : _getAudio, + child: Material( + color: AppConfig.primaryColor.withAlpha(64), + borderRadius: BorderRadius.circular(64), + child: const Icon( + // Change the icon based on some condition. If you have an audio player state, use it here. + Icons.play_arrow_outlined, + color: AppConfig.primaryColor, + ), + ), ); + + return localAudioEvent == null + ? Opacity( + opacity: text == null || text!.isEmpty ? 0.5 : 1, + child: SizedBox( + width: 44, // Match the size of the button in AudioPlayerState + height: 36, + child: Padding( + //only left side of the button is padded to match the padding of the AudioPlayerState + padding: const EdgeInsets.only(left: 8), + child: playButton, + ), + ), + ) + : Container( + constraints: const BoxConstraints( + maxWidth: 250, + ), + child: Column( + children: [ + AudioPlayerWidget( + localAudioEvent!, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ], + ), + ); } } diff --git a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart index 7ae2bf0c5..0c2fcc4f6 100644 --- a/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart +++ b/lib/utils/matrix_sdk_extensions/filtered_timeline_extension.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:matrix/matrix.dart'; import '../../config/app_config.dart'; @@ -18,6 +19,9 @@ extension IsStateExtension on Event { (!AppConfig.hideUnknownEvents || isEventTypeKnown) && // remove state events that we don't want to render (isState || !AppConfig.hideAllStateEvents) && + // #Pangea + content.tryGet(ModelKey.transcription) == null && + // Pangea# // hide unimportant state events (!AppConfig.hideUnimportantStateEvents || !isState || diff --git a/needed-translations.txt b/needed-translations.txt index 64a3f81b8..05c87f5e7 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -1,9 +1,20 @@ { "ar": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -748,14 +759,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -766,7 +769,35 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "bn": [ @@ -777,8 +808,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -1523,8 +1564,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -1541,7 +1580,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "bo": [ @@ -1552,8 +1624,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -2298,8 +2380,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -2316,14 +2396,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ca": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -3068,8 +3196,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -3086,14 +3212,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "cs": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -3838,8 +4012,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -3856,14 +4028,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "de": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -4558,7 +4774,6 @@ "addToClassOrExchange", "addToClassOrExchangeDesc", "invitedToClassOrExchange", - "decline", "declinedInvitation", "acceptedInvitation", "youreInvited", @@ -4608,14 +4823,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -4626,7 +4833,20 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "el": [ @@ -4637,8 +4857,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -5383,8 +5613,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -5401,14 +5629,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "eo": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -6153,8 +6429,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -6171,7 +6445,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "es": [ @@ -6180,9 +6487,20 @@ "et": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -6927,14 +7245,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -6945,14 +7255,53 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "eu": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -7647,7 +7996,6 @@ "addToClassOrExchange", "addToClassOrExchangeDesc", "invitedToClassOrExchange", - "decline", "declinedInvitation", "acceptedInvitation", "youreInvited", @@ -7697,14 +8045,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -7715,14 +8055,42 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "fa": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -8467,8 +8835,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -8485,14 +8851,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "fi": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -9237,8 +9651,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -9255,14 +9667,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "fr": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -10007,8 +10467,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -10025,14 +10483,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ga": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -10777,8 +11283,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -10795,14 +11299,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "gl": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -11497,7 +12045,6 @@ "addToClassOrExchange", "addToClassOrExchangeDesc", "invitedToClassOrExchange", - "decline", "declinedInvitation", "acceptedInvitation", "youreInvited", @@ -11547,14 +12094,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -11565,14 +12104,42 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "he": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -12317,8 +12884,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -12335,7 +12900,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "hi": [ @@ -12346,8 +12944,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -13092,8 +13700,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -13110,14 +13716,59 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "hr": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "blockListDescription", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -13862,14 +14513,7 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -13880,14 +14524,57 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "hu": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -14632,8 +15319,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -14650,14 +15335,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "id": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -15402,8 +16135,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -15420,16 +16151,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ie": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -16174,8 +16951,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -16192,14 +16967,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "it": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -16944,14 +17763,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -16962,14 +17773,57 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ja": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -17714,8 +18568,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -17732,14 +18584,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ko": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -18484,8 +19384,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -18502,14 +19400,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "lt": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -19254,8 +20200,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -19272,7 +20216,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "lv": [ @@ -19283,8 +20260,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -20029,8 +21016,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -20047,14 +21032,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "nb": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -20799,8 +21832,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -20817,14 +21848,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "nl": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -21569,8 +22648,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -21587,14 +22664,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "pl": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -22339,8 +23464,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -22357,7 +23480,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "pt": [ @@ -22368,8 +23524,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -23114,8 +24280,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -23132,14 +24296,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "pt_BR": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -23834,7 +25042,6 @@ "addToClassOrExchange", "addToClassOrExchangeDesc", "invitedToClassOrExchange", - "decline", "declinedInvitation", "acceptedInvitation", "youreInvited", @@ -23884,14 +25091,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -23902,14 +25101,42 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "pt_PT": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -24654,8 +25881,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -24672,14 +25897,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ro": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -25424,8 +26697,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -25442,14 +26713,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ru": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -26194,14 +27509,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -26212,15 +27519,45 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "sk": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -26965,8 +28302,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -26983,17 +28318,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "sl": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -27738,8 +29118,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -27756,14 +29134,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "sr": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -28508,8 +29934,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -28526,14 +29950,62 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "sv": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -29278,8 +30750,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -29296,7 +30766,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "ta": [ @@ -29307,8 +30810,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -30053,8 +31566,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -30071,7 +31582,40 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "th": [ @@ -30082,8 +31626,18 @@ "classes", "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -30828,8 +32382,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -30846,14 +32398,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "tr": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -31598,14 +33194,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -31616,14 +33204,53 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "uk": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -32368,14 +33995,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -32386,17 +34005,57 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "vi": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", "editChatPermissions", + "enterAGroupName", + "enterASpacepName", "groupDescription", "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -33141,8 +34800,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -33159,14 +34816,58 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "zh": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -33911,14 +35612,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", - "yourGlobalUserIdIs", - "noUsersFoundWithQuery", - "searchChatsRooms", - "groupName", - "createGroupAndInviteUsers", - "groupCanBeFoundViaSearch", "inNoSpaces", "createClass", "createExchange", @@ -33929,14 +35622,57 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ], "zh_Hant": [ "accountInformation", + "addGroupDescription", "addNewFriend", "alreadyHaveAnAccount", "classes", + "createNewGroup", + "editChatPermissions", + "enterAGroupName", + "enterASpacepName", + "groupDescription", + "groupDescriptionHasBeenChanged", + "ignoreListDescription", + "ignoreUsername", + "block", + "blockedUsers", + "blockListDescription", + "blockUsername", + "optionalGroupName", + "requests", "allCorrect", "newWayAllGood", "othersAreBetter", @@ -34681,8 +36417,6 @@ "reportMessageTitle", "reportMessageBody", "noTeachersFound", - "noAddToSpacePermissions", - "alreadyInSpace", "yourGlobalUserIdIs", "noUsersFoundWithQuery", "searchChatsRooms", @@ -34699,6 +36433,39 @@ "successfullySubscribed", "clickToManageSubscription", "emptyInviteWarning", - "errorGettingAudio" + "errorGettingAudio", + "nothingFound", + "wrongRecoveryKey", + "startConversation", + "commandHint_sendraw", + "databaseMigrationTitle", + "databaseMigrationBody", + "leaveEmptyToClearStatus", + "select", + "searchForUsers", + "pleaseEnterYourCurrentPassword", + "newPassword", + "pleaseChooseAStrongPassword", + "passwordsDoNotMatch", + "passwordIsWrong", + "publicLink", + "joinSpace", + "publicSpaces", + "addChatOrSubSpace", + "subspace", + "thisDevice", + "initAppError", + "databaseBuildErrorBody", + "sessionLostBody", + "restoreSessionBody", + "forwardMessageTo", + "signUp", + "pleaseChooseAtLeastChars", + "noEmailWarning", + "pleaseEnterValidEmail", + "noAddToSpacePermissions", + "alreadyInSpace", + "pleaseChooseAUsername", + "chooseAUsername" ] } From 57957eac9e0407f784481546fab21ac0246b96de Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sat, 27 Jan 2024 12:08:19 -0500 Subject: [PATCH 3/8] experimenting --- lib/main.dart | 15 +- lib/pages/chat/chat.dart | 3 +- lib/pages/chat/chat_event_list.dart | 2 +- lib/pages/chat/events/message.dart | 8 +- lib/pages/chat/events/message_content.dart | 1 + .../controllers/message_data_controller.dart | 11 +- .../extensions/pangea_room_extension.dart | 84 ++-- lib/pangea/utils/any_state_holder.dart | 6 - lib/pangea/utils/toolbar_util.dart | 364 ++++++++++++++---- lib/pangea/widgets/igc/pangea_rich_text.dart | 1 + 10 files changed, 346 insertions(+), 149 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f2123f915..88576b23d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,4 @@ -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:matrix/matrix.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; @@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/error_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:matrix/matrix.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/fluffy_chat_app.dart'; diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index f31568064..4d1eddcbd 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1277,10 +1277,11 @@ class ChatController extends State } void onSelectMessage(Event event) { -// #Pangea + // #Pangea if (choreographer.itController.isOpen) { return; } + // Pangea# if (!event.redacted) { if (selectedEvents.contains(event)) { diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index c30de518b..e69b7d074 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -148,7 +148,7 @@ class ChatEventList extends StatelessWidget { scrollToEventId: (String eventId) => controller.scrollToEventId(eventId), // #Pangea - // longPressSelect: controller.selectedEvents.isEmpty, + longPressSelect: controller.selectedEvents.isNotEmpty, selectedDisplayLang: controller.choreographer.messageOptions.selectedDisplayLang, immersionMode: controller.choreographer.immersionMode, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 478103a03..36bdb9fee 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -241,8 +241,14 @@ class Message extends StatelessWidget { alignment: alignment, padding: const EdgeInsets.only(left: 8), child: GestureDetector( + onTap: () => print("got message tap"), + onDoubleTap: () => print("got message double tap"), + onDoubleTapDown: (details) => + print("got message double tap down"), onLongPress: longPressSelect - ? null + ? selected + ? null + : () => print('long press') : () { onSelect(event); // Android usually has a vibration effect on long press: diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 0f6765df2..7a7c0b5bb 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -361,6 +361,7 @@ class MessageContent extends StatelessWidget { cause: cause, context: context, ), + onTap: () => messageToolbar?.onTextTap(context), ); }, ), diff --git a/lib/pangea/controllers/message_data_controller.dart b/lib/pangea/controllers/message_data_controller.dart index 130afcdfe..acaa18657 100644 --- a/lib/pangea/controllers/message_data_controller.dart +++ b/lib/pangea/controllers/message_data_controller.dart @@ -1,17 +1,16 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:collection/collection.dart'; -import 'package:matrix/matrix.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; import 'package:fluffychat/pangea/repo/tokens_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../constants/pangea_event_types.dart'; import '../enum/use_type.dart'; import '../models/choreo_record.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index dd51a06ce..70d6834ec 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -591,51 +591,51 @@ extension PangeaRoom on Room { required String parentEventId, required String type, }) async { - try { - debugPrint("creating $type child for $parentEventId"); - Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); - if (parentEventId.contains("web")) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "sendPangeaEvent with likely invalid parentEventId $parentEventId", - ), - ); - } - final Map repContent = { - // what is the functionality of m.reference? - "m.relates_to": {"rel_type": type, "event_id": parentEventId}, - type: content, - }; - - final String? newEventId = await sendEvent(repContent, type: type); - - if (newEventId == null) { - debugger(when: kDebugMode); - } - - //PTODO - handle the frequent case of a null newEventId - final Event? newEvent = await getEventById(newEventId!); - - if (newEvent == null) { - debugger(when: kDebugMode); - } - - return newEvent; - } catch (err, stack) { + // try { + debugPrint("creating $type child for $parentEventId"); + Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); + if (parentEventId.contains("web")) { debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "type": type, - "parentEventId": parentEventId, - "content": content, - }, + Sentry.addBreadcrumb( + Breadcrumb( + message: + "sendPangeaEvent with likely invalid parentEventId $parentEventId", + ), ); - return null; } + final Map repContent = { + // what is the functionality of m.reference? + "m.relates_to": {"rel_type": type, "event_id": parentEventId}, + type: content, + }; + + final String? newEventId = await sendEvent(repContent, type: type); + + if (newEventId == null) { + debugger(when: kDebugMode); + } + + //PTODO - handle the frequent case of a null newEventId + final Event? newEvent = await getEventById(newEventId!); + + if (newEvent == null) { + debugger(when: kDebugMode); + } + + return newEvent; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // e: err, + // s: stack, + // data: { + // "type": type, + // "parentEventId": parentEventId, + // "content": content, + // }, + // ); + // return null; + // } } ConstructEvent? _vocabEventLocal(String lemma) { diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index e7ee11451..fd6124420 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,13 +1,7 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import '../models/widget_measurement.dart'; - class PangeaAnyState { - final Map?> _streams = {}; - final Map> _pastValues = {}; final Map _layerLinkAndKeys = {}; OverlayEntry? overlay; diff --git a/lib/pangea/utils/toolbar_util.dart b/lib/pangea/utils/toolbar_util.dart index 409bcd034..4848269fb 100644 --- a/lib/pangea/utils/toolbar_util.dart +++ b/lib/pangea/utils/toolbar_util.dart @@ -1,94 +1,138 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; enum MessageMode { translation, play, definition, image, spellCheck } -class MessageOverlay { - static void showOverlay(BuildContext context, GlobalKey targetKey) { - final RenderBox renderBox = - targetKey.currentContext?.findRenderObject() as RenderBox; - final Offset offset = renderBox.localToGlobal(Offset.zero); - final Size size = renderBox.size; - final double screenWidth = MediaQuery.of(context).size.width; +class MessageOverlayController { + OverlayEntry? _overlayEntry; + final BuildContext _context; + final GlobalKey _targetKey; + MessageMode? _currentMode; + AnimationController? _animationController; - // Determines the vertical position of the overlay - final bool isBottomRoomAvailable = - MediaQuery.of(context).size.height - (offset.dy + size.height) >= - size.height; - - OverlayEntry overlayEntry; - MessageMode currentMode = MessageMode.translation; - - // Function to build the content based on the selected mode - Widget buildContent() { - switch (currentMode) { - case MessageMode.translation: - return const Text('Translation Mode'); - case MessageMode.play: - return const Text('Play Mode'); - case MessageMode.definition: - return const Text('Definition Mode'); - case MessageMode.image: - return const Text('Image Mode'); - case MessageMode.spellCheck: - return const Text('SpellCheck Mode'); - default: - return const SizedBox.shrink(); // Returns an empty container - } - } - - // Function to show the overlay with an animation - overlayEntry = OverlayEntry( - builder: (context) => AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - left: offset.dx + size.width / 2 - screenWidth / 2, - right: screenWidth - (offset.dx + size.width / 2 + screenWidth / 2), - top: isBottomRoomAvailable ? offset.dy + size.height : null, - bottom: isBottomRoomAvailable - ? null - : MediaQuery.of(context).size.height - offset.dy, - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - width: screenWidth, - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Material( - elevation: 4.0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - alignment: WrapAlignment.center, - children: MessageMode.values.map((mode) { - return IconButton( - icon: Icon(_getIconData(mode)), - onPressed: () { - currentMode = mode; - overlayEntry.markNeedsBuild(); - }, - ); - }).toList(), - ), - SizeTransition( - sizeFactor: currentMode != null - ? CurvedAnimation( - parent: Overlay.of(context).animation!, - curve: Curves.fastOutSlowIn) - : const AlwaysStoppedAnimation(0), - axisAlignment: -1.0, - child: buildContent(), - ), - ], - ), - ), - ), - ), + MessageOverlayController(this._context, this._targetKey) { + _animationController = AnimationController( + vsync: Navigator.of(_context), // Using the Navigator's TickerProvider + duration: const Duration(milliseconds: 300), ); - - Overlay.of(context).insert(overlayEntry); } - static IconData _getIconData(MessageMode mode) { + void showOverlay() { + final RenderBox renderBox = + _targetKey.currentContext?.findRenderObject() as RenderBox; + final Offset offset = renderBox.localToGlobal(Offset.zero); + final Size size = renderBox.size; + final double screenWidth = MediaQuery.of(_context).size.width; + + // Determines if there is more room above or below the RenderBox + final bool isBottomRoomAvailable = + MediaQuery.of(_context).size.height - (offset.dy + size.height) >= + size.height; + final double topPosition = isBottomRoomAvailable + ? offset.dy + size.height + : offset.dy - size.height; + + // Ensure the overlay does not overflow the screen horizontally + double leftPosition = offset.dx + size.width / 2 - screenWidth / 2; + leftPosition = leftPosition < 0 ? 0 : leftPosition; + final double rightPosition = + leftPosition + screenWidth > MediaQuery.of(_context).size.width + ? MediaQuery.of(_context).size.width - leftPosition - screenWidth + : leftPosition; + + _overlayEntry = OverlayEntry( + builder: (context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + left: leftPosition, + right: rightPosition, + top: isBottomRoomAvailable ? topPosition : null, + bottom: isBottomRoomAvailable + ? null + : MediaQuery.of(_context).size.height - + topPosition - + size.height, + child: AnimatedSize( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 300), + child: Material( + elevation: 4.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + return IconButton( + icon: Icon(_getIconData(mode)), + onPressed: () { + setState(() { + _currentMode = mode; + }); + _animationController?.forward(); + }, + ); + }).toList(), + ), + SizeTransition( + sizeFactor: CurvedAnimation( + parent: _animationController!, + curve: Curves.fastOutSlowIn, + ), + axisAlignment: -1.0, + child: _buildModeContent(), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ); + + Overlay.of(_context).insert(_overlayEntry!); + } + + void hideOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + _animationController?.reverse(); + } + + Widget _buildModeContent() { + switch (_currentMode) { + case MessageMode.translation: + return const Text('Translation Mode'); + case MessageMode.play: + return const Text('Play Mode'); + case MessageMode.definition: + return const Text('Definition Mode'); + case MessageMode.image: + return const Text('Image Mode'); + case MessageMode.spellCheck: + return const Text('SpellCheck Mode'); + default: + return const SizedBox + .shrink(); // Empty container for the default case, meaning no content + } + } + + IconData _getIconData(MessageMode mode) { switch (mode) { case MessageMode.translation: return Icons.g_translate; @@ -101,7 +145,159 @@ class MessageOverlay { case MessageMode.spellCheck: return Icons.spellcheck; default: - return Icons.error; + return Icons.error; // Icon to indicate an error or unsupported mode } } + + void dispose() { + _overlayEntry?.dispose(); + _animationController?.dispose(); + } +} + +class ShowDefintionUtil { + String messageText; + final String langCode; + final String targetId; + final FocusNode focusNode = FocusNode(); + final Room room; + String? textSelection; + bool inCooldown = false; + double? dx; + double? dy; + + ShowDefintionUtil({ + required this.targetId, + required this.room, + required this.langCode, + required this.messageText, + }); + + void onTextSelection({ + required BuildContext context, + TextSelection? selectedText, + SelectedContent? selectedContent, + SelectionChangedCause? cause, + }) { + if ((selectedText == null && selectedContent == null) || + selectedText?.isCollapsed == true) { + clearTextSelection(); + return; + } + textSelection = selectedText != null + ? selectedText.textInside(messageText) + : selectedContent!.plainText; + + if (BrowserContextMenu.enabled && kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + + if (kIsWeb && cause != SelectionChangedCause.tap) { + handleToolbar(context); + } + } + + void clearTextSelection() { + textSelection = null; + if (kIsWeb && !BrowserContextMenu.enabled) { + BrowserContextMenu.enableContextMenu(); + } + } + + void handleToolbar(BuildContext context) async { + if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return; + inCooldown = true; + Timer(const Duration(milliseconds: 750), () => inCooldown = false); + await Future.delayed(const Duration(milliseconds: 750)); + showToolbar(context); + } + + void showDefinition(BuildContext context) { + if (textSelection == null) return; + OverlayUtil.showPositionedCard( + context: context, + cardToShow: WordDataCard( + word: textSelection!, + wordLang: langCode, + fullText: messageText, + fullTextLang: langCode, + hasInfo: false, + room: room, + ), + cardSize: const Size(300, 300), + transformTargetId: targetId, + backDropToDismiss: false, + ); + } + + // web toolbar + Future showToolbar(BuildContext context) async { + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(targetId); + + final RenderObject? targetRenderBox = + layerLinkAndKey.key.currentContext!.findRenderObject(); + final Offset transformTargetOffset = + (targetRenderBox as RenderBox).localToGlobal(Offset.zero); + + if (dx != null && dx! > MediaQuery.of(context).size.width - 130) { + dx = MediaQuery.of(context).size.width - 130; + } + final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0; + final double yOffset = + dy != null ? dy! - transformTargetOffset.dy + 10 : 10; + + OverlayUtil.showOverlay( + context: context, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.zero, + ), + onPressed: () { + showDefinition(context); + }, + child: Text( + L10n.of(context)!.showDefinition, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + size: const Size(130, 45), + transformTargetId: targetId, + offset: Offset(xOffset, yOffset), + ); + } + + void onMouseRegionUpdate(PointerEvent event) { + dx = event.position.dx; + dy = event.position.dy; + } + + Widget contextMenuOverride({ + required BuildContext context, + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + }) { + if (textSelection == null && contentSelection == null) { + return const SizedBox(); + } + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: textSelection?.contextMenuAnchors ?? + contentSelection!.contextMenuAnchors, + buttonItems: [ + if (textSelection != null) ...textSelection.contextMenuButtonItems, + if (contentSelection != null) + ...contentSelection.contextMenuButtonItems, + ContextMenuButtonItem( + label: L10n.of(context)!.showDefinition, + onPressed: () { + showDefinition(context); + focusNode.unfocus(); + }, + ), + ], + ); + } } diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 782af3d8b..935268d97 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -105,6 +105,7 @@ class PangeaRichTextState extends State { cause: cause, context: context, ), + onTap: () => messageToolbar?.onTextTap(context), focusNode: widget.messageToolbar?.focusNode, contextMenuBuilder: (context, state) => widget.messageToolbar?.contextMenuOverride( From 2cb77732e3e8ccc748be32b4a593712f2f238a9e Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 7 Feb 2024 15:28:52 -0500 Subject: [PATCH 4/8] toolbar working --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat/chat.dart | 119 ++++-- lib/pages/chat/chat_event_list.dart | 10 +- lib/pages/chat/chat_input_row.dart | 27 +- lib/pages/chat/chat_view.dart | 383 ++++++++---------- lib/pages/chat/events/html_message.dart | 32 +- lib/pages/chat/events/message.dart | 52 +-- lib/pages/chat/events/message_content.dart | 188 ++++----- .../controllers/it_controller.dart | 1 + lib/pangea/choreographer/widgets/it_bar.dart | 65 +-- lib/pangea/controllers/pangea_controller.dart | 27 +- .../text_to_speech_controller.dart | 1 + .../extensions/pangea_room_extension.dart | 1 - lib/pangea/models/igc_text_data_model.dart | 18 +- lib/pangea/models/pangea_message_event.dart | 145 ++++--- lib/pangea/network/urls.dart | 3 +- .../repo/interactive_translation_repo.dart | 4 +- lib/pangea/repo/message_service.repo.dart | 2 +- lib/pangea/utils/any_state_holder.dart | 2 + lib/pangea/utils/download_chat.dart | 1 - .../utils/get_chat_list_item_subtitle.dart | 16 +- lib/pangea/utils/overlay.dart | 62 +-- lib/pangea/utils/show_defintion_util.dart | 159 -------- lib/pangea/utils/toolbar_util.dart | 303 -------------- .../widgets/chat/message_audio_card.dart | 149 +++++++ .../widgets/chat/message_context_menu.dart | 69 ++++ .../widgets/chat/message_text_selection.dart | 34 ++ lib/pangea/widgets/chat/message_toolbar.dart | 303 ++++++++++++++ .../chat/message_translation_card.dart | 103 +++++ lib/pangea/widgets/chat/overlay_message.dart | 119 ++++++ .../widgets/chat/text_to_speech_button.dart | 1 - lib/pangea/widgets/igc/pangea_rich_text.dart | 198 +++------ lib/pangea/widgets/igc/word_data_card.dart | 118 +++--- pubspec.lock | 8 - pubspec.yaml | 2 +- 35 files changed, 1518 insertions(+), 1210 deletions(-) delete mode 100644 lib/pangea/utils/show_defintion_util.dart delete mode 100644 lib/pangea/utils/toolbar_util.dart create mode 100644 lib/pangea/widgets/chat/message_audio_card.dart create mode 100644 lib/pangea/widgets/chat/message_context_menu.dart create mode 100644 lib/pangea/widgets/chat/message_text_selection.dart create mode 100644 lib/pangea/widgets/chat/message_toolbar.dart create mode 100644 lib/pangea/widgets/chat/message_translation_card.dart create mode 100644 lib/pangea/widgets/chat/overlay_message.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ba582158c..4ccb42939 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3874,5 +3874,6 @@ "type": "text", "placeholders": {} }, - "showDefinition": "Show Definition" + "define": "Define", + "listen": "Listen" } \ No newline at end of file diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 4d1eddcbd..c63a6fa83 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:desktop_drop/desktop_drop.dart'; +import 'package:collection/collection.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:file_picker/file_picker.dart'; @@ -20,14 +20,17 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/student_analytics_summary_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; import 'package:fluffychat/pangea/utils/report_message.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; @@ -144,45 +147,45 @@ class ChatController extends State Timer? typingCoolDown; Timer? typingTimeout; bool currentlyTyping = false; - bool dragging = false; + // #Pangea + // bool dragging = false; - void onDragEntered(_) => setState(() => dragging = true); + // void onDragEntered(_) => setState(() => dragging = true); - void onDragExited(_) => setState(() => dragging = false); + // void onDragExited(_) => setState(() => dragging = false); - void onDragDone(DropDoneDetails details) async { - setState(() => dragging = false); - final bytesList = await showFutureLoadingDialog( - context: context, - future: () => Future.wait( - details.files.map( - (xfile) => xfile.readAsBytes(), - ), - ), - ); - if (bytesList.error != null) return; + // void onDragDone(DropDoneDetails details) async { + // setState(() => dragging = false); + // final bytesList = await showFutureLoadingDialog( + // context: context, + // future: () => Future.wait( + // details.files.map( + // (xfile) => xfile.readAsBytes(), + // ), + // ), + // ); + // if (bytesList.error != null) return; - final matrixFiles = []; - for (var i = 0; i < bytesList.result!.length; i++) { - matrixFiles.add( - MatrixFile( - bytes: bytesList.result![i], - name: details.files[i].name, - ).detectFileType, - ); - } - // #Pangea - if (matrixFiles.isEmpty) return; - // Pangea# + // final matrixFiles = []; + // for (var i = 0; i < bytesList.result!.length; i++) { + // matrixFiles.add( + // MatrixFile( + // bytes: bytesList.result![i], + // name: details.files[i].name, + // ).detectFileType, + // ); + // } + // if (matrixFiles.isEmpty) return; - await showAdaptiveDialog( - context: context, - builder: (c) => SendFileDialog( - files: matrixFiles, - room: room, - ), - ); - } + // await showAdaptiveDialog( + // context: context, + // builder: (c) => SendFileDialog( + // files: matrixFiles, + // room: room, + // ), + // ); + // } + // Pangea# bool get canSaveSelectedEvent => selectedEvents.length == 1 && @@ -1542,7 +1545,51 @@ class ChatController extends State lastState = currentState; return currentState; } - // #Pangea + + List get events => + timeline!.events.where((event) => event.isVisibleInGui).toList(); + + final Map _messageToolbarControllers = {}; + final Map _pangeaMessageEvents = {}; + + PangeaMessageEvent? pangeaMessageEvent(String eventId) { + final Event? event = + events.firstWhereOrNull((event) => event.eventId == eventId); + if (timeline == null || event == null || event.type != EventTypes.Message) { + return null; + } + if (!_pangeaMessageEvents.containsKey(eventId)) { + _pangeaMessageEvents[eventId] = PangeaMessageEvent( + event: event, + timeline: timeline!, + ownMessage: event.senderId == Matrix.of(context).client.userID, + ); + } + return _pangeaMessageEvents[eventId]; + } + + ToolbarDisplayController? messageToolbarController(String eventId) { + final Event? event = + events.firstWhereOrNull((event) => event.eventId == eventId); + if (timeline == null || event == null || event.type != EventTypes.Message) { + return null; + } + + final PangeaMessageEvent? messageEvent = pangeaMessageEvent(eventId); + if (messageEvent == null) return null; + + if (!_messageToolbarControllers.containsKey(event.eventId)) { + _messageToolbarControllers[event.eventId] = ToolbarDisplayController( + targetId: event.eventId, + pangeaMessageEvent: messageEvent, + immersionMode: choreographer.immersionMode, + controller: this, + ); + _messageToolbarControllers[event.eventId]!.setToolbar(); + } + return _messageToolbarControllers[eventId]; + } + // Pangea# @override Widget build(BuildContext context) => ChatView(this); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index e69b7d074..d8fc89ebc 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -24,9 +24,12 @@ class ChatEventList extends StatelessWidget { Widget build(BuildContext context) { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - final events = controller.timeline!.events - .where((event) => event.isVisibleInGui) - .toList(); + // #Pangea + // final events = controller.timeline!.events + // .where((event) => event.isVisibleInGui) + // .toList(); + final events = controller.events; + // Pangea# final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of @@ -153,6 +156,7 @@ class ChatEventList extends StatelessWidget { controller.choreographer.messageOptions.selectedDisplayLang, immersionMode: controller.choreographer.immersionMode, definitions: controller.choreographer.definitionsEnabled, + controller: controller, // Pangea# selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index efe45d96f..bd1653fe0 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -58,21 +58,18 @@ class ChatInputRow extends StatelessWidget { ), ) else - // #Pangea - PangeaMessageActions(chatController: controller), - // SizedBox( - // height: 56, - // child: TextButton( - // onPressed: controller.forwardEventsAction, - // child: Row( - // children: [ - // const Icon(Icons.keyboard_arrow_left_outlined), - // Text(L10n.of(context)!.forward), - // ], - // ), - // ), - // ), - // Pangea# + SizedBox( + height: 56, + child: TextButton( + onPressed: controller.forwardEventsAction, + child: Row( + children: [ + const Icon(Icons.keyboard_arrow_left_outlined), + Text(L10n.of(context)!.forward), + ], + ), + ), + ), controller.selectedEvents.length == 1 ? controller.selectedEvents.first .getDisplayEvent(controller.timeline!) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index cb6ce4737..9f20616f3 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,4 @@ import 'package:badges/badges.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; @@ -26,10 +25,7 @@ import '../../utils/stream_extension.dart'; import 'chat_emoji_picker.dart'; import 'chat_input_row.dart'; -//#Pangea -// enum _EventContextAction { info, report } -enum _EventContextAction { info, forward, report } -//Pangea# +enum _EventContextAction { info, report } class ChatView extends StatelessWidget { final ChatController controller; @@ -39,9 +35,6 @@ class ChatView extends StatelessWidget { List _appBarActions(BuildContext context) { if (controller.selectMode) { return [ -// #Pangea - LanguageDisplayToggle(controller: controller), - // Pangea# if (controller.canEditSelectedEvents) IconButton( icon: const Icon(Icons.edit_outlined), @@ -85,11 +78,6 @@ class ChatView extends StatelessWidget { case _EventContextAction.report: controller.reportEventAction(); break; - // #Pangea - case _EventContextAction.forward: - controller.forwardEventsAction(); - break; - // Pangea# } }, itemBuilder: (context) => [ @@ -105,17 +93,6 @@ class ChatView extends StatelessWidget { // ], // ), // ), - PopupMenuItem( - value: _EventContextAction.forward, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.forward), - const SizedBox(width: 12), - Text(L10n.of(context)!.forward), - ], - ), - ), // Pangea# if (controller.selectedEvents.single.status.isSent) PopupMenuItem( @@ -248,199 +225,199 @@ class ChatView extends StatelessWidget { : null) // #Pangea : null, - body: DropTarget( - onDragDone: controller.onDragDone, - onDragEntered: controller.onDragEntered, - onDragExited: controller.onDragExited, - child: Stack( - children: [ - SafeArea( - child: Column( - children: [ - TombstoneDisplay(controller), - if (scrollUpBannerEventId != null) - Material( - color: Theme.of(context) - .colorScheme - .surfaceVariant, - shape: Border( - bottom: BorderSide( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), - child: ListTile( - leading: IconButton( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - icon: const Icon(Icons.close), - tooltip: L10n.of(context)!.close, - onPressed: () { - controller - .discardScrollUpBannerEventId(); - controller.setReadMarker(); - }, - ), - title: Text( - L10n.of(context)!.jumpToLastReadMessage, - ), - contentPadding: - const EdgeInsets.only(left: 8), - trailing: TextButton( - onPressed: () { - controller.scrollToEventId( - scrollUpBannerEventId, - ); - controller - .discardScrollUpBannerEventId(); - }, - child: Text(L10n.of(context)!.jump), - ), + body: + // #Pangea + // DropTarget( + // onDragDone: controller.onDragDone, + // onDragEntered: controller.onDragEntered, + // onDragExited: controller.onDragExited, + // child: + // Pangea# + Stack( + children: [ + SafeArea( + child: Column( + children: [ + TombstoneDisplay(controller), + if (scrollUpBannerEventId != null) + Material( + color: Theme.of(context) + .colorScheme + .surfaceVariant, + shape: Border( + bottom: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, ), ), - PinnedEvents(controller), - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: Builder( - builder: (context) { - if (controller.timeline == null) { - return const Center( - child: CircularProgressIndicator - .adaptive( - strokeWidth: 2, - ), - ); - } - return ChatEventList( - controller: controller, - ); + child: ListTile( + leading: IconButton( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + icon: const Icon(Icons.close), + tooltip: L10n.of(context)!.close, + onPressed: () { + controller.discardScrollUpBannerEventId(); + controller.setReadMarker(); }, ), + title: Text( + L10n.of(context)!.jumpToLastReadMessage, + ), + contentPadding: + const EdgeInsets.only(left: 8), + trailing: TextButton( + onPressed: () { + controller.scrollToEventId( + scrollUpBannerEventId, + ); + controller.discardScrollUpBannerEventId(); + }, + child: Text(L10n.of(context)!.jump), + ), ), ), - if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join) - // #Pangea - // Container( - ConditionalFlexible( + PinnedEvents(controller), + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + child: Builder( + builder: (context) { + if (controller.timeline == null) { + return const Center( + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + return ChatEventList( + controller: controller, + ); + }, + ), + ), + ), + if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + // #Pangea + // Container( + ConditionalFlexible( + isScroll: controller.isRowScrollable, + child: ConditionalScroll( isScroll: controller.isRowScrollable, - child: ConditionalScroll( - isScroll: controller.isRowScrollable, - child: MeasurableWidget( - onChange: (size, position) { - controller.inputRowSize = size!.height; - }, - child: Container( - // Pangea# - margin: EdgeInsets.only( - bottom: bottomSheetPadding, - left: bottomSheetPadding, - right: bottomSheetPadding, - ), - constraints: const BoxConstraints( - maxWidth: - FluffyThemes.columnWidth * 2.5, - ), - alignment: Alignment.center, - child: Material( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular( - AppConfig.borderRadius, - ), - bottomRight: Radius.circular( - AppConfig.borderRadius, - ), + child: MeasurableWidget( + onChange: (size, position) { + controller.inputRowSize = size!.height; + }, + child: Container( + // Pangea# + margin: EdgeInsets.only( + bottom: bottomSheetPadding, + left: bottomSheetPadding, + right: bottomSheetPadding, + ), + constraints: const BoxConstraints( + maxWidth: + FluffyThemes.columnWidth * 2.5, + ), + alignment: Alignment.center, + child: Material( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular( + AppConfig.borderRadius, + ), + bottomRight: Radius.circular( + AppConfig.borderRadius, ), - elevation: 4, - shadowColor: - Colors.black.withAlpha(64), - clipBehavior: Clip.hardEdge, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.white - : Colors.black, - child: controller - .room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment - .spaceEvenly, - children: [ - TextButton.icon( - style: - TextButton.styleFrom( - padding: - const EdgeInsets - .all(16), - foregroundColor: - Theme.of(context) - .colorScheme - .error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: - controller.leaveChat, - label: Text( - L10n.of(context)!.leave, - ), - ), - TextButton.icon( - style: - TextButton.styleFrom( - padding: - const EdgeInsets - .all(16), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: controller - .recreateChat, - label: Text( - L10n.of(context)! - .reopenChat, - ), - ), - ], - ) - : Column( - mainAxisSize: - MainAxisSize.min, - children: [ - const ConnectionStatusHeader(), - ReactionsPicker(controller), - ReplyDisplay(controller), - ChatInputRow(controller), - ChatEmojiPicker(controller), - ], - ), ), + elevation: 4, + shadowColor: Colors.black.withAlpha(64), + clipBehavior: Clip.hardEdge, + color: Theme.of(context).brightness == + Brightness.light + ? Colors.white + : Colors.black, + child: controller + .room.isAbandonedDMRoom == + true + ? Row( + mainAxisAlignment: + MainAxisAlignment + .spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16), + foregroundColor: + Theme.of(context) + .colorScheme + .error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: + controller.leaveChat, + label: Text( + L10n.of(context)!.leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: + const EdgeInsets.all( + 16), + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: + controller.recreateChat, + label: Text( + L10n.of(context)! + .reopenChat, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ConnectionStatusHeader(), + ReactionsPicker(controller), + ReplyDisplay(controller), + ChatInputRow(controller), + ChatEmojiPicker(controller), + ], + ), ), ), ), ), - if (controller.dragging) - Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.9), - alignment: Alignment.center, - child: const Icon( - Icons.upload_outlined, - size: 100, - ), - ), - ], - ), + ), + // #Pangea + // if (controller.dragging) + // Container( + // color: Theme.of(context) + // .scaffoldBackgroundColor + // .withOpacity(0.9), + // alignment: Alignment.center, + // child: const Icon( + // Icons.upload_outlined, + // size: 100, + // ), + // ), + // Pangea# + ], ), - ], - ), + ), + ], ), + // ), ); }, ), @@ -481,4 +458,4 @@ class ConditionalScroll extends StatelessWidget { return child; } } -// #Pangea \ No newline at end of file +// #Pangea diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 2ea75ab74..2f43cfdbb 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -1,10 +1,8 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -21,7 +19,7 @@ class HtmlMessage extends StatelessWidget { final Room room; final Color textColor; // #Pangea - final ShowDefintionUtil? messageToolbar; + // final ShowDefintionUtil? messageToolbar; // Pangea# const HtmlMessage({ @@ -30,7 +28,7 @@ class HtmlMessage extends StatelessWidget { required this.room, this.textColor = Colors.black, // #Pangea - this.messageToolbar, + // this.messageToolbar, // Pangea# }); @@ -101,20 +99,20 @@ class HtmlMessage extends StatelessWidget { // there is no need to pre-validate the html, as we validate it while rendering // #Pangea return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, + // onHover: messageToolbar?.onMouseRegionUpdate, child: SelectionArea( - onSelectionChanged: (SelectedContent? selection) => - messageToolbar?.onTextSelection( - selectedContent: selection, - context: context, - ), - focusNode: messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - messageToolbar?.contextMenuOverride( - context: context, - contentSelection: state, - ) ?? - const SizedBox(), + // onSelectionChanged: (SelectedContent? selection) => + // messageToolbar?.onTextSelection( + // selectedContent: selection, + // context: context, + // ), + // focusNode: messageToolbar?.focusNode, + // contextMenuBuilder: (context, state) => + // messageToolbar?.contextMenuOverride( + // context: context, + // contentSelection: state, + // ) ?? + // const SizedBox(), // Pangea# child: Html.fromElement( documentElement: element as dom.Element, diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 36bdb9fee..4f4d06d86 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,7 +1,9 @@ import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_color.dart'; @@ -9,6 +11,7 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; @@ -40,6 +43,7 @@ class Message extends StatelessWidget { final LanguageModel? selectedDisplayLang; final bool immersionMode; final bool definitions; + final ChatController controller; // Pangea# const Message( @@ -61,6 +65,7 @@ class Message extends StatelessWidget { required this.selectedDisplayLang, required this.immersionMode, required this.definitions, + required this.controller, // Pangea# super.key, }); @@ -138,12 +143,10 @@ class Message extends StatelessWidget { } // #Pangea - final pangeaMessageEvent = PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: ownMessage, - selected: selected, - ); + final PangeaMessageEvent? pangeaMessageEvent = + controller.pangeaMessageEvent(event.eventId); + final ToolbarDisplayController? toolbarController = + controller.messageToolbarController(event.eventId); // Pangea# final resetAnimateIn = this.resetAnimateIn; @@ -241,25 +244,13 @@ class Message extends StatelessWidget { alignment: alignment, padding: const EdgeInsets.only(left: 8), child: GestureDetector( - onTap: () => print("got message tap"), - onDoubleTap: () => print("got message double tap"), - onDoubleTapDown: (details) => - print("got message double tap down"), - onLongPress: longPressSelect - ? selected - ? null - : () => print('long press') - : () { - onSelect(event); - // Android usually has a vibration effect on long press: - if (PlatformInfos.isAndroid) { - Vibration.hasVibrator().then((has) { - if (has == true) { - Vibration.vibrate(duration: 50); - } - }); - } - }, + onTap: () => toolbarController?.showToolbar(context), + onDoubleTap: () => + toolbarController?.showToolbar(context), + onLongPress: () { + onSelect(event); + HapticFeedback.selectionClick(); + }, child: AnimatedOpacity( opacity: animateIn ? 0 @@ -356,9 +347,8 @@ class Message extends StatelessWidget { // #Pangea selected: selected, pangeaMessageEvent: pangeaMessageEvent, - selectedDisplayLang: selectedDisplayLang, immersionMode: immersionMode, - definitions: definitions, + toolbarController: toolbarController, // Pangea# ), if (event.hasAggregatedEvents( @@ -366,7 +356,8 @@ class Message extends StatelessWidget { RelationshipTypes.edit, ) // #Pangea || - (pangeaMessageEvent.showUseType) + (pangeaMessageEvent?.showUseType ?? + false) // Pangea# ) Padding( @@ -378,8 +369,9 @@ class Message extends StatelessWidget { children: [ // #Pangea if (pangeaMessageEvent - .showUseType) ...[ - pangeaMessageEvent.useType + ?.showUseType ?? + false) ...[ + pangeaMessageEvent!.useType .iconView( context, textColor.withAlpha(164), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 7a7c0b5bb..fd4791d3c 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -2,7 +2,9 @@ import 'package:fluffychat/pages/chat/events/html_message.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -31,27 +33,24 @@ class MessageContent extends StatelessWidget { final BorderRadius borderRadius; // #Pangea final bool selected; - final PangeaMessageEvent pangeaMessageEvent; + final PangeaMessageEvent? pangeaMessageEvent; //question: are there any performance benefits to using booleans //here rather than passing the choreographer? pangea rich text, a widget //further down in the chain is also using pangeaController so its not constant - final LanguageModel? selectedDisplayLang; final bool immersionMode; - final bool definitions; - ShowDefintionUtil? messageToolbar; + final ToolbarDisplayController? toolbarController; // Pangea# - MessageContent( + const MessageContent( this.event, { this.onInfoTab, super.key, required this.textColor, // #Pangea required this.selected, - required this.pangeaMessageEvent, - required this.selectedDisplayLang, + this.pangeaMessageEvent, required this.immersionMode, - required this.definitions, + required this.toolbarController, // Pangea# required this.borderRadius, }); @@ -124,18 +123,6 @@ class MessageContent extends StatelessWidget { @override Widget build(BuildContext context) { - // #Pangea - messageToolbar = ShowDefintionUtil( - targetId: pangeaMessageEvent.eventId, - room: pangeaMessageEvent.room, - langCode: selectedDisplayLang?.langCode ?? - MatrixState.pangeaController.languageController.activeL2Code( - roomID: pangeaMessageEvent.room.id, - ) ?? - LanguageModel.unknown.langCode, - messageText: "", - ); - // Pangea# final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final buttonTextColor = textColor; switch (event.type) { @@ -186,7 +173,7 @@ class MessageContent extends StatelessWidget { event.isRichMessage // #Pangea && - !pangeaMessageEvent.showRichText + !(pangeaMessageEvent?.showRichText ?? false) // Pangea# ) { var html = event.formattedText; @@ -194,13 +181,13 @@ class MessageContent extends StatelessWidget { html = '* $html'; } // #Pangea - messageToolbar?.messageText = html; + // messageToolbar?.messageText = html; // Pangea# return HtmlMessage( html: html, textColor: textColor, room: event.room, - messageToolbar: messageToolbar, + // messageToolbar: messageToolbar, ); } // else we fall through to the normal message rendering @@ -286,85 +273,86 @@ class MessageContent extends StatelessWidget { decoration: event.redacted ? TextDecoration.lineThrough : null, height: 1.3, ); - if (pangeaMessageEvent.showRichText) { - return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, - child: PangeaRichText( - style: messageTextStyle, - selected: selected, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: immersionMode, - definitions: definitions, - selectedDisplayLang: selectedDisplayLang, - messageToolbar: messageToolbar, - ), + if (pangeaMessageEvent?.showRichText ?? false) { + return PangeaRichText( + style: messageTextStyle, + pangeaMessageEvent: pangeaMessageEvent!, + immersionMode: immersionMode, + toolbarController: toolbarController!, + // selectedDisplayLang: selectedDisplayLang, + // highlighted: toolbarController!.highlighted, ); } - return MouseRegion( - onHover: messageToolbar?.onMouseRegionUpdate, - child: FutureBuilder( - // Pangea# - future: event.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ), - builder: (context, snapshot) { - // #Pangea - if (!snapshot.hasData) { - return Text( - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ), - style: messageTextStyle, - ); - } - // return Linkify( - final String messageText = snapshot.data ?? - event.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - ); - messageToolbar?.messageText = messageText; - return SelectableLinkify( - // Pangea# - text: messageText, - focusNode: messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - messageToolbar?.contextMenuOverride( - context: context, - textSelection: state, - ) ?? - const SizedBox(), - // text: snapshot.data ?? - // event.calcLocalizedBodyFallback( - // MatrixLocals(L10n.of(context)!), - // hideReply: true, - // ), - style: TextStyle( - color: textColor, - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: - event.redacted ? TextDecoration.lineThrough : null, - ), - options: const LinkifyOptions(humanize: false), - linkStyle: TextStyle( - color: textColor.withAlpha(150), - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decoration: TextDecoration.underline, - decorationColor: textColor.withAlpha(150), - ), - onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - onSelectionChanged: (selection, cause) => - messageToolbar?.onTextSelection( - selectedText: selection, - cause: cause, - context: context, - ), - onTap: () => messageToolbar?.onTextTap(context), - ); - }, + return FutureBuilder( + // Pangea# + future: event.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, ), + builder: (context, snapshot) { + // #Pangea + if (!snapshot.hasData) { + return Text( + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ), + style: messageTextStyle, + ); + } + // return Linkify( + final String messageText = snapshot.data ?? + event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + ); + toolbarController?.toolbar?.textSelection.setMessageText( + messageText, + ); + return SelectableLinkify( + onSelectionChanged: (selection, cause) => toolbarController + ?.toolbar?.textSelection + .onTextSelection(selection), + onTap: () => toolbarController?.showToolbar(context), + // Pangea# + text: toolbarController?.toolbar?.textSelection.messageText ?? + messageText, + focusNode: toolbarController?.focusNode, + contextMenuBuilder: (context, state) => + MessageContextMenu.contextMenuOverride( + context: context, + textSelection: state, + onDefine: () => toolbarController?.showToolbar( + context, + mode: MessageMode.definition, + ), + onListen: () => toolbarController?.showToolbar( + context, + mode: MessageMode.play, + ), + ), + // text: snapshot.data ?? + // event.calcLocalizedBodyFallback( + // MatrixLocals(L10n.of(context)!), + // hideReply: true, + // ), + style: TextStyle( + color: textColor, + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: + event.redacted ? TextDecoration.lineThrough : null, + ), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: textColor.withAlpha(150), + fontSize: bigEmotes ? fontSize * 3 : fontSize, + decoration: TextDecoration.underline, + decorationColor: textColor.withAlpha(150), + ), + onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), + // onTap: () => messageToolbar?.onTextTap(context), + ); + }, ); } case EventTypes.CallInvite: diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 9775beb3c..b4047e1e4 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -269,6 +269,7 @@ class ITController { completedITSteps.add(itStep); showChoiceFeedback = true; + Future.delayed( const Duration( milliseconds: ChoreoConstants.millisecondsToDisplayFeedback, diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 9fa092215..6f5cca545 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -224,39 +224,40 @@ class ITChoices extends StatelessWidget { int index, [ Color? borderColor, String? choiceFeedback, - ]) => - OverlayUtil.showPositionedCard( - context: context, - cardToShow: choiceFeedback == null - ? WordDataCard( - word: controller.currentITStep!.continuances[index].text, - wordLang: controller.targetLangCode, - fullText: sourceText ?? controller.choreographer.currentText, - fullTextLang: sourceText != null - ? controller.sourceLangCode - : controller.targetLangCode, - hasInfo: controller.currentITStep!.continuances[index].hasInfo, - choiceFeedback: choiceFeedback, - room: controller.choreographer.chatController.room, - ) - : ITFeedbackCard( - req: ITFeedbackRequestModel( - sourceText: sourceText!, - currentText: controller.choreographer.currentText, - chosenContinuance: - controller.currentITStep!.continuances[index].text, - bestContinuance: controller.currentITStep!.best.text, - feedbackLang: controller.targetLangCode, - sourceTextLang: controller.sourceLangCode, - targetLang: controller.targetLangCode, - ), - choiceFeedback: choiceFeedback, + ]) { + OverlayUtil.showPositionedCard( + context: context, + cardToShow: choiceFeedback == null + ? WordDataCard( + word: controller.currentITStep!.continuances[index].text, + wordLang: controller.targetLangCode, + fullText: sourceText ?? controller.choreographer.currentText, + fullTextLang: sourceText != null + ? controller.sourceLangCode + : controller.targetLangCode, + hasInfo: controller.currentITStep!.continuances[index].hasInfo, + choiceFeedback: choiceFeedback, + room: controller.choreographer.chatController.room, + ) + : ITFeedbackCard( + req: ITFeedbackRequestModel( + sourceText: sourceText!, + currentText: controller.choreographer.currentText, + chosenContinuance: + controller.currentITStep!.continuances[index].text, + bestContinuance: controller.currentITStep!.best.text, + feedbackLang: controller.targetLangCode, + sourceTextLang: controller.sourceLangCode, + targetLang: controller.targetLangCode, ), - cardSize: const Size(300, 300), - borderColor: borderColor, - transformTargetId: controller.choreographer.itBarTransformTargetKey, - backDropToDismiss: false, - ); + choiceFeedback: choiceFeedback, + ), + cardSize: const Size(300, 300), + borderColor: borderColor, + transformTargetId: controller.choreographer.itBarTransformTargetKey, + backDropToDismiss: false, + ); + } @override Widget build(BuildContext context) { diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index e60b44b40..558fc55b2 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'dart:math'; -import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/controllers/class_controller.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/language_controller.dart'; @@ -225,19 +224,19 @@ class PangeaController { continue; } final List userIds = participants.map((user) => user.id).toList(); - if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { - try { - await space.invite(BotName.byEnvironment); - await space.setPower( - BotName.byEnvironment, - ClassDefaultValues.powerLevelOfAdmin, - ); - } catch (err) { - ErrorHandler.logError( - e: "Failed to invite pangea bot to space ${space.id}", - ); - } - } + // if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { + // try { + // await space.invite(BotName.byEnvironment); + // await space.setPower( + // BotName.byEnvironment, + // ClassDefaultValues.powerLevelOfAdmin, + // ); + // } catch (err) { + // ErrorHandler.logError( + // e: "Failed to invite pangea bot to space ${space.id}", + // ); + // } + // } } } } diff --git a/lib/pangea/controllers/text_to_speech_controller.dart b/lib/pangea/controllers/text_to_speech_controller.dart index 4db6caa6e..10b204631 100644 --- a/lib/pangea/controllers/text_to_speech_controller.dart +++ b/lib/pangea/controllers/text_to_speech_controller.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/config/environment.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/network/urls.dart'; +import 'package:flutter/material.dart'; import 'package:http/http.dart'; import '../network/requests.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 70d6834ec..f15a9e87d 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -562,7 +562,6 @@ extension PangeaRoom on Room { event: event, timeline: timeline, ownMessage: true, - selected: false, ); msgs.add( RecentMessageRecord( diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 549b8afa5..390912fd5 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -283,10 +283,20 @@ class IGCTextData { nextTokenIndex = matchTokens.length; } - final String matchText = originalInput.substring( - matchTokens[tokenIndex].token.text.offset, - matchTokens[nextTokenIndex - 1].token.end, - ); + String matchText; + try { + matchText = originalInput.substring( + matchTokens[tokenIndex].token.text.offset, + matchTokens[nextTokenIndex - 1].token.end, + ); + } catch (err) { + return [ + TextSpan( + text: originalInput, + style: defaultStyle, + ), + ]; + } items.add( TextSpan( diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index b7caf64d4..d46ad0dc3 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/message_data_models.dart'; import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; import 'package:fluffychat/pangea/utils/bot_name.dart'; @@ -25,14 +26,13 @@ class PangeaMessageEvent { late Event _event; final Timeline timeline; final bool ownMessage; - final bool selected; bool _isValidPangeaMessageEvent = true; + RepresentationEvent? _displayRepresentation; PangeaMessageEvent({ required Event event, required this.timeline, required this.ownMessage, - required this.selected, }) { if (event.type != EventTypes.Message) { _isValidPangeaMessageEvent = false; @@ -46,6 +46,8 @@ class PangeaMessageEvent { //the timeline filters the edits and uses the original events //so this event will always be the original and the sdk getter body //handles getting the latest text from the aggregated events + Event get event => _event; + String get body => _event.body; String get senderId => _event.senderId; @@ -79,7 +81,7 @@ class PangeaMessageEvent { if ([EventStatus.error, EventStatus.sending].contains(_event.status)) { return false; } - if (ownMessage && !selected) return false; + // if (ownMessage && !selected) return false; return true; } @@ -87,13 +89,13 @@ class PangeaMessageEvent { //get audio for text and language //if no audio exists, create it //if audio exists, return it - Future getAudioGlobal(String langCode) async { + Future getAudioGlobal(String langCode) async { // try { final String text = representationByLanguage(langCode)?.text ?? body; final local = getAudioLocal(langCode, text); - if (local != null) return Future.value(local.eventId); + if (local != null) return Future.value(local); final TextToSpeechRequest params = TextToSpeechRequest( text: text, @@ -132,69 +134,61 @@ class PangeaMessageEvent { throw Exception("Unexpected mime type: ${file.mimeType}"); } - return room.sendFileEvent( - file, - inReplyTo: _event, - extraContent: { - 'info': { - ...file.info, - 'duration': response.durationMillis, + try { + final String? eventId = await room.sendFileEvent( + file, + inReplyTo: _event, + extraContent: { + 'info': { + ...file.info, + 'duration': response.durationMillis, + }, + 'org.matrix.msc3245.voice': {}, + 'org.matrix.msc1767.audio': { + 'duration': response.durationMillis, + 'waveform': response.waveform, + }, + ModelKey.transcription: { + ModelKey.text: text, + ModelKey.langCode: langCode, + }, }, - 'org.matrix.msc3245.voice': {}, - 'org.matrix.msc1767.audio': { - 'duration': response.durationMillis, - 'waveform': response.waveform, - }, - ModelKey.transcription: { - ModelKey.text: text, - ModelKey.langCode: langCode, - }, - }, - ).timeout( - Durations.long4, - onTimeout: () { - debugPrint("timeout in getAudioGlobal"); - return null; - }, - ).then((eventId) { + ); + // .timeout( + // Durations.long4, + // onTimeout: () { + // debugPrint("timeout in getAudioGlobal"); + // return null; + // }, + // ); + debugPrint("eventId in getAudioGlobal $eventId"); - return eventId; - }).catchError((err, s) { + return eventId != null ? room.getEventById(eventId) : null; + } catch (err) { debugPrint("error in getAudioGlobal"); - debugPrint(err); - debugPrint(s); debugger(when: kDebugMode); return null; - }); - - // } catch (err, s) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // e: err, - // s: s, - // ); - // return Future.value(null); - // } + } } Event? getAudioLocal(String langCode, String text) { return allAudio.firstWhereOrNull( (element) { // Safely access the transcription map - final transcription = element.content.tryGet(ModelKey.transcription); + final transcription = element.content.tryGetMap(ModelKey.transcription); - return transcription != null; - // if (transcription == null) { - // // If transcription is null, this element does not match. - // return false; - // } + // return transcription != null; + if (transcription == null) { + // If transcription is null, this element does not match. + return false; + } - // // Safely get language code and text from the transcription - // final elementLangCode = transcription.tryGet(ModelKey.langCode); - // final elementText = transcription.tryGet(ModelKey.text); + // Safely get language code and text from the transcription + final elementLangCode = transcription[ModelKey.langCode]; + final elementText = transcription[ModelKey.text]; - // // Check if both language code and text match - // return elementLangCode == langCode && elementText == text; + // Check if both language code and text match + return elementLangCode == langCode && elementText == text; }, ); } @@ -397,14 +391,53 @@ class PangeaMessageEvent { !room.isUserSpaceAdmin(_event.senderId) && _event.messageType != PangeaMessageTypes.report; + String get messageDisplayLangCode { + final bool immersionMode = MatrixState + .pangeaController.permissionsController + .isToolEnabled(ToolSetting.immersionMode, room); + final String? l2Code = MatrixState.pangeaController.languageController + .activeL2Code(roomID: room.id); + + final String? langCode = immersionMode ? l2Code : originalWritten?.langCode; + return langCode ?? LanguageKeys.unknownLanguage; + } + + Future getDisplayRepresentation( + BuildContext context, + ) async { + if (messageDisplayLangCode == LanguageKeys.unknownLanguage) return null; + if (_displayRepresentation != null) return _displayRepresentation; + _displayRepresentation = representationByLanguage(messageDisplayLangCode); + if (_displayRepresentation != null) { + return _displayRepresentation; + } + + try { + _displayRepresentation = await representationByLanguageGlobal( + context: context, + langCode: messageDisplayLangCode, + ); + return _displayRepresentation; + } catch (err, s) { + ErrorHandler.logError( + m: "error in getDisplayRepresentation", + e: err, + s: s, + ); + return null; + } + } + + String get displayMessageText => _displayRepresentation?.text ?? body; + // List get activities => //each match is turned into an activity that other students can access //they're not told the answer but have to find it themselves //the message has a blank piece which they fill in themselves // replication of logic from message_content.dart - bool get isHtml => - AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + // bool get isHtml => + // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; } class URLFinder { diff --git a/lib/pangea/network/urls.dart b/lib/pangea/network/urls.dart index 912376e45..d559065ee 100644 --- a/lib/pangea/network/urls.dart +++ b/lib/pangea/network/urls.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/config/environment.dart'; /// https://api.staging.pangea.chat/api/v1/ class PApiUrls { static String baseAPI = Environment.baseAPI; - static String choreoBaseApi = Environment.choreoApi; /// ---------------------- Languages -------------------------------------- static String getLanguages = "/languages"; @@ -51,7 +50,7 @@ class PApiUrls { static String firstStep = "/it_initialstep"; static String subseqStep = "/it_step"; - static String textToSpeech = "$choreoBaseApi/text_to_speech"; + static String textToSpeech = "${Environment.choreoApi}/text_to_speech"; ///-------------------------------- revenue cat -------------------------- static String rcApiV1 = "https://api.revenuecat.com/v1"; diff --git a/lib/pangea/repo/interactive_translation_repo.dart b/lib/pangea/repo/interactive_translation_repo.dart index b46db4b75..ab5d3b1d6 100644 --- a/lib/pangea/repo/interactive_translation_repo.dart +++ b/lib/pangea/repo/interactive_translation_repo.dart @@ -14,7 +14,7 @@ class ITRepo { CustomInputRequestModel initalText, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); final Response res = @@ -29,7 +29,7 @@ class ITRepo { SystemChoiceRequestModel subseqText, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); diff --git a/lib/pangea/repo/message_service.repo.dart b/lib/pangea/repo/message_service.repo.dart index 612ee0c61..ce51a3802 100644 --- a/lib/pangea/repo/message_service.repo.dart +++ b/lib/pangea/repo/message_service.repo.dart @@ -8,7 +8,7 @@ class MessageServiceRepo { String messageId, ) async { final Requests req = Requests( - baseUrl: PApiUrls.choreoBaseApi, + baseUrl: Environment.choreoApi, choreoApiKey: Environment.choreoApiKey, ); diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index fd6124420..d951ab449 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/lib/pangea/utils/download_chat.dart b/lib/pangea/utils/download_chat.dart index c1c28935d..9e3d4d423 100644 --- a/lib/pangea/utils/download_chat.dart +++ b/lib/pangea/utils/download_chat.dart @@ -136,7 +136,6 @@ List getPangeaMessageEvents( event: message, timeline: timeline, ownMessage: false, - selected: false, ), ) .cast() diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index a1ce04349..82b3461c4 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -17,9 +17,11 @@ class GetChatListItemSubtitle { ) async { if (event == null) return L10n.of(context)!.emptyChat; // try { - if (event.type != EventTypes.Message || - !pangeaController.permissionsController - .isToolEnabled(ToolSetting.immersionMode, event.room)) { + if (event.type != EventTypes.Message) + // || + // !pangeaController.permissionsController + // .isToolEnabled(ToolSetting.immersionMode, event.room)) + { return event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), hideReply: true, @@ -31,13 +33,17 @@ class GetChatListItemSubtitle { ); } + String? eventContextId = event.eventId; + if (!event.eventId.isValidMatrixId || event.eventId.sigil != '\$') { + eventContextId = null; + } final Timeline timeline = - await event.room.getTimeline(eventContextId: event.eventId); + await event.room.getTimeline(eventContextId: eventContextId); + final PangeaMessageEvent pangeaMessageEvent = PangeaMessageEvent( event: event, timeline: timeline, ownMessage: false, - selected: false, ); final l2Code = pangeaController.languageController.activeL2Code(roomID: event.roomId); diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 2d8c465eb..26399c8aa 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -15,11 +15,16 @@ class OverlayUtil { static showOverlay({ required BuildContext context, required Widget child, - required Size size, required String transformTargetId, + // Size? size, + double? width, + double? height, Offset? offset, backDropToDismiss = true, Color? borderColor, + Color? backgroundColor, + Alignment? targetAnchor, + Alignment? followerAnchor, }) { try { MatrixState.pAnyState.closeOverlay(); @@ -27,35 +32,37 @@ class OverlayUtil { MatrixState.pAnyState.layerLinkAndKey(transformTargetId); final OverlayEntry entry = OverlayEntry( - builder: (context) => Stack( - children: [ - // GestureDetector to detect when dismissed by clicking outside - Positioned.fill( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - MatrixState.pAnyState.closeOverlay(); - }, + builder: (context) => AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: Stack( + children: [ + if (backDropToDismiss) + TransparentBackdrop( + backgroundColor: backgroundColor, + ), + Positioned( + width: width, + height: height, + child: CompositedTransformFollower( + targetAnchor: targetAnchor ?? Alignment.topLeft, + followerAnchor: followerAnchor ?? Alignment.topLeft, + link: layerLinkAndKey.link, + showWhenUnlinked: false, + offset: offset ?? Offset.zero, + child: child, + ), ), - ), - if (backDropToDismiss) const TransparentBackdrop(), - Positioned( - width: size.width, - height: size.height, - child: CompositedTransformFollower( - link: layerLinkAndKey.link, - showWhenUnlinked: false, - offset: offset ?? Offset.zero, - child: child, - ), - ), - ], + ], + ), ), ); MatrixState.pAnyState.openOverlay(entry, context); } catch (err, stack) { - debugger(when: kDebugMode); + debugPrint("ERROR: $err"); + debugPrint("STACK: $stack"); + // debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); } } @@ -90,7 +97,8 @@ class OverlayUtil { showOverlay( context: context, child: child, - size: cardSize, + width: cardSize.width, + height: cardSize.height, transformTargetId: transformTargetId, offset: cardOffset, backDropToDismiss: backDropToDismiss, @@ -174,15 +182,17 @@ class OverlayUtil { } class TransparentBackdrop extends StatelessWidget { + final Color? backgroundColor; const TransparentBackdrop({ super.key, + this.backgroundColor, }); @override Widget build(BuildContext context) { return Material( borderOnForeground: false, - color: Colors.transparent, + color: backgroundColor ?? Colors.transparent, clipBehavior: Clip.antiAlias, child: InkWell( hoverColor: Colors.transparent, diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart deleted file mode 100644 index 6aa3ba29f..000000000 --- a/lib/pangea/utils/show_defintion_util.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:async'; - -import 'package:fluffychat/pangea/utils/any_state_holder.dart'; -import 'package:fluffychat/pangea/utils/overlay.dart'; -import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -class ShowDefintionUtil { - String messageText; - final String langCode; - final String targetId; - final FocusNode focusNode = FocusNode(); - final Room room; - String? textSelection; - bool inCooldown = false; - double? dx; - double? dy; - - ShowDefintionUtil({ - required this.targetId, - required this.room, - required this.langCode, - required this.messageText, - }); - - void onTextSelection({ - required BuildContext context, - TextSelection? selectedText, - SelectedContent? selectedContent, - SelectionChangedCause? cause, - }) { - if ((selectedText == null && selectedContent == null) || - selectedText?.isCollapsed == true) { - clearTextSelection(); - return; - } - textSelection = selectedText != null - ? selectedText.textInside(messageText) - : selectedContent!.plainText; - - if (BrowserContextMenu.enabled && kIsWeb) { - BrowserContextMenu.disableContextMenu(); - } - - if (kIsWeb && cause != SelectionChangedCause.tap) { - handleToolbar(context); - } - } - - void clearTextSelection() { - textSelection = null; - if (kIsWeb && !BrowserContextMenu.enabled) { - BrowserContextMenu.enableContextMenu(); - } - } - - void handleToolbar(BuildContext context) async { - if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return; - inCooldown = true; - Timer(const Duration(milliseconds: 750), () => inCooldown = false); - await Future.delayed(const Duration(milliseconds: 750)); - showToolbar(context); - } - - void showDefinition(BuildContext context) { - if (textSelection == null) return; - OverlayUtil.showPositionedCard( - context: context, - cardToShow: WordDataCard( - word: textSelection!, - wordLang: langCode, - fullText: messageText, - fullTextLang: langCode, - hasInfo: false, - room: room, - ), - cardSize: const Size(300, 300), - transformTargetId: targetId, - backDropToDismiss: false, - ); - } - - // web toolbar - Future showToolbar(BuildContext context) async { - final LayerLinkAndKey layerLinkAndKey = - MatrixState.pAnyState.layerLinkAndKey(targetId); - - final RenderObject? targetRenderBox = - layerLinkAndKey.key.currentContext!.findRenderObject(); - final Offset transformTargetOffset = - (targetRenderBox as RenderBox).localToGlobal(Offset.zero); - - if (dx != null && dx! > MediaQuery.of(context).size.width - 130) { - dx = MediaQuery.of(context).size.width - 130; - } - final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0; - final double yOffset = - dy != null ? dy! - transformTargetOffset.dy + 10 : 10; - - OverlayUtil.showOverlay( - context: context, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.zero, - ), - onPressed: () { - showDefinition(context); - }, - child: Text( - L10n.of(context)!.showDefinition, - style: const TextStyle( - fontSize: 14, - ), - ), - ), - size: const Size(130, 45), - transformTargetId: targetId, - offset: Offset(xOffset, yOffset), - ); - } - - void onMouseRegionUpdate(PointerEvent event) { - dx = event.position.dx; - dy = event.position.dy; - } - - Widget contextMenuOverride({ - required BuildContext context, - EditableTextState? textSelection, - SelectableRegionState? contentSelection, - }) { - if (textSelection == null && contentSelection == null) { - return const SizedBox(); - } - return AdaptiveTextSelectionToolbar.buttonItems( - anchors: textSelection?.contextMenuAnchors ?? - contentSelection!.contextMenuAnchors, - buttonItems: [ - if (textSelection != null) ...textSelection.contextMenuButtonItems, - if (contentSelection != null) - ...contentSelection.contextMenuButtonItems, - ContextMenuButtonItem( - label: L10n.of(context)!.showDefinition, - onPressed: () { - showDefinition(context); - focusNode.unfocus(); - }, - ), - ], - ); - } -} diff --git a/lib/pangea/utils/toolbar_util.dart b/lib/pangea/utils/toolbar_util.dart deleted file mode 100644 index 4848269fb..000000000 --- a/lib/pangea/utils/toolbar_util.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'dart:async'; - -import 'package:fluffychat/pangea/utils/any_state_holder.dart'; -import 'package:fluffychat/pangea/utils/overlay.dart'; -import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - -enum MessageMode { translation, play, definition, image, spellCheck } - -class MessageOverlayController { - OverlayEntry? _overlayEntry; - final BuildContext _context; - final GlobalKey _targetKey; - MessageMode? _currentMode; - AnimationController? _animationController; - - MessageOverlayController(this._context, this._targetKey) { - _animationController = AnimationController( - vsync: Navigator.of(_context), // Using the Navigator's TickerProvider - duration: const Duration(milliseconds: 300), - ); - } - - void showOverlay() { - final RenderBox renderBox = - _targetKey.currentContext?.findRenderObject() as RenderBox; - final Offset offset = renderBox.localToGlobal(Offset.zero); - final Size size = renderBox.size; - final double screenWidth = MediaQuery.of(_context).size.width; - - // Determines if there is more room above or below the RenderBox - final bool isBottomRoomAvailable = - MediaQuery.of(_context).size.height - (offset.dy + size.height) >= - size.height; - final double topPosition = isBottomRoomAvailable - ? offset.dy + size.height - : offset.dy - size.height; - - // Ensure the overlay does not overflow the screen horizontally - double leftPosition = offset.dx + size.width / 2 - screenWidth / 2; - leftPosition = leftPosition < 0 ? 0 : leftPosition; - final double rightPosition = - leftPosition + screenWidth > MediaQuery.of(_context).size.width - ? MediaQuery.of(_context).size.width - leftPosition - screenWidth - : leftPosition; - - _overlayEntry = OverlayEntry( - builder: (context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - left: leftPosition, - right: rightPosition, - top: isBottomRoomAvailable ? topPosition : null, - bottom: isBottomRoomAvailable - ? null - : MediaQuery.of(_context).size.height - - topPosition - - size.height, - child: AnimatedSize( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300), - child: Material( - elevation: 4.0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: MessageMode.values.map((mode) { - return IconButton( - icon: Icon(_getIconData(mode)), - onPressed: () { - setState(() { - _currentMode = mode; - }); - _animationController?.forward(); - }, - ); - }).toList(), - ), - SizeTransition( - sizeFactor: CurvedAnimation( - parent: _animationController!, - curve: Curves.fastOutSlowIn, - ), - axisAlignment: -1.0, - child: _buildModeContent(), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ); - - Overlay.of(_context).insert(_overlayEntry!); - } - - void hideOverlay() { - _overlayEntry?.remove(); - _overlayEntry = null; - _animationController?.reverse(); - } - - Widget _buildModeContent() { - switch (_currentMode) { - case MessageMode.translation: - return const Text('Translation Mode'); - case MessageMode.play: - return const Text('Play Mode'); - case MessageMode.definition: - return const Text('Definition Mode'); - case MessageMode.image: - return const Text('Image Mode'); - case MessageMode.spellCheck: - return const Text('SpellCheck Mode'); - default: - return const SizedBox - .shrink(); // Empty container for the default case, meaning no content - } - } - - IconData _getIconData(MessageMode mode) { - switch (mode) { - case MessageMode.translation: - return Icons.g_translate; - case MessageMode.play: - return Icons.play_arrow; - case MessageMode.definition: - return Icons.book; - case MessageMode.image: - return Icons.image; - case MessageMode.spellCheck: - return Icons.spellcheck; - default: - return Icons.error; // Icon to indicate an error or unsupported mode - } - } - - void dispose() { - _overlayEntry?.dispose(); - _animationController?.dispose(); - } -} - -class ShowDefintionUtil { - String messageText; - final String langCode; - final String targetId; - final FocusNode focusNode = FocusNode(); - final Room room; - String? textSelection; - bool inCooldown = false; - double? dx; - double? dy; - - ShowDefintionUtil({ - required this.targetId, - required this.room, - required this.langCode, - required this.messageText, - }); - - void onTextSelection({ - required BuildContext context, - TextSelection? selectedText, - SelectedContent? selectedContent, - SelectionChangedCause? cause, - }) { - if ((selectedText == null && selectedContent == null) || - selectedText?.isCollapsed == true) { - clearTextSelection(); - return; - } - textSelection = selectedText != null - ? selectedText.textInside(messageText) - : selectedContent!.plainText; - - if (BrowserContextMenu.enabled && kIsWeb) { - BrowserContextMenu.disableContextMenu(); - } - - if (kIsWeb && cause != SelectionChangedCause.tap) { - handleToolbar(context); - } - } - - void clearTextSelection() { - textSelection = null; - if (kIsWeb && !BrowserContextMenu.enabled) { - BrowserContextMenu.enableContextMenu(); - } - } - - void handleToolbar(BuildContext context) async { - if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return; - inCooldown = true; - Timer(const Duration(milliseconds: 750), () => inCooldown = false); - await Future.delayed(const Duration(milliseconds: 750)); - showToolbar(context); - } - - void showDefinition(BuildContext context) { - if (textSelection == null) return; - OverlayUtil.showPositionedCard( - context: context, - cardToShow: WordDataCard( - word: textSelection!, - wordLang: langCode, - fullText: messageText, - fullTextLang: langCode, - hasInfo: false, - room: room, - ), - cardSize: const Size(300, 300), - transformTargetId: targetId, - backDropToDismiss: false, - ); - } - - // web toolbar - Future showToolbar(BuildContext context) async { - final LayerLinkAndKey layerLinkAndKey = - MatrixState.pAnyState.layerLinkAndKey(targetId); - - final RenderObject? targetRenderBox = - layerLinkAndKey.key.currentContext!.findRenderObject(); - final Offset transformTargetOffset = - (targetRenderBox as RenderBox).localToGlobal(Offset.zero); - - if (dx != null && dx! > MediaQuery.of(context).size.width - 130) { - dx = MediaQuery.of(context).size.width - 130; - } - final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0; - final double yOffset = - dy != null ? dy! - transformTargetOffset.dy + 10 : 10; - - OverlayUtil.showOverlay( - context: context, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - minimumSize: Size.zero, - padding: EdgeInsets.zero, - ), - onPressed: () { - showDefinition(context); - }, - child: Text( - L10n.of(context)!.showDefinition, - style: const TextStyle( - fontSize: 14, - ), - ), - ), - size: const Size(130, 45), - transformTargetId: targetId, - offset: Offset(xOffset, yOffset), - ); - } - - void onMouseRegionUpdate(PointerEvent event) { - dx = event.position.dx; - dy = event.position.dy; - } - - Widget contextMenuOverride({ - required BuildContext context, - EditableTextState? textSelection, - SelectableRegionState? contentSelection, - }) { - if (textSelection == null && contentSelection == null) { - return const SizedBox(); - } - return AdaptiveTextSelectionToolbar.buttonItems( - anchors: textSelection?.contextMenuAnchors ?? - contentSelection!.contextMenuAnchors, - buttonItems: [ - if (textSelection != null) ...textSelection.contextMenuButtonItems, - if (contentSelection != null) - ...contentSelection.contextMenuButtonItems, - ContextMenuButtonItem( - label: L10n.of(context)!.showDefinition, - onPressed: () { - showDefinition(context); - focusNode.unfocus(); - }, - ), - ], - ); - } -} diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart new file mode 100644 index 000000000..dd306be54 --- /dev/null +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -0,0 +1,149 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.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'; + +class MessageAudioCard extends StatefulWidget { + final PangeaMessageEvent messageEvent; + + const MessageAudioCard({ + super.key, + required this.messageEvent, + }); + + @override + MessageAudioCardState createState() => MessageAudioCardState(); +} + +class MessageAudioCardState extends State { + // RepresentationEvent? repEvent; + bool _isLoading = false; + Event? localAudioEvent; + // String langCode = "en"; + + // void setLangCode() { + // final String? l2Code = + // MatrixState.pangeaController.languageController.activeL2Code( + // roomID: widget.messageEvent.room.id, + // ); + // setState(() => langCode = l2Code ?? "en"); + // } + + // void fetchRepresentation(BuildContext context) { + // repEvent = widget.messageEvent.representationByLanguage( + // langCode, + // ); + + // if (repEvent == null) { + // setState(() => _isLoading = true); + // widget.messageEvent + // .representationByLanguageGlobal( + // context: context, + // langCode: langCode, + // ) + // .onError((error, stackTrace) => ErrorHandler.logError()) + // .then(((RepresentationEvent? event) => repEvent = event)) + // .whenComplete( + // () => setState(() => _isLoading = false), + // ); + // } + // } + + void fetchAudio() { + if (!mounted) return; + // final String? text = widget.messageEvent.displayMessageText; + // if (text == null || text.isEmpty) return; + setState(() => _isLoading = true); + + widget.messageEvent + .getAudioGlobal(widget.messageEvent.messageDisplayLangCode) + .then((Event? event) { + localAudioEvent = event; + }).catchError((e) { + if (!mounted) return null; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.errorGettingAudio), + ), + ); + return null; + }).whenComplete(() { + if (mounted) setState(() => _isLoading = false); + }); + } + + @override + void initState() { + super.initState(); + widget.messageEvent + .getDisplayRepresentation(context) + .then((_) => fetchAudio()); + } + + @override + Widget build(BuildContext context) { + final playButton = InkWell( + borderRadius: BorderRadius.circular(64), + onTap: () => widget.messageEvent + .getDisplayRepresentation(context) + .then((event) => event == null ? null : fetchAudio), + child: Material( + color: AppConfig.primaryColor.withAlpha(64), + borderRadius: BorderRadius.circular(64), + child: const Icon( + // Change the icon based on some condition. If you have an audio player state, use it here. + Icons.play_arrow_outlined, + color: AppConfig.primaryColor, + ), + ), + ); + + return Padding( + padding: const EdgeInsets.all(8), + child: _isLoading + ? SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ) + : localAudioEvent != null + ? Container( + constraints: const BoxConstraints( + maxWidth: 250, + ), + child: Column( + children: [ + AudioPlayerWidget( + localAudioEvent!, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ], + ), + ) + : + // Opacity( + // opacity: widget.messageEvent.getDisplayRepresentation().then((event) => event == null ? ) == null + // ? 0.5 + // : 1, + // // child: SizedBox( + // // width: 44, + // // height: 36, + // child: + Padding( + padding: const EdgeInsets.only(left: 8), + child: playButton, + ), + // ), + // ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_context_menu.dart b/lib/pangea/widgets/chat/message_context_menu.dart new file mode 100644 index 000000000..5bb28dbe7 --- /dev/null +++ b/lib/pangea/widgets/chat/message_context_menu.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MessageContextMenu { + static List customToolbarOptions( + BuildContext context, + void Function()? onDefine, + void Function()? onListen, + ) { + return [ + ContextMenuButtonItem( + label: L10n.of(context)!.define, + onPressed: onDefine, + ), + ContextMenuButtonItem( + label: L10n.of(context)!.listen, + onPressed: onListen, + ), + ]; + } + + static List toolbarOptions( + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + BuildContext context, + void Function()? onDefine, + void Function()? onListen, + ) { + final List menuItems = + textSelection?.contextMenuButtonItems ?? + contentSelection?.contextMenuButtonItems ?? + []; + menuItems.sort((a, b) { + if (a.type == ContextMenuButtonType.copy) return -1; + if (b.type == ContextMenuButtonType.copy) return 1; + return 0; + }); + return MessageContextMenu.customToolbarOptions( + context, onDefine, onListen) + + menuItems; + } + + static Widget contextMenuOverride({ + required BuildContext context, + EditableTextState? textSelection, + SelectableRegionState? contentSelection, + void Function()? onDefine, + void Function()? onListen, + }) { + if (textSelection == null && contentSelection == null) { + return const SizedBox(); + } + + final List menuItems = + MessageContextMenu.toolbarOptions( + textSelection, + contentSelection, + context, + onDefine, + onListen, + ); + + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: textSelection?.contextMenuAnchors ?? + contentSelection!.contextMenuAnchors, + buttonItems: menuItems, + ); + } +} diff --git a/lib/pangea/widgets/chat/message_text_selection.dart b/lib/pangea/widgets/chat/message_text_selection.dart new file mode 100644 index 000000000..16982d979 --- /dev/null +++ b/lib/pangea/widgets/chat/message_text_selection.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MessageTextSelection { + String? selectedText; + String messageText = ""; + + void setMessageText(String text) { + messageText = text; + } + + void onTextSelection(TextSelection selection) => selection.isCollapsed == true + ? clearTextSelection() + : setTextSelection(selection); + + void setTextSelection(TextSelection selection) { + selectedText = selection.textInside(messageText); + if (BrowserContextMenu.enabled && kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + // selectionStream.add(selectedText); + } + + void clearTextSelection() { + selectedText = null; + if (kIsWeb && !BrowserContextMenu.enabled) { + BrowserContextMenu.enableContextMenu(); + } + // selectionStream.add(selectedText); + } +} diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart new file mode 100644 index 000000000..4dc2ac4b6 --- /dev/null +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -0,0 +1,303 @@ +import 'dart:async'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/chat.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; +import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +enum MessageMode { translation, play, definition } + +class ToolbarDisplayController { + final FocusNode focusNode = FocusNode(); + final PangeaMessageEvent pangeaMessageEvent; + final String targetId; + final bool immersionMode; + final ChatController controller; + + MessageToolbar? toolbar; + String? overlayId; + double? messageWidth; + + final toolbarModeStream = StreamController.broadcast(); + + ToolbarDisplayController({ + required this.pangeaMessageEvent, + required this.targetId, + required this.immersionMode, + required this.controller, + }); + + void setToolbar() { + toolbar ??= MessageToolbar( + textSelection: MessageTextSelection(), + room: pangeaMessageEvent.room, + toolbarModeStream: toolbarModeStream, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: immersionMode, + controller: controller, + ); + + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(targetId); + final targetRenderBox = + layerLinkAndKey.key.currentContext?.findRenderObject(); + if (targetRenderBox == null) return; + final Size transformTargetSize = (targetRenderBox as RenderBox).size; + messageWidth = transformTargetSize.width; + } + + void showToolbar(BuildContext context, {MessageMode? mode}) { + if (highlighted) return; + if (controller.selectMode) { + controller.clearSelectedEvents(); + } + focusNode.unfocus(); + Widget overlayEntry; + try { + overlayEntry = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: pangeaMessageEvent.ownMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + toolbar!, + OverlayMessage( + pangeaMessageEvent.event, + timeline: pangeaMessageEvent.timeline, + immersionMode: immersionMode, + ownMessage: pangeaMessageEvent.ownMessage, + toolbarController: this, + width: messageWidth, + ), + ], + ); + } catch (err) { + ErrorHandler.logError(e: err, s: StackTrace.current); + return; + } + OverlayUtil.showOverlay( + context: context, + child: overlayEntry, + transformTargetId: targetId, + targetAnchor: pangeaMessageEvent.ownMessage + ? Alignment.bottomRight + : Alignment.bottomLeft, + followerAnchor: pangeaMessageEvent.ownMessage + ? Alignment.bottomRight + : Alignment.bottomLeft, + backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(164), + ); + + if (MatrixState.pAnyState.overlay != null) { + overlayId = MatrixState.pAnyState.overlay.hashCode.toString(); + } + + if (mode != null) { + Future.delayed( + const Duration(milliseconds: 100), + () => toolbarModeStream.add(mode), + ); + } + } + + bool get highlighted => + MatrixState.pAnyState.overlay.hashCode.toString() == overlayId; +} + +class MessageToolbar extends StatefulWidget { + final MessageTextSelection textSelection; + final Room room; + final PangeaMessageEvent pangeaMessageEvent; + final StreamController toolbarModeStream; + final bool immersionMode; + final ChatController controller; + + const MessageToolbar({ + super.key, + required this.textSelection, + required this.room, + required this.pangeaMessageEvent, + required this.toolbarModeStream, + required this.immersionMode, + required this.controller, + }); + + @override + MessageToolbarState createState() => MessageToolbarState(); +} + +class MessageToolbarState extends State { + Widget? child; + MessageMode? _currentMode; + late StreamSubscription _selectionStream; + late StreamSubscription _toolbarModeStream; + + IconData _getIconData(MessageMode mode) { + switch (mode) { + case MessageMode.translation: + return Icons.g_translate; + case MessageMode.play: + return Icons.play_arrow; + case MessageMode.definition: + return Icons.book; + default: + return Icons.error; // Icon to indicate an error or unsupported mode + } + } + + bool _enabledButton(MessageMode mode) { + switch (mode) { + case MessageMode.translation: + return true; + case MessageMode.play: + return true; + case MessageMode.definition: + debugPrint("checking"); + return widget.textSelection.selectedText != null; + default: + return false; + } + } + + void updateMode(MessageMode newMode) { + debugPrint("updating toolbar mode"); + setState(() => _currentMode = newMode); + switch (_currentMode) { + case MessageMode.translation: + showTranslation(); + break; + case MessageMode.play: + playAudio(); + break; + case MessageMode.definition: + showDefinition(); + break; + default: + break; + } + setState(() {}); + } + + void showTranslation() { + debugPrint("show translation"); + child = MessageTranslationCard( + messageEvent: widget.pangeaMessageEvent, + immersionMode: widget.immersionMode, + ); + } + + void playAudio() { + debugPrint("play audio"); + child = MessageAudioCard( + messageEvent: widget.pangeaMessageEvent, + ); + } + + void showDefinition() { + if (widget.textSelection.selectedText == null || + widget.textSelection.selectedText!.isEmpty) { + return; + } + + child = WordDataCard( + word: widget.textSelection.selectedText!, + wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, + fullText: widget.textSelection.messageText, + fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, + hasInfo: false, + room: widget.room, + ); + } + + void showImage() {} + + void spellCheck() {} + + void showMore() { + MatrixState.pAnyState.closeOverlay(); + widget.controller.onSelectMessage(widget.pangeaMessageEvent.event); + } + + @override + void initState() { + super.initState(); + _toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) { + updateMode(mode); + }); + } + + @override + void dispose() { + _selectionStream.cancel(); + _toolbarModeStream.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all( + width: 2, + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.all( + Radius.circular(25), + ), + ), + constraints: const BoxConstraints( + maxWidth: 300, + minWidth: 300, + maxHeight: 300, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: AnimatedSize( + duration: FluffyThemes.animationDuration, + child: Column( + children: [ + child ?? const SizedBox(), + SizedBox(height: child == null ? 0 : 20), + ], + ), + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: MessageMode.values.map((mode) { + return IconButton( + icon: Icon(_getIconData(mode)), + onPressed: + _enabledButton(mode) ? () => updateMode(mode) : null, + ); + }).toList() + + [ + IconButton( + icon: Icon(Icons.adaptive.more_outlined), + onPressed: showMore, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart new file mode 100644 index 000000000..5cb22e919 --- /dev/null +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -0,0 +1,103 @@ +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class MessageTranslationCard extends StatefulWidget { + final PangeaMessageEvent messageEvent; + final bool immersionMode; + + const MessageTranslationCard({ + super.key, + required this.messageEvent, + required this.immersionMode, + }); + + @override + MessageTranslationCardState createState() => MessageTranslationCardState(); +} + +class MessageTranslationCardState extends State { + RepresentationEvent? repEvent; + bool _fetchingRepresentation = false; + + String? translationLangCode() { + final String? l1Code = + MatrixState.pangeaController.languageController.activeL1Code( + roomID: widget.messageEvent.room.id, + ); + if (widget.immersionMode) return l1Code; + + final String? l2Code = + MatrixState.pangeaController.languageController.activeL2Code( + roomID: widget.messageEvent.room.id, + ); + final String? originalWrittenCode = + widget.messageEvent.originalWritten?.content.langCode; + return l1Code == originalWrittenCode ? l2Code : l1Code; + } + + void fetchRepresentation(BuildContext context) { + final String? langCode = translationLangCode(); + if (langCode == null) return; + + repEvent = widget.messageEvent.representationByLanguage( + langCode, + ); + + if (repEvent == null && mounted) { + setState(() => _fetchingRepresentation = true); + widget.messageEvent + .representationByLanguageGlobal( + context: context, + langCode: langCode, + ) + .onError( + (error, stackTrace) => ErrorHandler.logError( + e: error, + s: stackTrace, + ), + ) + .then((RepresentationEvent? event) => repEvent = event) + .whenComplete( + () => setState(() => _fetchingRepresentation = false), + ); + } else { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + fetchRepresentation(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: _fetchingRepresentation + ? SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: Theme.of(context).colorScheme.primary, + ), + ) + : repEvent != null + ? Text( + repEvent!.text, + style: BotStyle.text(context), + ) + : Text( + L10n.of(context)!.oopsSomethingWentWrong, + style: BotStyle.text(context), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart new file mode 100644 index 000000000..068908ba6 --- /dev/null +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -0,0 +1,119 @@ +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/message_content.dart'; +import 'package:fluffychat/pangea/models/language_model.dart'; +import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +import '../../../config/app_config.dart'; + +class OverlayMessage extends StatelessWidget { + final Event event; + final bool selected; + final Timeline timeline; + // #Pangea + // final LanguageModel? selectedDisplayLang; + final bool immersionMode; + // final bool definitions; + final bool ownMessage; + final ToolbarDisplayController toolbarController; + final double? width; + // Pangea# + + const OverlayMessage( + this.event, { + this.selected = false, + required this.timeline, + // #Pangea + // required this.selectedDisplayLang, + required this.immersionMode, + // required this.definitions, + required this.ownMessage, + required this.toolbarController, + this.width, + // Pangea# + super.key, + }); + + @override + Widget build(BuildContext context) { + if (event.type != EventTypes.Message || + event.messageType == EventTypes.KeyVerificationRequest) { + return const SizedBox.shrink(); + } + + var color = Theme.of(context).colorScheme.surfaceVariant; + final textColor = ownMessage + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onBackground; + + final borderRadius = BorderRadius.only( + topLeft: !ownMessage + ? const Radius.circular(4) + : const Radius.circular(AppConfig.borderRadius), + topRight: const Radius.circular(AppConfig.borderRadius), + bottomLeft: const Radius.circular(AppConfig.borderRadius), + bottomRight: ownMessage + ? const Radius.circular(4) + : const Radius.circular(AppConfig.borderRadius), + ); + final noBubble = { + MessageTypes.Video, + MessageTypes.Image, + MessageTypes.Sticker, + }.contains(event.messageType) && + !event.redacted; + final noPadding = { + MessageTypes.File, + MessageTypes.Audio, + }.contains(event.messageType); + + if (ownMessage) { + color = Theme.of(context).colorScheme.primaryContainer; + } + + // #Pangea + final pangeaMessageEvent = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: ownMessage, + ); + // Pangea# + + return Material( + color: noBubble ? Colors.transparent : color, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + padding: noBubble || noPadding + ? EdgeInsets.zero + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + constraints: BoxConstraints( + maxWidth: width ?? FluffyThemes.columnWidth * 1.25, + ), + child: MessageContent( + event, + textColor: textColor, + borderRadius: borderRadius, + selected: selected, + pangeaMessageEvent: pangeaMessageEvent, + // selectedDisplayLang: selectedDisplayLang, + immersionMode: immersionMode, + // definitions: definitions, + toolbarController: toolbarController, + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/chat/text_to_speech_button.dart b/lib/pangea/widgets/chat/text_to_speech_button.dart index e591a448d..ec3dbd0db 100644 --- a/lib/pangea/widgets/chat/text_to_speech_button.dart +++ b/lib/pangea/widgets/chat/text_to_speech_button.dart @@ -45,7 +45,6 @@ class _TextToSpeechButtonState extends State { timeline: widget.controller.timeline!, ownMessage: widget.selectedEvent.senderId == Matrix.of(context).client.userID, - selected: true, ); } diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 935268d97..a2d32adb2 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,44 +1,29 @@ -import 'dart:developer'; import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat/events/html_message.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import '../../models/pangea_match_model.dart'; -import '../../models/pangea_representation_event.dart'; -import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; - final TextStyle? style; - final bool selected; - final LanguageModel? selectedDisplayLang; final bool immersionMode; - final bool definitions; - final Choreographer? choreographer; - final ShowDefintionUtil? messageToolbar; + final ToolbarDisplayController toolbarController; + final TextStyle? style; const PangeaRichText({ super.key, required this.pangeaMessageEvent, - required this.selected, - required this.selectedDisplayLang, required this.immersionMode, - required this.definitions, - this.choreographer, + required this.toolbarController, this.style, - this.messageToolbar, }); @override @@ -47,93 +32,73 @@ class PangeaRichText extends StatefulWidget { class PangeaRichTextState extends State { final PangeaController pangeaController = MatrixState.pangeaController; + RepresentationEvent? repEvent; bool _fetchingRepresentation = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; - String textSpan = ""; @override void initState() { super.initState(); - updateTextSpan(); + setTextSpan(); } - @override - void didUpdateWidget(PangeaRichText oldWidget) { - super.didUpdateWidget(oldWidget); - updateTextSpan(); - } + Future setTextSpan() async { + setState(() => _fetchingRepresentation = true); + try { + await widget.pangeaMessageEvent.getDisplayRepresentation(context); + } catch (err) { + ErrorHandler.logError(e: err); + } + setState(() => _fetchingRepresentation = false); - void updateTextSpan() { - setState(() { - textSpan = getTextSpan(context); - widget.messageToolbar?.messageText = textSpan; - }); + widget.toolbarController.toolbar?.textSelection.setMessageText( + widget.pangeaMessageEvent.displayMessageText, + ); } @override Widget build(BuildContext context) { //TODO - take out of build function of every message - // if (areLanguagesSet) { - - if (!widget.selected && - widget.selectedDisplayLang != null && - widget.selectedDisplayLang!.langCode != LanguageKeys.unknownLanguage) { - pangeaController.instructions.show( - context, - InstructionsEnum.clickMessage, - widget.pangeaMessageEvent.eventId, - ); - } else if (blur > 0) { - pangeaController.instructions.show( - context, - InstructionsEnum.blurMeansTranslate, - widget.pangeaMessageEvent.eventId, - ); - } - - final Widget richText = widget.pangeaMessageEvent.isHtml - ? HtmlMessage( - html: textSpan, - room: widget.pangeaMessageEvent.room, - textColor: widget.style?.color ?? Colors.black, - messageToolbar: widget.messageToolbar, - ) - : SelectableText.rich( - onSelectionChanged: (selection, cause) => - widget.messageToolbar?.onTextSelection( - selectedText: selection, - cause: cause, - context: context, - ), - onTap: () => messageToolbar?.onTextTap(context), - focusNode: widget.messageToolbar?.focusNode, - contextMenuBuilder: (context, state) => - widget.messageToolbar?.contextMenuOverride( - context: context, - textSelection: state, - ) ?? - const SizedBox(), - TextSpan( - text: textSpan, - style: widget.style, - children: [ - if (widget.selected && (_fetchingRepresentation)) - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(left: 5.0), - child: SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.secondaryColor, - ), - ), - ), + final Widget richText = SelectableText.rich( + onSelectionChanged: (selection, cause) => widget + .toolbarController.toolbar?.textSelection + .onTextSelection(selection), + onTap: () => widget.toolbarController.showToolbar(context), + focusNode: widget.toolbarController.focusNode, + contextMenuBuilder: (context, state) => + MessageContextMenu.contextMenuOverride( + context: context, + textSelection: state, + onDefine: () => widget.toolbarController.showToolbar( + context, + mode: MessageMode.definition, + ), + onListen: () => widget.toolbarController.showToolbar( + context, + mode: MessageMode.play, + ), + ), + TextSpan( + text: widget.pangeaMessageEvent.displayMessageText, + style: widget.style, + children: [ + if (_fetchingRepresentation) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 5.0), + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.secondaryColor, ), - ], + ), + ), ), - ); + ], + ), + ); return blur > 0 ? ImageFiltered( @@ -143,55 +108,6 @@ class PangeaRichTextState extends State { : richText; } - String getTextSpan(BuildContext context) { - final String? displayLangCode = - widget.selected ? widget.selectedDisplayLang?.langCode : userL2LangCode; - - if (displayLangCode == null || !widget.immersionMode) { - return widget.pangeaMessageEvent.body; - } - - if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { - debugger(when: kDebugMode); - return widget.pangeaMessageEvent.body; - } - - final RepresentationEvent? repEvent = - widget.pangeaMessageEvent.representationByLanguage( - displayLangCode, - ); - - if (repEvent == null) { - _fetchingRepresentation = true; - - setState(() => {}); - widget.pangeaMessageEvent - .representationByLanguageGlobal( - context: context, - langCode: displayLangCode, - ) - .onError((error, stackTrace) => ErrorHandler.logError()) - .whenComplete(() => setState(() => _fetchingRepresentation = false)); - return widget.pangeaMessageEvent.body; - } - - if (repEvent.event?.eventId.contains("web") ?? false) { - Sentry.addBreadcrumb( - Breadcrumb.fromJson({"repEvent.event": repEvent.event?.toJson()}), - ); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "representationByLanguageGlobal returned RepEvent with event ID containing 'web' - ${repEvent.event?.eventId}", - ), - ); - } - - return widget.pangeaMessageEvent.isHtml - ? repEvent.formatBody() ?? repEvent.text - : repEvent.text; - } - bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 8c1940aae..2ef498887 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -79,6 +79,18 @@ class WordDataCardController extends State { super.initState(); } + @override + void didUpdateWidget(covariant WordDataCard oldWidget) { + if (oldWidget.word != widget.word) { + if (!widget.hasInfo) { + getContextualDefinition(); + } else { + getWordNet(); + } + } + super.didUpdateWidget(oldWidget); + } + Future getContextualDefinition() async { ContextualDefinitionRequestModel? req; try { @@ -89,7 +101,14 @@ class WordDataCardController extends State { fullTextLang: widget.fullTextLang, wordLang: widget.wordLang, ); - if (mounted) setState(() => isLoadingContextualDefinition = true); + if (!mounted) return; + + setState(() { + contextualDefinitionRes = null; + definitionError = null; + isLoadingContextualDefinition = true; + }); + contextualDefinitionRes = await controller.definitions.get(req); if (contextualDefinitionRes == null) { definitionError = Exception("Error getting definition"); @@ -159,54 +178,57 @@ class WordDataCardView extends StatelessWidget { return Scrollbar( thumbVisibility: true, controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CardHeader( - text: controller.widget.word, - botExpression: BotExpression.down, - ), - if (controller.widget.choiceFeedback != null) - Text( - controller.widget.choiceFeedback!, - style: BotStyle.text(context), + child: Expanded( + child: SingleChildScrollView( + controller: scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: controller.widget.word, + botExpression: BotExpression.down, ), - const SizedBox(height: 5.0), - if (controller.wordData != null && controller.wordNetError == null) - WordNetInfo( - wordData: controller.wordData!, - activeL1: controller.activeL1!, - activeL2: controller.activeL2!, - ), - if (controller.isLoadingWordNet) const PCircular(), - const SizedBox(height: 5.0), - // if (controller.widget.hasInfo && - // !controller.isLoadingContextualDefinition && - // controller.contextualDefinitionRes == null) - // Material( - // type: MaterialType.transparency, - // child: ListTile( - // leading: const BotFace( - // width: 40, expression: BotExpression.surprised), - // title: Text(L10n.of(context)!.askPangeaBot), - // onTap: controller.handleGetDefinitionButtonPress, - // ), - // ), - if (controller.isLoadingContextualDefinition) const PCircular(), - if (controller.contextualDefinitionRes != null) - Text( - controller.contextualDefinitionRes!.text, - style: BotStyle.text(context), - ), - if (controller.definitionError != null) - Text( - L10n.of(context)!.sorryNoResults, - style: BotStyle.text(context), - ), - ], + if (controller.widget.choiceFeedback != null) + Text( + controller.widget.choiceFeedback!, + style: BotStyle.text(context), + ), + const SizedBox(height: 5.0), + if (controller.wordData != null && + controller.wordNetError == null) + WordNetInfo( + wordData: controller.wordData!, + activeL1: controller.activeL1!, + activeL2: controller.activeL2!, + ), + if (controller.isLoadingWordNet) const PCircular(), + const SizedBox(height: 5.0), + // if (controller.widget.hasInfo && + // !controller.isLoadingContextualDefinition && + // controller.contextualDefinitionRes == null) + // Material( + // type: MaterialType.transparency, + // child: ListTile( + // leading: const BotFace( + // width: 40, expression: BotExpression.surprised), + // title: Text(L10n.of(context)!.askPangeaBot), + // onTap: controller.handleGetDefinitionButtonPress, + // ), + // ), + if (controller.isLoadingContextualDefinition) const PCircular(), + if (controller.contextualDefinitionRes != null) + Text( + controller.contextualDefinitionRes!.text, + style: BotStyle.text(context), + ), + if (controller.definitionError != null) + Text( + L10n.of(context)!.sorryNoResults, + style: BotStyle.text(context), + ), + ], + ), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index f39c96502..25e4ca8c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,14 +337,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.3" - desktop_drop: - dependency: "direct main" - description: - name: desktop_drop - sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d - url: "https://pub.dev" - source: hosted - version: "0.4.4" desktop_lifecycle: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d37aa3845..24f5e0ac8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: chewie: ^1.7.1 collection: ^1.17.2 cupertino_icons: any - desktop_drop: ^0.4.4 + # desktop_drop: ^0.4.4 desktop_notifications: ^0.6.3 device_info_plus: ^9.1.0 dynamic_color: ^1.6.8 From 7982fa6387f2207224a1badc8baef262cc408fe0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 7 Feb 2024 15:29:39 -0500 Subject: [PATCH 5/8] auto generated changes --- linux/flutter/generated_plugin_registrant.cc | 4 ---- linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- windows/flutter/generated_plugin_registrant.cc | 3 --- windows/flutter/generated_plugins.cmake | 1 - 5 files changed, 11 deletions(-) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index eb3c11cf1..935db7735 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,7 +6,6 @@ #include "generated_plugin_registrant.h" -#include #include #include #include @@ -20,9 +19,6 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) desktop_drop_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); - desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) desktop_lifecycle_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopLifecyclePlugin"); desktop_lifecycle_plugin_register_with_registrar(desktop_lifecycle_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 044576a2d..6c5e71b28 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - desktop_drop desktop_lifecycle dynamic_color emoji_picker_flutter diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a77aa2d5c..caaa5763f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import appkit_ui_element_colors import audio_session import connectivity_plus -import desktop_drop import desktop_lifecycle import device_info_plus import dynamic_color @@ -47,7 +46,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopLifecyclePlugin.register(with: registry.registrar(forPlugin: "DesktopLifecyclePlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3f7add542..2e0327735 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -26,8 +25,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); - DesktopDropPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopDropPlugin")); DesktopLifecyclePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopLifecyclePlugin")); DynamicColorPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f0c310aec..98f992b44 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST connectivity_plus - desktop_drop desktop_lifecycle dynamic_color emoji_picker_flutter From 13b38eac291c0b0e634d7be1075c650cebcb8cc2 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 8 Feb 2024 16:22:49 -0500 Subject: [PATCH 6/8] changes from testing --- lib/pages/chat/chat.dart | 73 ++++++++++--------- lib/pages/chat/chat_event_list.dart | 10 +-- lib/pages/chat/events/message.dart | 6 +- lib/pages/chat/events/message_content.dart | 7 +- lib/pangea/models/pangea_message_event.dart | 30 ++++---- .../widgets/chat/message_audio_card.dart | 21 ++---- .../chat/message_translation_card.dart | 14 +++- lib/pangea/widgets/igc/pangea_rich_text.dart | 57 +++++++++++---- 8 files changed, 124 insertions(+), 94 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index c63a6fa83..da9c883ab 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -30,7 +30,6 @@ import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/error_reporter.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/filtered_timeline_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_lock.dart'; @@ -1546,48 +1545,52 @@ class ChatController extends State return currentState; } - List get events => - timeline!.events.where((event) => event.isVisibleInGui).toList(); - - final Map _messageToolbarControllers = {}; final Map _pangeaMessageEvents = {}; + final Map _toolbarDisplayControllers = {}; - PangeaMessageEvent? pangeaMessageEvent(String eventId) { - final Event? event = - events.firstWhereOrNull((event) => event.eventId == eventId); - if (timeline == null || event == null || event.type != EventTypes.Message) { - return null; + void setPangeaMessageEvent(String eventId) { + final Event? event = timeline!.events.firstWhereOrNull( + (e) => e.eventId == eventId, + ); + if (event == null || timeline == null) return; + _pangeaMessageEvents[eventId] = PangeaMessageEvent( + event: event, + timeline: timeline!, + ownMessage: event.senderId == room.client.userID, + ); + _pangeaMessageEvents[eventId]!.setDisplayRepresentation(context); + } + + void setToolbarDisplayController(String eventId) { + final Event? event = timeline!.events.firstWhereOrNull( + (e) => e.eventId == eventId, + ); + if (event == null || timeline == null) return; + if (_pangeaMessageEvents[eventId] == null) { + setPangeaMessageEvent(eventId); + if (_pangeaMessageEvents[eventId] == null) return; } - if (!_pangeaMessageEvents.containsKey(eventId)) { - _pangeaMessageEvents[eventId] = PangeaMessageEvent( - event: event, - timeline: timeline!, - ownMessage: event.senderId == Matrix.of(context).client.userID, - ); + _toolbarDisplayControllers[eventId] = ToolbarDisplayController( + targetId: event.eventId, + pangeaMessageEvent: _pangeaMessageEvents[eventId]!, + immersionMode: choreographer.immersionMode, + controller: this, + ); + _toolbarDisplayControllers[eventId]!.setToolbar(); + } + + PangeaMessageEvent? getPangeaMessageEvent(String eventId) { + if (_pangeaMessageEvents[eventId] == null) { + setPangeaMessageEvent(eventId); } return _pangeaMessageEvents[eventId]; } - ToolbarDisplayController? messageToolbarController(String eventId) { - final Event? event = - events.firstWhereOrNull((event) => event.eventId == eventId); - if (timeline == null || event == null || event.type != EventTypes.Message) { - return null; + ToolbarDisplayController? getToolbarDisplayController(String eventId) { + if (_toolbarDisplayControllers[eventId] == null) { + setToolbarDisplayController(eventId); } - - final PangeaMessageEvent? messageEvent = pangeaMessageEvent(eventId); - if (messageEvent == null) return null; - - if (!_messageToolbarControllers.containsKey(event.eventId)) { - _messageToolbarControllers[event.eventId] = ToolbarDisplayController( - targetId: event.eventId, - pangeaMessageEvent: messageEvent, - immersionMode: choreographer.immersionMode, - controller: this, - ); - _messageToolbarControllers[event.eventId]!.setToolbar(); - } - return _messageToolbarControllers[eventId]; + return _toolbarDisplayControllers[eventId]; } // Pangea# diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index d8fc89ebc..30f733d1d 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -24,12 +24,10 @@ class ChatEventList extends StatelessWidget { Widget build(BuildContext context) { final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; - // #Pangea - // final events = controller.timeline!.events - // .where((event) => event.isVisibleInGui) - // .toList(); - final events = controller.events; - // Pangea# + final events = controller.timeline!.events + .where((event) => event.isVisibleInGui) + .toList(); + final animateInEventIndex = controller.animateInEventIndex; // create a map of eventId --> index to greatly improve performance of diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 4f4d06d86..61bf83061 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -5,7 +5,6 @@ import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; @@ -15,7 +14,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:swipe_to_action/swipe_to_action.dart'; -import 'package:vibration/vibration.dart'; import '../../../config/app_config.dart'; import 'message_content.dart'; @@ -144,9 +142,9 @@ class Message extends StatelessWidget { // #Pangea final PangeaMessageEvent? pangeaMessageEvent = - controller.pangeaMessageEvent(event.eventId); + controller.getPangeaMessageEvent(event.eventId); final ToolbarDisplayController? toolbarController = - controller.messageToolbarController(event.eventId); + controller.getToolbarDisplayController(event.eventId); // Pangea# final resetAnimateIn = this.resetAnimateIn; diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index fd4791d3c..51bf1e969 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,16 +1,13 @@ import 'package:fluffychat/pages/chat/events/html_message.dart'; import 'package:fluffychat/pages/chat/events/video_player.dart'; -import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; -import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.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:flutter_linkify/flutter_linkify.dart'; @@ -173,7 +170,7 @@ class MessageContent extends StatelessWidget { event.isRichMessage // #Pangea && - !(pangeaMessageEvent?.showRichText ?? false) + !(pangeaMessageEvent?.showRichText(selected) ?? false) // Pangea# ) { var html = event.formattedText; @@ -273,7 +270,7 @@ class MessageContent extends StatelessWidget { decoration: event.redacted ? TextDecoration.lineThrough : null, height: 1.3, ); - if (pangeaMessageEvent?.showRichText ?? false) { + if (pangeaMessageEvent?.showRichText(selected) ?? false) { return PangeaRichText( style: messageTextStyle, pangeaMessageEvent: pangeaMessageEvent!, diff --git a/lib/pangea/models/pangea_message_event.dart b/lib/pangea/models/pangea_message_event.dart index d46ad0dc3..2be000eb1 100644 --- a/lib/pangea/models/pangea_message_event.dart +++ b/lib/pangea/models/pangea_message_event.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_message_types.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; @@ -27,7 +26,6 @@ class PangeaMessageEvent { final Timeline timeline; final bool ownMessage; bool _isValidPangeaMessageEvent = true; - RepresentationEvent? _displayRepresentation; PangeaMessageEvent({ required Event event, @@ -71,7 +69,7 @@ class PangeaMessageEvent { .firstOrNull ?? _event; - bool get showRichText { + bool showRichText(bool selected) { if (!_isValidPangeaMessageEvent) { return false; } @@ -81,7 +79,7 @@ class PangeaMessageEvent { if ([EventStatus.error, EventStatus.sending].contains(_event.status)) { return false; } - // if (ownMessage && !selected) return false; + if (ownMessage && !selected) return false; return true; } @@ -187,7 +185,7 @@ class PangeaMessageEvent { final elementLangCode = transcription[ModelKey.langCode]; final elementText = transcription[ModelKey.text]; - // Check if both language code and text match + // Check if both language code and text matsch return elementLangCode == langCode && elementText == text; }, ); @@ -402,34 +400,36 @@ class PangeaMessageEvent { return langCode ?? LanguageKeys.unknownLanguage; } - Future getDisplayRepresentation( + RepresentationEvent? _displayRepresentation; + + RepresentationEvent? displayRepresentation(String langCode) => + _displayRepresentation; + + Future setDisplayRepresentation( BuildContext context, ) async { - if (messageDisplayLangCode == LanguageKeys.unknownLanguage) return null; - if (_displayRepresentation != null) return _displayRepresentation; - _displayRepresentation = representationByLanguage(messageDisplayLangCode); - if (_displayRepresentation != null) { - return _displayRepresentation; + if (messageDisplayLangCode == LanguageKeys.unknownLanguage || + _displayRepresentation != null) { + return; } + _displayRepresentation = representationByLanguage(messageDisplayLangCode); + if (_displayRepresentation != null) return; try { _displayRepresentation = await representationByLanguageGlobal( context: context, langCode: messageDisplayLangCode, ); - return _displayRepresentation; + return; } catch (err, s) { ErrorHandler.logError( m: "error in getDisplayRepresentation", e: err, s: s, ); - return null; } } - String get displayMessageText => _displayRepresentation?.text ?? body; - // List get activities => //each match is turned into an activity that other students can access //they're not told the answer but have to find it themselves diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index dd306be54..c523e5048 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -1,10 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/pangea_representation_event.dart'; -import 'package:fluffychat/pangea/utils/bot_style.dart'; -import 'package:fluffychat/pangea/utils/error_handler.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'; @@ -57,10 +53,7 @@ class MessageAudioCardState extends State { void fetchAudio() { if (!mounted) return; - // final String? text = widget.messageEvent.displayMessageText; - // if (text == null || text.isEmpty) return; setState(() => _isLoading = true); - widget.messageEvent .getAudioGlobal(widget.messageEvent.messageDisplayLangCode) .then((Event? event) { @@ -81,18 +74,20 @@ class MessageAudioCardState extends State { @override void initState() { super.initState(); - widget.messageEvent - .getDisplayRepresentation(context) - .then((_) => fetchAudio()); + fetchAudio(); + } + + @override + void dispose() { + super.dispose(); + setState(() => _isLoading = false); } @override Widget build(BuildContext context) { final playButton = InkWell( borderRadius: BorderRadius.circular(64), - onTap: () => widget.messageEvent - .getDisplayRepresentation(context) - .then((event) => event == null ? null : fetchAudio), + onTap: fetchAudio, child: Material( color: AppConfig.primaryColor.withAlpha(64), borderRadius: BorderRadius.circular(64), diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 5cb22e919..3ff08a84b 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -63,10 +63,12 @@ class MessageTranslationCardState extends State { ) .then((RepresentationEvent? event) => repEvent = event) .whenComplete( - () => setState(() => _fetchingRepresentation = false), - ); + () { + setState(() => _fetchingRepresentation = false); + }, + ); } else { - setState(() {}); + if (mounted) setState(() {}); } } @@ -76,6 +78,12 @@ class MessageTranslationCardState extends State { fetchRepresentation(context); } + @override + void dispose() { + super.dispose(); + setState(() => _fetchingRepresentation = false); + } + @override Widget build(BuildContext context) { return Padding( diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index a2d32adb2..cdd86b1b1 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; @@ -9,7 +10,9 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + import '../../models/pangea_match_model.dart'; class PangeaRichText extends StatefulWidget { @@ -32,28 +35,56 @@ class PangeaRichText extends StatefulWidget { class PangeaRichTextState extends State { final PangeaController pangeaController = MatrixState.pangeaController; - RepresentationEvent? repEvent; bool _fetchingRepresentation = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; + String textSpan = ""; @override void initState() { super.initState(); - setTextSpan(); + updateTextSpan(); } - Future setTextSpan() async { - setState(() => _fetchingRepresentation = true); - try { - await widget.pangeaMessageEvent.getDisplayRepresentation(context); - } catch (err) { - ErrorHandler.logError(e: err); - } - setState(() => _fetchingRepresentation = false); + void updateTextSpan() { + setState(() { + textSpan = getTextSpan(); + }); + } - widget.toolbarController.toolbar?.textSelection.setMessageText( - widget.pangeaMessageEvent.displayMessageText, + @override + void didUpdateWidget(PangeaRichText oldWidget) { + super.didUpdateWidget(oldWidget); + updateTextSpan(); + } + + String getTextSpan() { + if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { + debugger(when: kDebugMode); + } + + final RepresentationEvent? repEvent = + widget.pangeaMessageEvent.representationByLanguage( + widget.pangeaMessageEvent.messageDisplayLangCode, ); + + if (repEvent == null) { + setState(() => _fetchingRepresentation = true); + + widget.pangeaMessageEvent + .representationByLanguageGlobal( + context: context, + langCode: widget.pangeaMessageEvent.messageDisplayLangCode, + ) + .onError((error, stackTrace) => ErrorHandler.logError()) + .then((_) { + widget.toolbarController.toolbar?.textSelection.setMessageText( + repEvent?.text ?? widget.pangeaMessageEvent.body, + ); + }).whenComplete(() => setState(() => _fetchingRepresentation = false)); + return widget.pangeaMessageEvent.body; + } + + return repEvent.text; } @override @@ -79,7 +110,7 @@ class PangeaRichTextState extends State { ), ), TextSpan( - text: widget.pangeaMessageEvent.displayMessageText, + text: textSpan, style: widget.style, children: [ if (_fetchingRepresentation) From ebbfba7ac97b2170c14f02406c51baedc3ba7e94 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 8 Feb 2024 17:09:43 -0500 Subject: [PATCH 7/8] added pangea tags, more testing --- lib/pages/chat/events/audio_player.dart | 6 ++ lib/pages/chat/events/html_message.dart | 6 -- lib/pages/chat/events/message.dart | 8 ++ lib/pages/chat/events/message_content.dart | 13 +-- .../widgets/language_display_toggle.dart | 68 --------------- lib/pangea/controllers/pangea_controller.dart | 27 +++--- .../extensions/pangea_room_extension.dart | 87 ++++++++++--------- .../utils/get_chat_list_item_subtitle.dart | 17 ++-- lib/pangea/utils/overlay.dart | 4 +- lib/pangea/widgets/chat/message_actions.dart | 3 +- .../widgets/chat/message_audio_card.dart | 43 +-------- lib/pangea/widgets/chat/overlay_message.dart | 2 - lib/pangea/widgets/igc/pangea_rich_text.dart | 6 +- pubspec.yaml | 2 + 14 files changed, 93 insertions(+), 199 deletions(-) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index 5cf5a98a7..beeb24d4c 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -81,7 +81,9 @@ class AudioPlayerState extends State { }); _playAction(); } catch (e, s) { + // #Pangea debugger(); + // Pangea# Logs().v('Could not download audio file', e, s); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -134,6 +136,7 @@ class AudioPlayerState extends State { if (audioFile != null) { audioPlayer.setFilePath(audioFile.path); } else { + // #Pangea final data = matrixFile!.bytes; final mimeType = matrixFile!.mimeType; //shouldn't have to be settting this here @@ -150,10 +153,13 @@ class AudioPlayerState extends State { debugPrint("still an ogg file!"); } try { + // Pangea# await audioPlayer.setAudioSource(MatrixFileAudioSource(matrixFile!)); + // #Pangea } catch (e, s) { debugger(when: kDebugMode); } + // Pangea# } audioPlayer.play().onError( ErrorReporter(context, 'Unable to play audio message') diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index 2f43cfdbb..a98ab4fc1 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -18,18 +18,12 @@ class HtmlMessage extends StatelessWidget { final String html; final Room room; final Color textColor; - // #Pangea - // final ShowDefintionUtil? messageToolbar; - // Pangea# const HtmlMessage({ super.key, required this.html, required this.room, this.textColor = Colors.black, - // #Pangea - // this.messageToolbar, - // Pangea# }); dom.Node _linkifyHtml(dom.Node element) { diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 61bf83061..971e85a58 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -242,6 +242,7 @@ class Message extends StatelessWidget { alignment: alignment, padding: const EdgeInsets.only(left: 8), child: GestureDetector( + // #Pangea onTap: () => toolbarController?.showToolbar(context), onDoubleTap: () => toolbarController?.showToolbar(context), @@ -249,6 +250,13 @@ class Message extends StatelessWidget { onSelect(event); HapticFeedback.selectionClick(); }, + // onLongPress: longPressSelect + // ? null + // : () { + // onSelect(event); + // HapticFeedback.selectionClick(); + // }, + // Pangea# child: AnimatedOpacity( opacity: animateIn ? 0 diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 51bf1e969..c772d3582 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -177,14 +177,10 @@ class MessageContent extends StatelessWidget { if (event.messageType == MessageTypes.Emote) { html = '* $html'; } - // #Pangea - // messageToolbar?.messageText = html; - // Pangea# return HtmlMessage( html: html, textColor: textColor, room: event.room, - // messageToolbar: messageToolbar, ); } // else we fall through to the normal message rendering @@ -276,12 +272,10 @@ class MessageContent extends StatelessWidget { pangeaMessageEvent: pangeaMessageEvent!, immersionMode: immersionMode, toolbarController: toolbarController!, - // selectedDisplayLang: selectedDisplayLang, - // highlighted: toolbarController!.highlighted, ); } + // Pangea# return FutureBuilder( - // Pangea# future: event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), hideReply: true, @@ -290,10 +284,12 @@ class MessageContent extends StatelessWidget { // #Pangea if (!snapshot.hasData) { return Text( + // Pangea# event.calcLocalizedBodyFallback( MatrixLocals(L10n.of(context)!), hideReply: true, ), + // #Pangea style: messageTextStyle, ); } @@ -311,7 +307,6 @@ class MessageContent extends StatelessWidget { ?.toolbar?.textSelection .onTextSelection(selection), onTap: () => toolbarController?.showToolbar(context), - // Pangea# text: toolbarController?.toolbar?.textSelection.messageText ?? messageText, focusNode: toolbarController?.focusNode, @@ -333,6 +328,7 @@ class MessageContent extends StatelessWidget { // MatrixLocals(L10n.of(context)!), // hideReply: true, // ), + // Pangea# style: TextStyle( color: textColor, fontSize: bigEmotes ? fontSize * 3 : fontSize, @@ -347,7 +343,6 @@ class MessageContent extends StatelessWidget { decorationColor: textColor.withAlpha(150), ), onOpen: (url) => UrlLauncher(context, url.url).launchUrl(), - // onTap: () => messageToolbar?.onTextTap(context), ); }, ); diff --git a/lib/pangea/choreographer/widgets/language_display_toggle.dart b/lib/pangea/choreographer/widgets/language_display_toggle.dart index 451e31782..9478b4220 100644 --- a/lib/pangea/choreographer/widgets/language_display_toggle.dart +++ b/lib/pangea/choreographer/widgets/language_display_toggle.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/pangea/widgets/flag.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -55,70 +54,3 @@ class LanguageDisplayToggle extends StatelessWidget { // ); } } - -class LanguageToggleSwitch extends StatefulWidget { - final ChatController controller; - - const LanguageToggleSwitch({super.key, required this.controller}); - - @override - _LanguageToggleSwitchState createState() => _LanguageToggleSwitchState(); -} - -class _LanguageToggleSwitchState extends State { - @override - Widget build(BuildContext context) { - final borderRadius = - BorderRadius.circular(20.0); // Use the same radius as your LanguageFlag - - return Tooltip( - message: L10n.of(context)!.toggleLanguages, - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.transparent, // No background color - shape: RoundedRectangleBorder(borderRadius: borderRadius), - padding: EdgeInsets.zero, // Aligns with your custom padding - ), - onPressed: _toggleLanguage, // Use the onTap logic for onPressed - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .background, // Adapt to your app theme or custom color - borderRadius: borderRadius, - ), - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - //trranslatte icon - Opacity( - opacity: isL1Selected ? 1.0 : 0.6, - child: LanguageFlag( - language: widget.controller.choreographer.l1Lang, - ), - ), - const SizedBox(width: 8.0), // Spacing between flags - Opacity( - opacity: isL1Selected ? 0.6 : 1.0, - child: LanguageFlag( - language: widget.controller.choreographer.l2Lang, - ), - ), - ], - ), - ), - ), - ); - } - - bool get isL1Selected => - widget.controller.choreographer.messageOptions.isTranslationOn; - - void _toggleLanguage() { - setState(() { - widget.controller.choreographer.messageOptions - .toggleSelectedDisplayLang(); - }); - } -} diff --git a/lib/pangea/controllers/pangea_controller.dart b/lib/pangea/controllers/pangea_controller.dart index 558fc55b2..e60b44b40 100644 --- a/lib/pangea/controllers/pangea_controller.dart +++ b/lib/pangea/controllers/pangea_controller.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'dart:math'; +import 'package:fluffychat/pangea/constants/class_default_values.dart'; import 'package:fluffychat/pangea/controllers/class_controller.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/language_controller.dart'; @@ -224,19 +225,19 @@ class PangeaController { continue; } final List userIds = participants.map((user) => user.id).toList(); - // if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { - // try { - // await space.invite(BotName.byEnvironment); - // await space.setPower( - // BotName.byEnvironment, - // ClassDefaultValues.powerLevelOfAdmin, - // ); - // } catch (err) { - // ErrorHandler.logError( - // e: "Failed to invite pangea bot to space ${space.id}", - // ); - // } - // } + if (space.canInvite && !userIds.contains(BotName.byEnvironment)) { + try { + await space.invite(BotName.byEnvironment); + await space.setPower( + BotName.byEnvironment, + ClassDefaultValues.powerLevelOfAdmin, + ); + } catch (err) { + ErrorHandler.logError( + e: "Failed to invite pangea bot to space ${space.id}", + ); + } + } } } } diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index 8901085db..85d68672f 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -591,51 +591,52 @@ extension PangeaRoom on Room { required String parentEventId, required String type, }) async { - // try { - debugPrint("creating $type child for $parentEventId"); - Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); - if (parentEventId.contains("web")) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb( - message: - "sendPangeaEvent with likely invalid parentEventId $parentEventId", - ), + try { + debugPrint("creating $type child for $parentEventId"); + Sentry.addBreadcrumb(Breadcrumb.fromJson(content)); + if (parentEventId.contains("web")) { + debugger(when: kDebugMode); + Sentry.addBreadcrumb( + Breadcrumb( + message: + "sendPangeaEvent with likely invalid parentEventId $parentEventId", + ), + ); + } + final Map repContent = { + // what is the functionality of m.reference? + "m.relates_to": {"rel_type": type, "event_id": parentEventId}, + type: content, + }; + + final String? newEventId = await sendEvent(repContent, type: type); + + if (newEventId == null) { + debugger(when: kDebugMode); + return null; + } + + //PTODO - handle the frequent case of a null newEventId + final Event? newEvent = await getEventById(newEventId); + + if (newEvent == null) { + debugger(when: kDebugMode); + } + + return newEvent; + } catch (err, stack) { + // debugger(when: kDebugMode); + ErrorHandler.logError( + e: err, + s: stack, + data: { + "type": type, + "parentEventId": parentEventId, + "content": content, + }, ); + return null; } - final Map repContent = { - // what is the functionality of m.reference? - "m.relates_to": {"rel_type": type, "event_id": parentEventId}, - type: content, - }; - - final String? newEventId = await sendEvent(repContent, type: type); - - if (newEventId == null) { - debugger(when: kDebugMode); - } - - //PTODO - handle the frequent case of a null newEventId - final Event? newEvent = await getEventById(newEventId!); - - if (newEvent == null) { - debugger(when: kDebugMode); - } - - return newEvent; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // e: err, - // s: stack, - // data: { - // "type": type, - // "parentEventId": parentEventId, - // "content": content, - // }, - // ); - // return null; - // } } ConstructEvent? _vocabEventLocal(String lemma) { diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index 82b3461c4..2a77decad 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/constants/language_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/class_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + import '../../utils/matrix_sdk_extensions/matrix_locals.dart'; class GetChatListItemSubtitle { @@ -17,11 +16,9 @@ class GetChatListItemSubtitle { ) async { if (event == null) return L10n.of(context)!.emptyChat; // try { - if (event.type != EventTypes.Message) - // || - // !pangeaController.permissionsController - // .isToolEnabled(ToolSetting.immersionMode, event.room)) - { + if (event.type != EventTypes.Message || + !pangeaController.permissionsController + .isToolEnabled(ToolSetting.immersionMode, event.room)) { return event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), hideReply: true, diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 26399c8aa..07eab5133 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -60,9 +60,7 @@ class OverlayUtil { MatrixState.pAnyState.openOverlay(entry, context); } catch (err, stack) { - debugPrint("ERROR: $err"); - debugPrint("STACK: $stack"); - // debugger(when: kDebugMode); + debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); } } diff --git a/lib/pangea/widgets/chat/message_actions.dart b/lib/pangea/widgets/chat/message_actions.dart index 03d3f8e6e..05731f234 100644 --- a/lib/pangea/widgets/chat/message_actions.dart +++ b/lib/pangea/widgets/chat/message_actions.dart @@ -1,5 +1,4 @@ import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/language_display_toggle.dart'; import 'package:fluffychat/pangea/widgets/chat/text_to_speech_button.dart'; import 'package:flutter/material.dart'; @@ -13,7 +12,7 @@ class PangeaMessageActions extends StatelessWidget { return chatController.selectedEvents.length == 1 ? Row( children: [ - LanguageToggleSwitch(controller: chatController), + // LanguageToggleSwitch(controller: chatController), TextToSpeechButton( controller: chatController, selectedEvent: chatController.selectedEvents.first, diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index c523e5048..ff4a3baea 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -18,38 +18,8 @@ class MessageAudioCard extends StatefulWidget { } class MessageAudioCardState extends State { - // RepresentationEvent? repEvent; bool _isLoading = false; Event? localAudioEvent; - // String langCode = "en"; - - // void setLangCode() { - // final String? l2Code = - // MatrixState.pangeaController.languageController.activeL2Code( - // roomID: widget.messageEvent.room.id, - // ); - // setState(() => langCode = l2Code ?? "en"); - // } - - // void fetchRepresentation(BuildContext context) { - // repEvent = widget.messageEvent.representationByLanguage( - // langCode, - // ); - - // if (repEvent == null) { - // setState(() => _isLoading = true); - // widget.messageEvent - // .representationByLanguageGlobal( - // context: context, - // langCode: langCode, - // ) - // .onError((error, stackTrace) => ErrorHandler.logError()) - // .then(((RepresentationEvent? event) => repEvent = event)) - // .whenComplete( - // () => setState(() => _isLoading = false), - // ); - // } - // } void fetchAudio() { if (!mounted) return; @@ -124,21 +94,10 @@ class MessageAudioCardState extends State { ], ), ) - : - // Opacity( - // opacity: widget.messageEvent.getDisplayRepresentation().then((event) => event == null ? ) == null - // ? 0.5 - // : 1, - // // child: SizedBox( - // // width: 44, - // // height: 36, - // child: - Padding( + : Padding( padding: const EdgeInsets.only(left: 8), child: playButton, ), - // ), - // ), ); } } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index 8bc1b5cf0..e39ffe827 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -27,9 +27,7 @@ class OverlayMessage extends StatelessWidget { this.selected = false, required this.timeline, // #Pangea - // required this.selectedDisplayLang, required this.immersionMode, - // required this.definitions, required this.ownMessage, required this.toolbarController, this.width, diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index cdd86b1b1..d0a7ed11e 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -80,7 +80,11 @@ class PangeaRichTextState extends State { widget.toolbarController.toolbar?.textSelection.setMessageText( repEvent?.text ?? widget.pangeaMessageEvent.body, ); - }).whenComplete(() => setState(() => _fetchingRepresentation = false)); + }).whenComplete(() { + if (mounted) { + setState(() => _fetchingRepresentation = false); + } + }); return widget.pangeaMessageEvent.body; } diff --git a/pubspec.yaml b/pubspec.yaml index b36f4c64c..c759327fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,9 @@ dependencies: chewie: ^1.7.1 collection: ^1.17.2 cupertino_icons: any + # #Pangea # desktop_drop: ^0.4.4 + # Pangea# desktop_notifications: ^0.6.3 device_info_plus: ^9.1.0 dynamic_color: ^1.6.8 From 648b25a25276db367387029ce764cd707066a09a Mon Sep 17 00:00:00 2001 From: Gabby Gurdin Date: Sat, 10 Feb 2024 10:44:58 -0500 Subject: [PATCH 8/8] fix for long press selecting messages --- lib/pages/chat/events/message.dart | 16 ++++++---------- lib/pages/chat/events/message_content.dart | 16 +++++++++++++--- lib/pangea/widgets/igc/pangea_rich_text.dart | 14 +++++++++++--- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 971e85a58..20574ac2d 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -246,17 +246,13 @@ class Message extends StatelessWidget { onTap: () => toolbarController?.showToolbar(context), onDoubleTap: () => toolbarController?.showToolbar(context), - onLongPress: () { - onSelect(event); - HapticFeedback.selectionClick(); - }, - // onLongPress: longPressSelect - // ? null - // : () { - // onSelect(event); - // HapticFeedback.selectionClick(); - // }, // Pangea# + onLongPress: longPressSelect + ? null + : () { + onSelect(event); + HapticFeedback.selectionClick(); + }, child: AnimatedOpacity( opacity: animateIn ? 0 diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index c772d3582..def3b2975 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -303,9 +303,19 @@ class MessageContent extends StatelessWidget { messageText, ); return SelectableLinkify( - onSelectionChanged: (selection, cause) => toolbarController - ?.toolbar?.textSelection - .onTextSelection(selection), + onSelectionChanged: (selection, cause) { + if (cause == SelectionChangedCause.longPress && + toolbarController != null && + pangeaMessageEvent != null && + !(toolbarController!.highlighted)) { + toolbarController!.controller.onSelectMessage( + pangeaMessageEvent!.event, + ); + return; + } + toolbarController?.toolbar?.textSelection + .onTextSelection(selection); + }, onTap: () => toolbarController?.showToolbar(context), text: toolbarController?.toolbar?.textSelection.messageText ?? messageText, diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index d0a7ed11e..68deabcb0 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -95,9 +95,17 @@ class PangeaRichTextState extends State { Widget build(BuildContext context) { //TODO - take out of build function of every message final Widget richText = SelectableText.rich( - onSelectionChanged: (selection, cause) => widget - .toolbarController.toolbar?.textSelection - .onTextSelection(selection), + onSelectionChanged: (selection, cause) { + if (cause == SelectionChangedCause.longPress && + !widget.toolbarController.highlighted) { + widget.toolbarController.controller.onSelectMessage( + widget.pangeaMessageEvent.event, + ); + return; + } + widget.toolbarController.toolbar?.textSelection + .onTextSelection(selection); + }, onTap: () => widget.toolbarController.showToolbar(context), focusNode: widget.toolbarController.focusNode, contextMenuBuilder: (context, state) =>