resolve merge conflict

This commit is contained in:
ggurdin 2025-06-17 10:57:00 -04:00
commit 88f7ea400b
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
26 changed files with 733 additions and 100 deletions

View file

@ -653,6 +653,9 @@ abstract class AppRoutes {
state,
ChatMembersPage(
roomId: state.pathParameters['roomid']!,
// #Pangea
filter: state.uri.queryParameters['filter'],
// Pangea#
),
),
redirect: loggedOutRedirect,

View file

@ -5020,5 +5020,16 @@
"endNow": "End now",
"setDuration": "Set duration",
"activityEnded": "Thats 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 dont 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"
}
}
}
}

View file

@ -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,
),
);

View file

@ -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),

View file

@ -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#
),
),
),
),

View file

@ -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<ChatMembersPage> createState() => ChatMembersController();
@ -24,6 +34,22 @@ class ChatMembersController extends State<ChatMembersPage> {
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<ChatMembersPage> {
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<ChatMembersPage> {
false,
)
.listen(refreshMembers);
// #Pangea
if (widget.filter != null) {
membershipFilter = Membership.values.firstWhere(
(membership) => membership.name == widget.filter,
orElse: () => Membership.join,
);
}
// Pangea#
}
@override

View file

@ -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: [

View file

@ -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";
}

View file

@ -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";

View file

@ -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<String, dynamic> json) {
return LanguageArc(
l1: LanguageModel.fromJson(json['l1'] as Map<String, dynamic>),
l2: LanguageModel.fromJson(json['l2'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() {
return {
'l1': l1.toJson(),
'l2': l2.toJson(),
};
}
}

View file

@ -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,
),
),
),
],

View file

@ -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)

View file

@ -28,17 +28,35 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
@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<void> _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(

View file

@ -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,
),

View file

@ -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<PhoneticTranscriptionResponse> _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<PhoneticTranscriptionResponse> get(
PhoneticTranscriptionRequest request,
) async {
try {
return await _fetch(request);
} catch (e) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, data: request.toJson());
rethrow;
}
}
}

View file

@ -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<String, dynamic> json) {
return PhoneticTranscriptionRequest(
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
content:
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
requiresTokenization: json['requires_tokenization'] ?? true,
);
}
Map<String, dynamic> toJson() {
return {
'arc': arc.toJson(),
'content': content.toJson(),
'requires_tokenization': requiresTokenization,
};
}
String get storageKey => '${arc.l1}-${arc.l2}-${content.hashCode}';
}

View file

@ -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<String, dynamic> json) {
return PhoneticTranscriptionToken(
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
tokenL2:
PangeaTokenText.fromJson(json['token_l2'] as Map<String, dynamic>),
phoneticL1Transcription: PangeaTokenText.fromJson(
json['phonetic_l1_transcription'] as Map<String, dynamic>,
),
);
}
Map<String, dynamic> toJson() => {
'arc': arc.toJson(),
'token_l2': tokenL2.toJson(),
'phonetic_l1_transcription': phoneticL1Transcription.toJson(),
};
}
class PhoneticTranscription {
final LanguageArc arc;
final PangeaTokenText transcriptionL2;
final List<PhoneticTranscriptionToken> phoneticTranscription;
final PhoneticTranscriptionDelimEnum delim;
PhoneticTranscription({
required this.arc,
required this.transcriptionL2,
required this.phoneticTranscription,
this.delim = PhoneticTranscriptionDelimEnum.sp,
});
factory PhoneticTranscription.fromJson(Map<String, dynamic> json) {
return PhoneticTranscription(
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
transcriptionL2: PangeaTokenText.fromJson(
json['transcription_l2'] as Map<String, dynamic>,
),
phoneticTranscription: (json['phonetic_transcription'] as List)
.map(
(e) =>
PhoneticTranscriptionToken.fromJson(e as Map<String, dynamic>),
)
.toList(),
delim: json['delim'] != null
? PhoneticTranscriptionDelimEnumExt.fromString(
json['delim'] as String,
)
: PhoneticTranscriptionDelimEnum.sp,
);
}
Map<String, dynamic> 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<String, dynamic>
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<String, dynamic> json) {
return PhoneticTranscriptionResponse(
arc: LanguageArc.fromJson(json['arc'] as Map<String, dynamic>),
content:
PangeaTokenText.fromJson(json['content'] as Map<String, dynamic>),
tokenization: Map<String, dynamic>.from(json['tokenization'] as Map),
phoneticTranscriptionResult: PhoneticTranscription.fromJson(
json['phonetic_transcription_result'] as Map<String, dynamic>,
),
expireAt: json['expireAt'] == null
? null
: DateTime.parse(json['expireAt'] as String),
);
}
Map<String, dynamic> 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;
}

View file

@ -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<PhoneticTranscriptionWidget> createState() =>
_PhoneticTranscriptionWidgetState();
}
class _PhoneticTranscriptionWidgetState
extends State<PhoneticTranscriptionWidget> {
late Future<String?> _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<String?> _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<void> _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<String?>(
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,
),
),
],
),
),
),
);
},
);
}
}

View file

@ -200,12 +200,14 @@ class PublicRoomBottomSheetState extends State<PublicRoomBottomSheet> {
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<PublicRoomBottomSheet> {
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,

View file

@ -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<KnockingUsersIndicator> {
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",
),
),
),

View file

@ -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,

View file

@ -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]!;
}

View file

@ -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(

View file

@ -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;
}
}
}

View file

@ -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(

View file

@ -48,6 +48,24 @@ class _PresenceBuilderState extends State<PresenceBuilder> {
}
}
// #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();