Merge pull request #3210 from pangeachat/3163-expand-the-box-for-long-message

chore: allow audio transcript to exapnd outside of original message b…
This commit is contained in:
ggurdin 2025-06-25 10:37:25 -04:00 committed by GitHub
commit 8cced95abe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 137 additions and 78 deletions

View file

@ -44,7 +44,6 @@ abstract class AppConfig {
toolbarButtonsHeight +
(chatInputRowOverlayPadding * 2) +
toolbarSpacing;
static const double audioTranscriptionMaxHeight = 150.0;
static TextStyle messageTextStyle(
Event? event,

View file

@ -22,6 +22,8 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
final bool enabled;
final VoidCallback? onTranscriptionFetched;
const PhoneticTranscriptionWidget({
super.key,
required this.text,
@ -30,6 +32,7 @@ class PhoneticTranscriptionWidget extends StatefulWidget {
this.iconSize,
this.iconColor,
this.enabled = true,
this.onTranscriptionFetched,
});
@override
@ -103,7 +106,12 @@ class _PhoneticTranscriptionWidgetState
},
);
} finally {
if (mounted) setState(() => _isLoading = false);
if (mounted) {
setState(() {
_isLoading = false;
widget.onTranscriptionFetched?.call();
});
}
}
}

View file

@ -101,6 +101,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
bool showSpeechTranslation = false;
String? speechTranslation;
final StreamController contentChangedStream = StreamController.broadcast();
double maxWidth = AppConfig.toolbarMinWidth;
/////////////////////////////////////
@ -121,6 +123,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
WidgetsBinding.instance.addPostFrameCallback(
(_) => widget.chatController.clearSelectedEvents(),
);
contentChangedStream.close();
super.dispose();
}
@ -587,7 +590,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
void setTranslation(String value) {
if (mounted) {
setState(() => translation = value);
setState(() {
translation = value;
contentChangedStream.add(true);
});
}
}
@ -598,12 +604,18 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
if (showTranslation == show) return;
setState(() => showTranslation = show);
setState(() {
showTranslation = show;
contentChangedStream.add(true);
});
}
void setSpeechTranslation(String value) {
if (mounted) {
setState(() => speechTranslation = value);
setState(() {
speechTranslation = value;
contentChangedStream.add(true);
});
}
}
@ -614,7 +626,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
if (showSpeechTranslation == show) return;
setState(() => showSpeechTranslation = show);
setState(() {
showSpeechTranslation = show;
contentChangedStream.add(true);
});
}
void setTranscription(SpeechToTextModel value) {
@ -622,13 +637,17 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {
transcriptionError = null;
transcription = value;
contentChangedStream.add(true);
});
}
}
void setTranscriptionError(String value) {
if (mounted) {
setState(() => transcriptionError = value);
setState(() {
transcriptionError = value;
contentChangedStream.add(true);
});
}
}

View file

