Merge branch 'main' into 3148-when-bot-responds-in-an-audio-message-play-that-message-without-a-click
This commit is contained in:
commit
7613a63898
12 changed files with 261 additions and 120 deletions
|
|
@ -37,7 +37,6 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
final PangeaAudioFile? matrixFile;
|
||||
final ChatController chatController;
|
||||
final MessageOverlayController? overlayController;
|
||||
final VoidCallback? onPlay;
|
||||
final bool autoplay;
|
||||
// Pangea#
|
||||
|
||||
|
|
@ -55,7 +54,6 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
this.matrixFile,
|
||||
required this.chatController,
|
||||
this.overlayController,
|
||||
this.onPlay,
|
||||
this.autoplay = false,
|
||||
// Pangea#
|
||||
super.key,
|
||||
|
|
@ -465,11 +463,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
onLongPress: () =>
|
||||
widget.event?.saveFile(context),
|
||||
// Pangea#
|
||||
onTap: () {
|
||||
widget.onPlay != null
|
||||
? widget.onPlay!.call()
|
||||
: _onButtonTap();
|
||||
},
|
||||
onTap: _onButtonTap,
|
||||
child: Material(
|
||||
color: widget.color.withAlpha(64),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
|
|
|
|||
|
|
@ -225,14 +225,6 @@ class MessageContent extends StatelessWidget {
|
|||
eventId: event.eventId,
|
||||
roomId: event.room.id,
|
||||
senderId: event.senderId,
|
||||
onPlay: overlayController == null
|
||||
? () {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!.event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
autoplay: overlayController != null,
|
||||
// Pangea#
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -544,7 +544,7 @@ class RoomDetailsButtonRowState extends State<RoomDetailsButtonRow> {
|
|||
);
|
||||
}
|
||||
|
||||
final button = buttons[index];
|
||||
final button = mainViewButtons[index];
|
||||
return Expanded(
|
||||
child: RoomDetailsButton(
|
||||
mini: mini,
|
||||
|
|
@ -729,66 +729,70 @@ class RoomParticipantsSection extends StatelessWidget {
|
|||
padding: EdgeInsets.all(_padding),
|
||||
child: SizedBox(
|
||||
width: _width,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (gradient != null)
|
||||
CircleAvatar(
|
||||
radius: _width / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: gradient,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _width,
|
||||
width: _width,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => showMemberActionsPopupMenu(
|
||||
context: context,
|
||||
user: user,
|
||||
child: Opacity(
|
||||
opacity: user.membership == Membership.join ? 1.0 : 0.5,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (gradient != null)
|
||||
CircleAvatar(
|
||||
radius: _width / 2,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: gradient,
|
||||
),
|
||||
child: Center(
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: _width - 6.0,
|
||||
presenceUserId: user.id,
|
||||
showPresence: false,
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _width,
|
||||
width: _width,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () => showMemberActionsPopupMenu(
|
||||
context: context,
|
||||
user: user,
|
||||
),
|
||||
child: Center(
|
||||
child: Avatar(
|
||||
mxContent: user.avatarUrl,
|
||||
name: user.calcDisplayname(),
|
||||
size: _width - 6.0,
|
||||
presenceUserId: user.id,
|
||||
showPresence: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
user.calcDisplayname(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
},
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
LevelDisplayName(
|
||||
userId: user.id,
|
||||
textStyle: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text(
|
||||
user.calcDisplayname(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
LevelDisplayName(
|
||||
userId: user.id,
|
||||
textStyle: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -256,14 +256,7 @@ class IgcController {
|
|||
timeline: choreographer.chatController.timeline!,
|
||||
ownMessage: event.senderId ==
|
||||
choreographer.pangeaController.matrixState.client.userID,
|
||||
)
|
||||
.getSpeechToTextLocal(
|
||||
choreographer.l1LangCode,
|
||||
choreographer.l2LangCode,
|
||||
)
|
||||
?.transcript
|
||||
.text
|
||||
.trim(); // trim whitespace
|
||||
).getSpeechToTextLocal()?.transcript.text.trim(); // trim whitespace
|
||||
if (content == null) continue;
|
||||
messages.add(
|
||||
PreviousMessage(
|
||||
|
|
|
|||
|
|
@ -232,13 +232,7 @@ class PangeaMessageEvent {
|
|||
null;
|
||||
}).toSet();
|
||||
|
||||
SpeechToTextModel? getSpeechToTextLocal(
|
||||
String? l1Code,
|
||||
String? l2Code,
|
||||
) {
|
||||
if (l1Code == null || l2Code == null) {
|
||||
return null;
|
||||
}
|
||||
SpeechToTextModel? getSpeechToTextLocal() {
|
||||
return representations
|
||||
.firstWhereOrNull(
|
||||
(element) => element.content.speechToText != null,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class LemmaReactionPickerState extends State<LemmaReactionPicker> {
|
|||
} catch (e, s) {
|
||||
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
if (mounted) setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,85 @@
|
|||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
|
||||
class TokenPositionModel {
|
||||
/// Start index of the full substring in the message
|
||||
final int start;
|
||||
|
||||
/// End index of the full substring in the message
|
||||
final int end;
|
||||
|
||||
/// Start index of the token in the message
|
||||
final int tokenStart;
|
||||
|
||||
/// End index of the token in the message
|
||||
final int tokenEnd;
|
||||
|
||||
final bool selected;
|
||||
final bool hideContent;
|
||||
class TokenPosition {
|
||||
final PangeaToken? token;
|
||||
final int startIndex;
|
||||
final int endIndex;
|
||||
|
||||
const TokenPositionModel({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.tokenStart,
|
||||
required this.tokenEnd,
|
||||
required this.hideContent,
|
||||
required this.selected,
|
||||
const TokenPosition({
|
||||
this.token,
|
||||
required this.startIndex,
|
||||
required this.endIndex,
|
||||
});
|
||||
}
|
||||
|
||||
class TokensUtil {
|
||||
static List<TokenPosition> getTokenPositions(
|
||||
List<PangeaToken> tokens,
|
||||
) {
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int tokenPointer = 0;
|
||||
int globalPointer = 0;
|
||||
|
||||
while (tokenPointer < tokens.length) {
|
||||
int endIndex = tokenPointer;
|
||||
PangeaToken token = tokens[tokenPointer];
|
||||
|
||||
if (token.text.offset > globalPointer) {
|
||||
// If the token starts after the current global pointer, we need to
|
||||
// create a new token position for the gap
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
startIndex: globalPointer,
|
||||
endIndex: token.text.offset,
|
||||
),
|
||||
);
|
||||
|
||||
globalPointer = token.text.offset;
|
||||
}
|
||||
|
||||
// move the end index if the next token is right next to the current token
|
||||
// and either the current token is punctuation or the next token is punctuation
|
||||
while (endIndex < tokens.length - 1) {
|
||||
final PangeaToken currentToken = tokens[endIndex];
|
||||
final PangeaToken nextToken = tokens[endIndex + 1];
|
||||
|
||||
final currentIsPunct = currentToken.pos == 'PUNCT' &&
|
||||
currentToken.text.content.trim().isNotEmpty;
|
||||
final nextIsPunct = nextToken.pos == 'PUNCT' &&
|
||||
nextToken.text.content.trim().isNotEmpty;
|
||||
|
||||
if (currentToken.text.offset + currentToken.text.length !=
|
||||
nextToken.text.offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((currentIsPunct && nextIsPunct) ||
|
||||
(currentIsPunct && nextToken.text.content.trim().isNotEmpty) ||
|
||||
(nextIsPunct && currentToken.text.content.trim().isNotEmpty)) {
|
||||
if (token.pos == 'PUNCT' && !nextIsPunct) {
|
||||
token = nextToken;
|
||||
}
|
||||
|
||||
endIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
token: token,
|
||||
startIndex: tokens[tokenPointer].text.offset,
|
||||
endIndex: tokens[endIndex].text.offset + tokens[endIndex].text.length,
|
||||
),
|
||||
);
|
||||
|
||||
// Move to the next token
|
||||
tokenPointer = tokenPointer + (endIndex - tokenPointer) + 1;
|
||||
globalPointer =
|
||||
tokens[endIndex].text.offset + tokens[endIndex].text.length;
|
||||
}
|
||||
|
||||
return tokenPositions;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,14 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (a.membership != Membership.join && b.membership != Membership.join) {
|
||||
return a.displayName?.compareTo(b.displayName ?? '') ?? 0;
|
||||
} else if (a.membership != Membership.join) {
|
||||
return 1;
|
||||
} else if (b.membership != Membership.join) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final PublicProfileModel? aProfile = _levelsCache[a.id];
|
||||
final PublicProfileModel? bProfile = _levelsCache[b.id];
|
||||
|
||||
|
|
@ -100,7 +108,7 @@ class LoadParticipantsUtilState extends State<LoadParticipantsUtil> {
|
|||
|
||||
Future<void> _cacheLevels() async {
|
||||
for (final user in participants) {
|
||||
if (_levelsCache[user.id] == null) {
|
||||
if (_levelsCache[user.id] == null && user.membership == Membership.join) {
|
||||
_levelsCache[user.id] = await MatrixState
|
||||
.pangeaController.userController
|
||||
.getPublicProfile(user.id);
|
||||
|
|
|
|||
|
|
@ -461,9 +461,18 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
pangeaMessageEvent?.messageDisplayLangCode.split("-")[0] ==
|
||||
MatrixState.pangeaController.languageController.userL2?.langCodeShort;
|
||||
|
||||
PangeaToken? get selectedToken =>
|
||||
pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.firstWhereOrNull(isTokenSelected);
|
||||
PangeaToken? get selectedToken {
|
||||
if (pangeaMessageEvent?.isAudioMessage == true) {
|
||||
final stt = pangeaMessageEvent!.getSpeechToTextLocal();
|
||||
if (stt == null || stt.transcript.sttTokens.isEmpty) return null;
|
||||
return stt.transcript.sttTokens
|
||||
.firstWhereOrNull((t) => isTokenSelected(t.token))
|
||||
?.token;
|
||||
}
|
||||
|
||||
return pangeaMessageEvent?.messageDisplayRepresentation?.tokens
|
||||
?.firstWhereOrNull(isTokenSelected);
|
||||
}
|
||||
|
||||
/// Whether the overlay is currently displaying a selection
|
||||
bool get isSelection => _selectedSpan != null || _highlightedTokens != null;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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/pangea/toolbar/widgets/stt_transcript_tokens.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/file_description.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -178,15 +179,17 @@ class OverlayMessage extends StatelessWidget {
|
|||
spacing: 8.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
overlayController
|
||||
.transcription!.transcript.text,
|
||||
SttTranscriptTokens(
|
||||
model: overlayController.transcription!,
|
||||
style: AppConfig.messageTextStyle(
|
||||
event,
|
||||
textColor,
|
||||
).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
onClick: overlayController
|
||||
.onClickOverlayMessageToken,
|
||||
isSelected: overlayController.isTokenSelected,
|
||||
),
|
||||
if (MatrixState.pangeaController
|
||||
.languageController.showTrancription)
|
||||
|
|
|
|||
91
lib/pangea/toolbar/widgets/stt_transcript_tokens.dart
Normal file
91
lib/pangea/toolbar/widgets/stt_transcript_tokens.dart
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/message_token_text/token_position_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SttTranscriptTokens extends StatelessWidget {
|
||||
final SpeechToTextModel model;
|
||||
final TextStyle? style;
|
||||
|
||||
final void Function(PangeaToken)? onClick;
|
||||
final bool Function(PangeaToken)? isSelected;
|
||||
|
||||
const SttTranscriptTokens({
|
||||
super.key,
|
||||
required this.model,
|
||||
this.onClick,
|
||||
this.isSelected,
|
||||
this.style,
|
||||
});
|
||||
|
||||
List<PangeaToken> get tokens =>
|
||||
model.transcript.sttTokens.map((t) => t.token).toList();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.transcript.sttTokens.isEmpty) {
|
||||
return Text(
|
||||
model.transcript.text,
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
textScaler: TextScaler.noScaling,
|
||||
);
|
||||
}
|
||||
|
||||
final messageCharacters = model.transcript.text.characters;
|
||||
return RichText(
|
||||
textScaler: TextScaler.noScaling,
|
||||
text: TextSpan(
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
children: TokensUtil.getTokenPositions(tokens).map((tokenPosition) {
|
||||
final text = messageCharacters
|
||||
.skip(tokenPosition.startIndex)
|
||||
.take(tokenPosition.endIndex - tokenPosition.startIndex)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token == null) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: style ?? DefaultTextStyle.of(context).style,
|
||||
);
|
||||
}
|
||||
|
||||
final token = tokenPosition.token!;
|
||||
final selected = isSelected?.call(token) ?? false;
|
||||
|
||||
return WidgetSpan(
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.link,
|
||||
child: MouseRegion(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey(token.text.uniqueKey)
|
||||
.key,
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onClick != null ? () => onClick?.call(token) : null,
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: (style ?? DefaultTextStyle.of(context).style)
|
||||
.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationThickness: 4,
|
||||
decorationColor: selected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.white.withAlpha(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue