diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 1b647b2ae..e7fb1d376 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -653,6 +653,9 @@ abstract class AppRoutes { state, ChatMembersPage( roomId: state.pathParameters['roomid']!, + // #Pangea + filter: state.uri.queryParameters['filter'], + // Pangea# ), ), redirect: loggedOutRedirect, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2d44b7524..b85b940fe 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5020,5 +5020,16 @@ "endNow": "End now", "setDuration": "Set duration", "activityEnded": "That’s a wrap for this activity! Big thanks to everyone for chatting, learning, and making this space so lively. Language grows with conversation, and every word exchanged brings us closer to confidence and fluency.\n\nKeep practicing, stay curious, and don’t be shy to keep the conversation going!", - "duration": "Duration" + "duration": "Duration", + "transcriptionFailed": "Failed to transcribe audio", + "aUserIsKnocking": "1 user is requesting to join your space", + "usersAreKnocking": "{users} users are requesting to join your space", + "@usersAreKnocking": { + "type": "int", + "placeholders": { + "users": { + "type": "int" + } + } + } } \ No newline at end of file diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index d7af1008f..ddfa041b3 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -407,7 +407,10 @@ class HtmlMessage extends StatelessWidget { avatar: user.avatarUrl, uri: href, outerContext: context, - fontSize: fontSize, + // #Pangea + // fontSize: fontSize, + fontSize: renderer.fontSize(context), + // Pangea# color: linkStyle.color, // #Pangea userId: user.id, @@ -428,7 +431,10 @@ class HtmlMessage extends StatelessWidget { avatar: room?.avatar, uri: href, outerContext: context, - fontSize: fontSize, + // #Pangea + // fontSize: fontSize, + fontSize: renderer.fontSize(context), + // Pangea# color: linkStyle.color, ), ); diff --git a/lib/pages/chat/events/message_reactions.dart b/lib/pages/chat/events/message_reactions.dart index e34dad9a0..b3afbef60 100644 --- a/lib/pages/chat/events/message_reactions.dart +++ b/lib/pages/chat/events/message_reactions.dart @@ -158,7 +158,9 @@ class _Reaction extends StatelessWidget { borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), child: Container( decoration: BoxDecoration( - color: color, + // #Pangea + // color: color, + // Pangea# borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), ), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index a68112cf8..492804381 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -89,39 +89,45 @@ class NaviRailItem extends StatelessWidget { // color: isSelected // ? theme.colorScheme.primaryContainer // : theme.colorScheme.surfaceContainerHigh, - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: backgroundColor ?? - (isSelected - ? theme.colorScheme.primaryContainer - : theme.colorScheme.surfaceContainerHigh), - borderRadius: borderRadius, + child: UnreadRoomsBadge( + filter: unreadBadgeFilter ?? (_) => false, + badgePosition: BadgePosition.topEnd( + top: -4, + end: isColumnMode ? 8 : 4, ), - margin: EdgeInsets.symmetric( - horizontal: isColumnMode ? 16.0 : 12.0, - vertical: isColumnMode ? 8.0 : 6.0, - ), - // Pangea# - child: Tooltip( - message: toolTip, - child: InkWell( + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: backgroundColor ?? + (isSelected + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerHigh), borderRadius: borderRadius, - onTap: onTap, - child: unreadBadgeFilter == null - ? icon - : UnreadRoomsBadge( - filter: unreadBadgeFilter, - badgePosition: BadgePosition.topEnd( - // #Pangea - // top: -12, - // end: -8, - top: -20, - end: -16, - // Pangea# - ), - child: icon, - ), + ), + margin: EdgeInsets.symmetric( + horizontal: isColumnMode ? 16.0 : 12.0, + vertical: isColumnMode ? 8.0 : 6.0, + ), + // Pangea# + child: Tooltip( + message: toolTip, + child: InkWell( + borderRadius: borderRadius, + onTap: onTap, + // #Pangea + child: icon, + // child: unreadBadgeFilter == null + // ? icon + // : UnreadRoomsBadge( + // filter: unreadBadgeFilter, + // badgePosition: BadgePosition.topEnd( + // top: -12, + // end: -8, + // ), + // child: icon, + // ), + // Pangea# + ), ), ), ), diff --git a/lib/pages/chat_members/chat_members.dart b/lib/pages/chat_members/chat_members.dart index 6376a9b2c..971b538f4 100644 --- a/lib/pages/chat_members/chat_members.dart +++ b/lib/pages/chat_members/chat_members.dart @@ -9,8 +9,18 @@ import 'chat_members_view.dart'; class ChatMembersPage extends StatefulWidget { final String roomId; + // #Pangea + final String? filter; + // Pangea# - const ChatMembersPage({required this.roomId, super.key}); + // #Pangea + // const ChatMembersPage({required this.roomId, super.key}); + const ChatMembersPage({ + required this.roomId, + this.filter, + super.key, + }); + // Pangea# @override State createState() => ChatMembersController(); @@ -24,6 +34,22 @@ class ChatMembersController extends State { final TextEditingController filterController = TextEditingController(); + // #Pangea + @override + void didUpdateWidget(ChatMembersPage oldWidget) { + super.didUpdateWidget(oldWidget); + // Update the membership filter if the widget's filter changes + if (oldWidget.filter != widget.filter) { + setState(() { + membershipFilter = Membership.values.firstWhere( + (membership) => membership.name == widget.filter, + orElse: () => Membership.join, + ); + }); + } + } + // Pangea# + void setMembershipFilter(Membership membership) { membershipFilter = membership; setFilter(); @@ -79,6 +105,19 @@ class ChatMembersController extends State { if (!mounted) return; + // #Pangea + final availableFilters = (participants ?? []) + .map( + (p) => p.membership, + ) + .toSet(); + + if (availableFilters.length == 1 && + membershipFilter != availableFilters.first) { + membershipFilter = availableFilters.first; + } + // Pangea# + setState(() { members = participants; }); @@ -110,6 +149,15 @@ class ChatMembersController extends State { false, ) .listen(refreshMembers); + + // #Pangea + if (widget.filter != null) { + membershipFilter = Membership.values.firstWhere( + (membership) => membership.name == widget.filter, + orElse: () => Membership.join, + ); + } + // Pangea# } @override diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 56fabe42c..d472a64b1 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -11,6 +11,8 @@ import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_icon.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; +import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -26,6 +28,9 @@ class VocabDetailsView extends StatelessWidget { ConstructUses get _construct => constructId.constructUses; + String? get _userL1 => + MatrixState.pangeaController.languageController.userL1?.langCode; + /// Get the language code for the current lemma String? get _userL2 => MatrixState.pangeaController.languageController.userL2?.langCode; @@ -49,14 +54,34 @@ class VocabDetailsView extends StatelessWidget { : _construct.lemmaCategory.darkColor(context)); return AnalyticsDetailsViewContent( - title: WordTextWithAudioButton( - text: _construct.lemma, - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: textColor, + title: Column( + children: [ + LayoutBuilder( + builder: (context, constraints) { + return ShrinkableText( + text: _construct.lemma, + maxWidth: constraints.maxWidth - 40.0, + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: textColor, + ), + ); + }, + ), + if (MatrixState.pangeaController.languageController.userL2 != null) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: PhoneticTranscriptionWidget( + text: _construct.lemma, + textLanguage: + MatrixState.pangeaController.languageController.userL2!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: textColor.withAlpha((0.7 * 255).toInt()), + fontSize: 18, + ), + iconSize: _iconSize * 0.8, + ), ), - iconSize: _iconSize, - uniqueID: "${_construct.lemma}-${_construct.category}", - langCode: _userL2!, + ], ), subtitle: Column( children: [ diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index a667066c8..5bb4f2c86 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -86,4 +86,7 @@ class PApiUrls { static String rcProductsTrial = "${PApiUrls.subscriptionEndpoint}/free_trial"; static String rcSubscription = PApiUrls.subscriptionEndpoint; + + static String phoneticTranscription = + "${PApiUrls.choreoEndpoint}/phonetic_transcription"; } diff --git a/lib/pangea/events/models/pangea_token_text_model.dart b/lib/pangea/events/models/pangea_token_text_model.dart index 7f7323cf6..efef9a712 100644 --- a/lib/pangea/events/models/pangea_token_text_model.dart +++ b/lib/pangea/events/models/pangea_token_text_model.dart @@ -22,6 +22,14 @@ class PangeaTokenText { ); } + static PangeaTokenText fromString(String content) { + return PangeaTokenText( + offset: 0, + content: content, + length: content.length, + ); + } + static const String _offsetKey = "offset"; static const String _contentKey = "content"; static const String _lengthKey = "length"; diff --git a/lib/pangea/learning_settings/models/language_model.dart b/lib/pangea/learning_settings/models/language_model.dart index 61f021e91..1e1ef69b9 100644 --- a/lib/pangea/learning_settings/models/language_model.dart +++ b/lib/pangea/learning_settings/models/language_model.dart @@ -80,3 +80,27 @@ class LanguageModel { @override int get hashCode => langCode.hashCode; } + +class LanguageArc { + final LanguageModel l1; + final LanguageModel l2; + + LanguageArc({ + required this.l1, + required this.l2, + }); + + factory LanguageArc.fromJson(Map json) { + return LanguageArc( + l1: LanguageModel.fromJson(json['l1'] as Map), + l2: LanguageModel.fromJson(json['l2'] as Map), + ); + } + + Map toJson() { + return { + 'l1': l1.toJson(), + 'l2': l2.toJson(), + }; + } +} diff --git a/lib/pangea/learning_settings/pages/settings_learning_view.dart b/lib/pangea/learning_settings/pages/settings_learning_view.dart index 74c8cb0c6..c521e9717 100644 --- a/lib/pangea/learning_settings/pages/settings_learning_view.dart +++ b/lib/pangea/learning_settings/pages/settings_learning_view.dart @@ -192,13 +192,15 @@ class SettingsLearningView extends StatelessWidget { .colorScheme .error, ), - Text( - L10n.of(context) - .noIdenticalLanguages, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .error, + Flexible( + child: Text( + L10n.of(context) + .noIdenticalLanguages, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .error, + ), ), ), ], diff --git a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart index 462eaf650..c71fea714 100644 --- a/lib/pangea/learning_settings/widgets/p_language_dropdown.dart +++ b/lib/pangea/learning_settings/widgets/p_language_dropdown.dart @@ -239,15 +239,17 @@ class LanguageDropDownEntry extends StatelessWidget { Expanded( child: Row( children: [ - Text( - languageModel.getDisplayName(context) ?? "", - style: const TextStyle().copyWith( - color: enabled - ? Theme.of(context).textTheme.bodyLarge!.color - : Theme.of(context).disabledColor, - fontSize: 14, + Flexible( + child: Text( + languageModel.getDisplayName(context) ?? "", + style: const TextStyle().copyWith( + color: enabled + ? Theme.of(context).textTheme.bodyLarge!.color + : Theme.of(context).disabledColor, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), const SizedBox(width: 10), if (isL2List && languageModel.l2Support != L2SupportEnum.full) diff --git a/lib/pangea/lemmas/lemma_reaction_picker.dart b/lib/pangea/lemmas/lemma_reaction_picker.dart index b229f2396..92a3a70ef 100644 --- a/lib/pangea/lemmas/lemma_reaction_picker.dart +++ b/lib/pangea/lemmas/lemma_reaction_picker.dart @@ -28,17 +28,35 @@ class LemmaReactionPickerState extends State { @override void initState() { super.initState(); - widget.cId.getLemmaInfo().then((info) { - loading = false; - setState(() => displayEmoji = info.emoji); - }).catchError((e, s) { - ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s); - setState(() => loading = false); - }); + _refresh(); + } + + @override + void didUpdateWidget(LemmaReactionPicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.cId != widget.cId) { + _refresh(); + } } void setEmoji(String emoji) => widget.controller.sendEmojiAction(emoji); + Future _refresh() async { + setState(() { + loading = true; + displayEmoji = []; + }); + + try { + final info = await widget.cId.getLemmaInfo(); + displayEmoji = info.emoji; + } catch (e, s) { + ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s); + } finally { + setState(() => loading = false); + } + } + @override Widget build(BuildContext context) { return Container( diff --git a/lib/pangea/onboarding/onboarding_complete.dart b/lib/pangea/onboarding/onboarding_complete.dart index cbc3c821b..796602d28 100644 --- a/lib/pangea/onboarding/onboarding_complete.dart +++ b/lib/pangea/onboarding/onboarding_complete.dart @@ -19,7 +19,10 @@ class OnboardingComplete extends StatelessWidget { children: [ Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface.withAlpha(20), + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh + .withAlpha(170), borderRadius: BorderRadius.circular( 10.0, ), diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart new file mode 100644 index 000000000..124389eed --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_repo.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; + +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_response.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PhoneticTranscriptionRepo { + static final GetStorage _storage = + GetStorage('phonetic_transcription_storage'); + + static void set( + PhoneticTranscriptionRequest request, + PhoneticTranscriptionResponse response, + ) { + response.expireAt ??= DateTime.now().add(const Duration(days: 100)); + _storage.write(request.storageKey, response.toJson()); + } + + static Future _fetch( + PhoneticTranscriptionRequest request, + ) async { + final cachedJson = _storage.read(request.storageKey); + final cached = cachedJson == null + ? null + : PhoneticTranscriptionResponse.fromJson(cachedJson); + + if (cached != null) { + if (DateTime.now().isBefore(cached.expireAt!)) { + return cached; + } else { + _storage.remove(request.storageKey); + } + } + + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.phoneticTranscription, + body: request.toJson(), + ); + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + final response = PhoneticTranscriptionResponse.fromJson(decodedBody); + set(request, response); + return response; + } + + static Future get( + PhoneticTranscriptionRequest request, + ) async { + try { + return await _fetch(request); + } catch (e) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, data: request.toJson()); + rethrow; + } + } +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart new file mode 100644 index 000000000..cbd22fc22 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_request.dart @@ -0,0 +1,33 @@ +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + +class PhoneticTranscriptionRequest { + final LanguageArc arc; + final PangeaTokenText content; + final bool requiresTokenization; + + PhoneticTranscriptionRequest({ + required this.arc, + required this.content, + this.requiresTokenization = false, + }); + + factory PhoneticTranscriptionRequest.fromJson(Map json) { + return PhoneticTranscriptionRequest( + arc: LanguageArc.fromJson(json['arc'] as Map), + content: + PangeaTokenText.fromJson(json['content'] as Map), + requiresTokenization: json['requires_tokenization'] ?? true, + ); + } + + Map toJson() { + return { + 'arc': arc.toJson(), + 'content': content.toJson(), + 'requires_tokenization': requiresTokenization, + }; + } + + String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}'; +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart new file mode 100644 index 000000000..a4cd2b3a3 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_response.dart @@ -0,0 +1,156 @@ +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; + +enum PhoneticTranscriptionDelimEnum { sp, noSp } + +extension PhoneticTranscriptionDelimEnumExt on PhoneticTranscriptionDelimEnum { + String get value { + switch (this) { + case PhoneticTranscriptionDelimEnum.sp: + return " "; + case PhoneticTranscriptionDelimEnum.noSp: + return ""; + } + } + + static PhoneticTranscriptionDelimEnum fromString(String s) { + switch (s) { + case " ": + return PhoneticTranscriptionDelimEnum.sp; + case "": + return PhoneticTranscriptionDelimEnum.noSp; + default: + return PhoneticTranscriptionDelimEnum.sp; + } + } +} + +class PhoneticTranscriptionToken { + final LanguageArc arc; + final PangeaTokenText tokenL2; + final PangeaTokenText phoneticL1Transcription; + + PhoneticTranscriptionToken({ + required this.arc, + required this.tokenL2, + required this.phoneticL1Transcription, + }); + + factory PhoneticTranscriptionToken.fromJson(Map json) { + return PhoneticTranscriptionToken( + arc: LanguageArc.fromJson(json['arc'] as Map), + tokenL2: + PangeaTokenText.fromJson(json['token_l2'] as Map), + phoneticL1Transcription: PangeaTokenText.fromJson( + json['phonetic_l1_transcription'] as Map, + ), + ); + } + + Map toJson() => { + 'arc': arc.toJson(), + 'token_l2': tokenL2.toJson(), + 'phonetic_l1_transcription': phoneticL1Transcription.toJson(), + }; +} + +class PhoneticTranscription { + final LanguageArc arc; + final PangeaTokenText transcriptionL2; + final List phoneticTranscription; + final PhoneticTranscriptionDelimEnum delim; + + PhoneticTranscription({ + required this.arc, + required this.transcriptionL2, + required this.phoneticTranscription, + this.delim = PhoneticTranscriptionDelimEnum.sp, + }); + + factory PhoneticTranscription.fromJson(Map json) { + return PhoneticTranscription( + arc: LanguageArc.fromJson(json['arc'] as Map), + transcriptionL2: PangeaTokenText.fromJson( + json['transcription_l2'] as Map, + ), + phoneticTranscription: (json['phonetic_transcription'] as List) + .map( + (e) => + PhoneticTranscriptionToken.fromJson(e as Map), + ) + .toList(), + delim: json['delim'] != null + ? PhoneticTranscriptionDelimEnumExt.fromString( + json['delim'] as String, + ) + : PhoneticTranscriptionDelimEnum.sp, + ); + } + + Map toJson() => { + 'arc': arc.toJson(), + 'transcription_l2': transcriptionL2.toJson(), + 'phonetic_transcription': + phoneticTranscription.map((e) => e.toJson()).toList(), + 'delim': delim.value, + }; +} + +class PhoneticTranscriptionResponse { + final LanguageArc arc; + final PangeaTokenText content; + final Map + tokenization; // You can define a typesafe model if needed + final PhoneticTranscription phoneticTranscriptionResult; + DateTime? expireAt; + + PhoneticTranscriptionResponse({ + required this.arc, + required this.content, + required this.tokenization, + required this.phoneticTranscriptionResult, + this.expireAt, + }); + + factory PhoneticTranscriptionResponse.fromJson(Map json) { + return PhoneticTranscriptionResponse( + arc: LanguageArc.fromJson(json['arc'] as Map), + content: + PangeaTokenText.fromJson(json['content'] as Map), + tokenization: Map.from(json['tokenization'] as Map), + phoneticTranscriptionResult: PhoneticTranscription.fromJson( + json['phonetic_transcription_result'] as Map, + ), + expireAt: json['expireAt'] == null + ? null + : DateTime.parse(json['expireAt'] as String), + ); + } + + Map toJson() { + return { + 'arc': arc.toJson(), + 'content': content.toJson(), + 'tokenization': tokenization, + 'phonetic_transcription_result': phoneticTranscriptionResult.toJson(), + 'expireAt': expireAt?.toIso8601String(), + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PhoneticTranscriptionResponse && + runtimeType == other.runtimeType && + arc == other.arc && + content == other.content && + tokenization == other.tokenization && + phoneticTranscriptionResult == other.phoneticTranscriptionResult; + + @override + int get hashCode => + arc.hashCode ^ + content.hashCode ^ + tokenization.hashCode ^ + phoneticTranscriptionResult.hashCode; +} diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart new file mode 100644 index 000000000..0e7c69f74 --- /dev/null +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_repo.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_request.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class PhoneticTranscriptionWidget extends StatefulWidget { + final String text; + final LanguageModel textLanguage; + final TextStyle? style; + final double? iconSize; + + const PhoneticTranscriptionWidget({ + super.key, + required this.text, + required this.textLanguage, + this.style, + this.iconSize, + }); + + @override + State createState() => + _PhoneticTranscriptionWidgetState(); +} + +class _PhoneticTranscriptionWidgetState + extends State { + late Future _transcriptionFuture; + bool _hovering = false; + bool _isPlaying = false; + bool _isLoading = false; + late final StreamSubscription _loadingChoreoSubscription; + + @override + void initState() { + super.initState(); + _transcriptionFuture = _fetchTranscription(); + _loadingChoreoSubscription = + TtsController.loadingChoreoStream.stream.listen((val) { + if (mounted) setState(() => _isLoading = val); + }); + } + + @override + void dispose() { + TtsController.stop(); + _loadingChoreoSubscription.cancel(); + super.dispose(); + } + + Future _fetchTranscription() async { + if (MatrixState.pangeaController.languageController.userL1 == null) { + ErrorHandler.logError( + e: Exception('User L1 is not set'), + data: { + 'text': widget.text, + 'textLanguageCode': widget.textLanguage.langCode, + }, + ); + return widget.text; // Fallback to original text if no L1 is set + } + final req = PhoneticTranscriptionRequest( + arc: LanguageArc( + l1: MatrixState.pangeaController.languageController.userL1!, + l2: widget.textLanguage, + ), + content: PangeaTokenText.fromString(widget.text), + // arc can be omitted for default empty map + ); + final res = await PhoneticTranscriptionRepo.get(req); + return res.phoneticTranscriptionResult.phoneticTranscription.first + .phoneticL1Transcription.content; + } + + Future _handleAudioTap(BuildContext context) async { + if (_isPlaying) { + await TtsController.stop(); + setState(() => _isPlaying = false); + } else { + await TtsController.tryToSpeak( + widget.text, + context: context, + targetID: 'phonetic-transcription-${widget.text}', + langCode: widget.textLanguage.langCode, + onStart: () { + if (mounted) setState(() => _isPlaying = true); + }, + onStop: () { + if (mounted) setState(() => _isPlaying = false); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _transcriptionFuture, + builder: (context, snapshot) { + final transcription = snapshot.data ?? ''; + return MouseRegion( + onEnter: (_) => setState(() => _hovering = true), + onExit: (_) => setState(() => _hovering = false), + child: GestureDetector( + onTap: () => _handleAudioTap(context), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + decoration: BoxDecoration( + color: _hovering + ? Colors.grey.withAlpha((0.2 * 255).round()) + : Colors.transparent, + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + "/${transcription.isNotEmpty ? transcription : widget.text}/", + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(width: 8), + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 3), + ) + : Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + size: widget.iconSize ?? 24, + color: _isPlaying + ? Theme.of(context).colorScheme.primary + : Theme.of(context).iconTheme.color, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pangea/public_spaces/public_room_bottom_sheet.dart b/lib/pangea/public_spaces/public_room_bottom_sheet.dart index ba0b71fc7..89ed09700 100644 --- a/lib/pangea/public_spaces/public_room_bottom_sheet.dart +++ b/lib/pangea/public_spaces/public_room_bottom_sheet.dart @@ -200,12 +200,14 @@ class PublicRoomBottomSheetState extends State { Row( spacing: 16.0, children: [ - (chunk?.avatarUrl != null) + (chunk?.avatarUrl != null || chunk?.roomType != 'm.space') ? Avatar( mxContent: chunk?.avatarUrl, name: chunk?.name, size: 160.0, - borderRadius: BorderRadius.circular(24.0), + borderRadius: BorderRadius.circular( + chunk?.roomType != 'm.space' ? 80 : 24.0, + ), ) : ClipRRect( borderRadius: BorderRadius.circular(24.0), @@ -242,7 +244,11 @@ class PublicRoomBottomSheetState extends State { child: SingleChildScrollView( child: Text( chunk?.topic ?? - L10n.of(context).noSpaceDescriptionYet, + (chunk?.roomType != 'm.space' + ? L10n.of(context) + .noChatDescriptionYet + : L10n.of(context) + .noSpaceDescriptionYet), softWrap: true, textAlign: TextAlign.start, maxLines: null, diff --git a/lib/pangea/spaces/widgets/knocking_users_indicator.dart b/lib/pangea/spaces/widgets/knocking_users_indicator.dart index 76cc30959..b40519584 100644 --- a/lib/pangea/spaces/widgets/knocking_users_indicator.dart +++ b/lib/pangea/spaces/widgets/knocking_users_indicator.dart @@ -7,6 +7,7 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; @@ -89,15 +90,16 @@ class KnockingUsersIndicatorState extends State { Expanded( child: Text( _knockingUsers.length == 1 - ? "1 user is requesting to join your space" - : "${_knockingUsers.length} users are requesting to join your space", + ? L10n.of(context).aUserIsKnocking + : L10n.of(context) + .usersAreKnocking(_knockingUsers.length), style: Theme.of(context).textTheme.bodyMedium, ), ), ], ), onTap: () => context.push( - "/rooms/${widget.room.id}/details/members", + "/rooms/${widget.room.id}/details/members?filter=knock", ), ), ), diff --git a/lib/pangea/spaces/widgets/leaderboard_participant_list.dart b/lib/pangea/spaces/widgets/leaderboard_participant_list.dart index 347d12cd2..c9906cea9 100644 --- a/lib/pangea/spaces/widgets/leaderboard_participant_list.dart +++ b/lib/pangea/spaces/widgets/leaderboard_participant_list.dart @@ -48,7 +48,10 @@ class LeaderboardParticipantListState return LoadParticipantsUtil( space: widget.space, builder: (participantsLoader) { - final participants = participantsLoader.filteredParticipants(""); + final participants = participantsLoader + .filteredParticipants("") + .where((p) => p.membership == Membership.join) + .toList(); return AnimatedSize( duration: FluffyThemes.animationDuration, diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index d6d2cc27f..8efbeeedc 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -27,7 +27,7 @@ class TokenRenderingUtil { return readingAssistanceMode == ReadingAssistanceMode.transitionMode; } - double? _fontSize(BuildContext context) => showCenterStyling + double? fontSize(BuildContext context) => showCenterStyling ? overlayController != null && overlayController!.maxWidth > 600 ? Theme.of(context).textTheme.titleLarge?.fontSize : Theme.of(context).textTheme.bodyLarge?.fontSize @@ -38,14 +38,14 @@ class TokenRenderingUtil { Color? color, }) => existingStyle.copyWith( - fontSize: _fontSize(context), + fontSize: fontSize(context), decoration: TextDecoration.underline, decorationThickness: 4, decorationColor: color ?? Colors.white.withAlpha(0), ); double tokenTextWidthForContainer(BuildContext context, String text) { - final tokenSizeKey = "$text-${_fontSize(context)}"; + final tokenSizeKey = "$text-${fontSize(context)}"; if (_tokensWidthCache.containsKey(tokenSizeKey)) { return _tokensWidthCache[tokenSizeKey]!; } diff --git a/lib/pangea/toolbar/widgets/overlay_message.dart b/lib/pangea/toolbar/widgets/overlay_message.dart index ed2f8b71e..aa3300f3c 100644 --- a/lib/pangea/toolbar/widgets/overlay_message.dart +++ b/lib/pangea/toolbar/widgets/overlay_message.dart @@ -10,6 +10,9 @@ import 'package:fluffychat/pages/chat/events/message_content.dart'; import 'package:fluffychat/pages/chat/events/reply_content.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; +import 'package:fluffychat/pangea/learning_settings/utils/p_language_store.dart'; +import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; @@ -160,7 +163,7 @@ class OverlayMessage extends StatelessWidget { ), const SizedBox(width: 8), Text( - L10n.of(context).oopsSomethingWentWrong, + L10n.of(context).transcriptionFailed, style: AppConfig.messageTextStyle( event, textColor, @@ -170,14 +173,34 @@ class OverlayMessage extends StatelessWidget { ) : overlayController.transcription != null ? SingleChildScrollView( - child: Text( - overlayController.transcription!.transcript.text, - style: AppConfig.messageTextStyle( - event, - textColor, - ).copyWith( - fontStyle: FontStyle.italic, - ), + child: Column( + spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + overlayController + .transcription!.transcript.text, + style: AppConfig.messageTextStyle( + event, + textColor, + ).copyWith( + fontStyle: FontStyle.italic, + ), + ), + PhoneticTranscriptionWidget( + text: overlayController + .transcription!.transcript.text, + textLanguage: PLanguageStore.byLangCode( + pangeaMessageEvent! + .messageDisplayLangCode, + ) ?? + LanguageModel.unknown, + style: AppConfig.messageTextStyle( + event, + textColor, + ), + ), + ], ), ) : Row( diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart index 0c353c3f4..07f5d3722 100644 --- a/lib/pangea/toolbar/widgets/select_mode_buttons.dart +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -37,11 +37,10 @@ enum SelectMode { case SelectMode.audio: return l10n.playAudio; case SelectMode.translate: + case SelectMode.speechTranslation: return l10n.translationTooltip; case SelectMode.practice: return l10n.practice; - case SelectMode.speechTranslation: - return l10n.speechToTextTooltip; } } } diff --git a/lib/widgets/member_actions_popup_menu_button.dart b/lib/widgets/member_actions_popup_menu_button.dart index 6502beedd..f4ff7c3ca 100644 --- a/lib/widgets/member_actions_popup_menu_button.dart +++ b/lib/widgets/member_actions_popup_menu_button.dart @@ -90,20 +90,21 @@ void showMemberActionsPopupMenu({ ), const PopupMenuDivider(), // #Pangea - PopupMenuItem( - value: _MemberActions.chat, - child: Row( - children: [ - const Icon(Icons.forum_outlined), - const SizedBox(width: 18), - Text( - dmRoomId == null - ? L10n.of(context).startConversation - : L10n.of(context).sendAMessage, - ), - ], + if (user.room.client.userID != user.id) + PopupMenuItem( + value: _MemberActions.chat, + child: Row( + children: [ + const Icon(Icons.forum_outlined), + const SizedBox(width: 18), + Text( + dmRoomId == null + ? L10n.of(context).startConversation + : L10n.of(context).sendAMessage, + ), + ], + ), ), - ), // Pangea# if (onMention != null) PopupMenuItem( diff --git a/lib/widgets/presence_builder.dart b/lib/widgets/presence_builder.dart index 2f1a66d77..4332485ab 100644 --- a/lib/widgets/presence_builder.dart +++ b/lib/widgets/presence_builder.dart @@ -48,6 +48,24 @@ class _PresenceBuilderState extends State { } } + // #Pangea + @override + void didUpdateWidget(PresenceBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.userId == widget.userId) return; + + final client = widget.client ?? Matrix.of(context).client; + final userId = widget.userId; + if (userId != null) { + client.fetchCurrentPresence(userId).then(_updatePresence); + _sub?.cancel(); + _sub = client.onPresenceChanged.stream + .where((presence) => presence.userid == userId) + .listen(_updatePresence); + } + } + // Pangea# + @override void dispose() { _sub?.cancel();