@ -71,6 +71,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
Offset? _currentOffset;
StreamSubscription? _reactionSubscription;
StreamSubscription? _contentChangedSubscription;
final _animationDuration = const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
@ -106,6 +107,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
},
).listen((_) => setState(() {}));
_contentChangedSubscription = widget
.overlayController.contentChangedStream.stream
.listen(_onContentSizeChanged);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _centeredMessageCompleter.future;
if (!mounted) return;
@ -138,6 +143,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
void dispose() {
_animationController.dispose();
_reactionSubscription?.cancel();
_contentChangedSubscription?.cancel();
MatrixState.pangeaController.matrixState.audioPlayer
?..stop()
..dispose();
@ -196,34 +202,9 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
if (mode == ReadingAssistanceMode.selectMode) {
_overlayOffsetAnimation = Tween<Offset>(
begin: _currentOffset,
end: _adjustedOriginalMessageOffset,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
)..addListener(() {
if (mounted) {
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
}
});
_resetOffsetAnimation(_adjustedOriginalMessageOffset);
} else if (mode == ReadingAssistanceMode.practiceMode) {
_overlayOffsetAnimation = Tween<Offset>(
begin: _currentOffset,
end: _centeredMessageOffset!,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
)..addListener(() {
if (mounted) {
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
}
});
_resetOffsetAnimation(_centeredMessageOffset!);
_messageSizeAnimation = Tween<Size>(
begin: Size(
_originalMessageSize.width,
@ -244,6 +225,40 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
}
void _onContentSizeChanged(_) {
Future.delayed(FluffyThemes.animationDuration, () {
final offset = _overlayMessageRenderBox?.localToGlobal(Offset.zero);
if (offset == null || !_overlayMessageRenderBox!.hasSize) {
return null;
}
final newOffset = _adjustedMessageOffset(
_overlayMessageRenderBox!.size,
offset,
);
if (newOffset == _currentOffset) return;
_resetOffsetAnimation(newOffset);
_animationController.forward(from: 0);
});
}
void _resetOffsetAnimation(Offset offset) {
_overlayOffsetAnimation = Tween<Offset>(
begin: _currentOffset,
end: offset,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
)..addListener(() {
if (mounted) {
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
}
});
}
T _runWithLogging<T>(
Function runner,
String errorMessage,
@ -326,6 +341,14 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
null,
);
RenderBox? get _overlayMessageRenderBox => _runWithLogging<RenderBox?>(
() => MatrixState.pAnyState.getRenderBox(
'overlay_message_${widget.event.eventId}',
),
"Error getting overlay message render box",
null,
);
Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100);
/// The size of the message in the chat list (as opposed to the expanded size in the center overlay)
@ -394,17 +417,28 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
Offset get _adjustedOriginalMessageOffset {
return _adjustedMessageOffset(
_originalMessageSize,
_originalMessageOffset,
);
}
Offset _adjustedMessageOffset(
Size messageSize,
Offset messageOffset,
) {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageOffset;
}
final topOffset = _originalMessageOffset.dy;
final bottomOffset = _originalMessageBottomOffset -
_reactionsHeight -
_selectionButtonsHeight;
final topOffset = messageOffset.dy;
final bottomOffset =
(_mediaQuery!.size.height - topOffset - messageSize.height) -
_reactionsHeight -
_selectionButtonsHeight;
final hasHeaderOverflow = topOffset <
(_headerHeight + AppConfig.toolbarSpacing + _audioTranscriptionHeight);
final hasHeaderOverflow =
topOffset < (_headerHeight + AppConfig.toolbarSpacing);
final hasFooterOverflow =
bottomOffset < (_footerHeight + AppConfig.toolbarSpacing);
@ -416,15 +450,12 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
if (hasHeaderOverflow) {
final difference = topOffset -
(_headerHeight +
AppConfig.toolbarSpacing +
_audioTranscriptionHeight);
final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
double newBottomOffset = _mediaQuery!.size.height -
_originalMessageOffset.dy +
topOffset +
difference -
_originalMessageSize.height -
messageSize.height -
_selectionButtonsHeight;
if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) {
@ -524,12 +555,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return showSelectionButtons ? AppConfig.toolbarButtonsHeight : 0;
}
double get _audioTranscriptionHeight {
return widget.pangeaMessageEvent?.isAudioMessage ?? false
? AppConfig.audioTranscriptionMaxHeight
: 0;
}
bool get _hasReactions {
final reactionsEvents = widget.event.aggregatedEvents(
widget.chatController.timeline!,

View file

@ -9,6 +9,7 @@ import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dar
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
import 'package:fluffychat/widgets/matrix.dart';
class OverlayCenterContent extends StatelessWidget {
final Event event;
@ -69,6 +70,9 @@ class OverlayCenterContent extends StatelessWidget {
MeasureRenderBox(
onChange: onChangeMessageSize,
child: OverlayMessage(
key: MatrixState.pAnyState
.layerLinkAndKey('overlay_message_${event.eventId}')
.key,
event,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: chatController.choreographer.immersionMode,

View file

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
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/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message_content.dart';
@ -149,9 +150,8 @@ class OverlayMessage extends StatelessWidget {
final transcription = showTranscription
? Container(
width: messageWidth,
constraints: const BoxConstraints(
maxHeight: AppConfig.audioTranscriptionMaxHeight,
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Padding(
padding: const EdgeInsets.all(12.0),
@ -178,6 +178,7 @@ class OverlayMessage extends StatelessWidget {
child: Column(
spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SttTranscriptTokens(
model: overlayController.transcription!,
@ -208,6 +209,9 @@ class OverlayMessage extends StatelessWidget {
iconColor: textColor,
enabled:
event.senderId != BotName.byEnvironment,
onTranscriptionFetched: () =>
overlayController.contentChangedStream
.add(true),
),
],
),
@ -226,9 +230,8 @@ class OverlayMessage extends StatelessWidget {
final translation = showTranslation || showSpeechTranslation
? Container(
width: messageWidth,
constraints: const BoxConstraints(
maxHeight: AppConfig.audioTranscriptionMaxHeight,
maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(
@ -271,8 +274,6 @@ class OverlayMessage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (readingAssistanceMode == ReadingAssistanceMode.transitionMode)
transcription,
if (event.relationshipType == RelationshipTypes.reply)
FutureBuilder<Event?>(
future: event.getReplyEvent(
@ -371,8 +372,6 @@ class OverlayMessage extends StatelessWidget {
],
),
),
if (readingAssistanceMode == ReadingAssistanceMode.transitionMode)
translation,
],
),
),
@ -386,26 +385,31 @@ class OverlayMessage extends StatelessWidget {
color: noBubble ? Colors.transparent : color,
borderRadius: borderRadius,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (readingAssistanceMode != ReadingAssistanceMode.transitionMode)
constraints: BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
maxHeight: maxHeight,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
transcription,
sizeAnimation != null
? AnimatedBuilder(
animation: sizeAnimation!,
builder: (context, child) {
return SizedBox(
height: sizeAnimation!.value.height,
width: sizeAnimation!.value.width,
child: content,
);
},
)
: content,
if (readingAssistanceMode != ReadingAssistanceMode.transitionMode)
sizeAnimation != null
? AnimatedBuilder(
animation: sizeAnimation!,
builder: (context, child) {
return SizedBox(
height: sizeAnimation!.value.height,
width: sizeAnimation!.value.width,
child: content,
);
},
)
: content,
translation,
],
],
),
),
),
);