Message overlay updates (#3522)

* added slide animation to overlay

* re-enable practice mode

* chore: position overlay over original message

* chore: fix spacing on mobile

* chore: remove unreferenced files
This commit is contained in:
ggurdin 2025-07-22 11:49:35 -04:00 committed by GitHub
parent b06d368058
commit 641a18a1fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 600 additions and 1101 deletions

View file

@ -11,7 +11,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
@ -21,7 +20,6 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart
import 'package:fluffychat/utils/event_checkbox_extension.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
import '../../../utils/url_launcher.dart';
@ -412,68 +410,57 @@ class HtmlMessage extends StatelessWidget {
alignment: readingAssistanceMode == ReadingAssistanceMode.practiceMode
? PlaceholderAlignment.bottom
: PlaceholderAlignment.middle,
child: CompositedTransformTarget(
link: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.link
: LayerLinkAndKey(token.hashCode.toString()).link,
child: Column(
key: token != null && renderer.assignTokenKey
? MatrixState.pAnyState
.layerLinkAndKey(token.text.uniqueKey)
.key
: null,
children: [
if (renderer.showCenterStyling && token != null)
MessageTokenButton(
token: token,
overlayController: overlayController,
textStyle: renderer.style(
child: Column(
children: [
if (renderer.showCenterStyling && token != null)
MessageTokenButton(
token: token,
overlayController: overlayController,
textStyle: renderer.style(
context,
color: renderer.backgroundColor(
context,
color: renderer.backgroundColor(
context,
selected,
highlighted,
isNew,
),
selected,
highlighted,
isNew,
),
width: tokenWidth,
animateIn: isTransitionAnimation,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onClick != null && token != null
? () => onClick?.call(token)
: null,
child: RichText(
textDirection: pangeaMessageEvent?.textDirection,
text: TextSpan(
children: [
LinkifySpan(
text: node.text,
style: renderer.style(
width: tokenWidth,
animateIn: isTransitionAnimation,
),
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onClick != null && token != null
? () => onClick?.call(token)
: null,
child: RichText(
textDirection: pangeaMessageEvent?.textDirection,
text: TextSpan(
children: [
LinkifySpan(
text: node.text,
style: renderer.style(
context,
color: renderer.backgroundColor(
context,
color: renderer.backgroundColor(
context,
selected,
highlighted,
isNew,
),
selected,
highlighted,
isNew,
),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
],
),
linkStyle: linkStyle,
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
],
),
),
),
],
),
),
],
// ),
),
);
// Pangea#

View file

@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.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/practice_mode_buttons.dart';
class OverlayFooter extends StatelessWidget {
final ChatController controller;
final MessageOverlayController overlayController;
final bool showToolbarButtons;
final ReadingAssistanceMode? readingAssistanceMode;
const OverlayFooter({
required this.controller,
required this.overlayController,
required this.showToolbarButtons,
required this.readingAssistanceMode,
super.key,
});
@override
Widget build(BuildContext context) {
//@ggurdin can we change this mobile padding to 0? seems a some extrea space on mobile
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
return Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,
left: bottomSheetPadding,
right: bottomSheetPadding,
),
height: readingAssistanceMode == ReadingAssistanceMode.practiceMode ||
readingAssistanceMode == ReadingAssistanceMode.transitionMode
? AppConfig.practiceModeInputBarHeight
: AppConfig.selectModeInputBarHeight,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showToolbarButtons)
PracticeModeButtons(overlayController: overlayController),
Material(
clipBehavior: Clip.hardEdge,
color: Colors.transparent,
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
child: PangeaChatInputRow(
controller: controller,
),
),
],
),
);
}
}

View file

@ -4,11 +4,11 @@ import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_buttons.dart';
const double minContentHeight = 120;
@ -23,12 +23,6 @@ class ReadingAssistanceInputBar extends StatelessWidget {
});
Widget barContent(BuildContext context) {
if (overlayController.readingAssistanceMode !=
ReadingAssistanceMode.practiceMode) {
return const SizedBox();
// return ReactionsPicker(controller);
}
Widget? content;
final target =
overlayController.toolbarMode.associatedActivityType != null &&
@ -57,6 +51,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
.textTheme
.bodyLarge
?.copyWith(fontStyle: FontStyle.italic),
textAlign: TextAlign.center,
);
case MessageMode.messageTranslation:
@ -78,7 +73,10 @@ class ReadingAssistanceInputBar extends StatelessWidget {
overlayController: overlayController,
);
} else {
content = Text(L10n.of(context).allDone);
content = Text(
L10n.of(context).allDone,
textAlign: TextAlign.center,
);
}
case MessageMode.wordMorph:
if (target != null) {
@ -92,37 +90,43 @@ class ReadingAssistanceInputBar extends StatelessWidget {
child: Text(
L10n.of(context).selectForGrammar,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
);
}
}
}
return Container(
constraints: const BoxConstraints(
minHeight: minContentHeight,
),
child: Center(child: content),
);
return content;
}
@override
Widget build(BuildContext context) {
return Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: AppConfig.readingAssistanceInputBarHeight,
maxWidth: overlayController.maxWidth,
return Column(
children: [
PracticeModeButtons(
overlayController: overlayController,
),
child: AnimatedSize(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
child: SingleChildScrollView(
child: barContent(context),
Material(
child: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
constraints: BoxConstraints(
minHeight: minContentHeight,
maxHeight: AppConfig.readingAssistanceInputBarHeight,
maxWidth: overlayController.maxWidth,
),
child: AnimatedSize(
duration: const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
),
child: SingleChildScrollView(
child: barContent(context),
),
),
),
),
),
],
);
}
}

View file

@ -25,7 +25,7 @@ class TokenRenderingUtil {
bool get showCenterStyling {
if (overlayController == null) return false;
if (!isTransitionAnimation) return true;
return readingAssistanceMode == ReadingAssistanceMode.transitionMode;
return readingAssistanceMode == ReadingAssistanceMode.practiceMode;
}
double? fontSize(BuildContext context) => showCenterStyling

View file

@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
class IconNumberWidget extends StatelessWidget {
final IconData icon;
final String number;
final Color? iconColor;
final double? iconSize;
final String? toolTip;
final VoidCallback? onPressed;
const IconNumberWidget({
super.key,
required this.icon,
required this.number,
this.toolTip,
this.iconColor,
this.iconSize,
this.onPressed,
});
Widget _content(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(
icon,
color: iconColor ?? Theme.of(context).iconTheme.color,
size: iconSize ?? Theme.of(context).iconTheme.size,
),
onPressed: onPressed,
),
const SizedBox(width: 5),
Text(
number.toString(),
style: TextStyle(
fontSize:
iconSize ?? Theme.of(context).textTheme.bodyMedium?.fontSize,
color: iconColor ?? Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return toolTip != null
? Tooltip(message: toolTip!, child: _content(context))
: _content(context);
}
}

View file

@ -14,10 +14,12 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/widgets/over_message_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_transition_animation.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_card_switcher.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -51,57 +53,32 @@ class MessageSelectionPositioner extends StatefulWidget {
class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
with TickerProviderStateMixin {
// late AnimationController _animationController;
// Offset? _centeredMessageOffset;
// Size? _centeredMessageSize;
// Size? _tooltipSize;
// final Completer _centeredMessageCompleter = Completer();
// final Completer _tooltipCompleter = Completer();
// MessageMode _currentMode = MessageMode.noneSelected;
// Animation<Offset>? _overlayOffsetAnimation;
// Animation<Size>? _messageSizeAnimation;
// Offset? _currentOffset;
StreamSubscription? _reactionSubscription;
StreamSubscription? _contentChangedSubscription;
ScrollController? _scrollController;
ScrollController? scrollController;
// final _animationDuration = const Duration(
// milliseconds: AppConfig.overlayAnimationDuration,
// // seconds: 5,
// );
bool finishedTransition = false;
bool startedTransition = false;
ReadingAssistanceMode readingAssistanceMode =
ReadingAssistanceMode.selectMode;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
Future.delayed(
const Duration(milliseconds: 100),
() {
if (_scrollController == null || !_scrollController!.hasClients) {
return;
}
// _scrollController!.animateTo(
// _scrollController!.position.maxScrollExtent,
// duration: FluffyThemes.animationDuration,
// curve: FluffyThemes.animationCurve,
// );
scrollController = ScrollController(
onAttach: (position) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
scrollController?.jumpTo(
scrollController!.position.maxScrollExtent,
);
}
});
},
);
// // _currentMode = widget.overlayController.toolbarMode;
// _animationController = AnimationController(
// vsync: this,
// duration: _animationDuration,
// );
_reactionSubscription =
widget.chatController.room.client.onSync.stream.where(
(update) {
@ -125,360 +102,19 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
_contentChangedSubscription = widget
.overlayController.contentChangedStream.stream
.listen(_onContentSizeChanged);
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// await _centeredMessageCompleter.future;
// if (!mounted) return;
// setState(() {
// _currentOffset = Offset(
// _ownMessage ? _messageRightOffset : _messageLeftOffset,
// _originalMessageBottomOffset -
// _reactionsHeight -
// _selectionButtonsHeight,
// );
// });
// _setReadingAssistanceMode(
// ReadingAssistanceMode.selectMode,
// );
// });
}
// @override
// void didUpdateWidget(MessageSelectionPositioner oldWidget) {
// super.didUpdateWidget(oldWidget);
// final mode = widget.overlayController.toolbarMode;
// if (mode != _currentMode) {
// setState(() => _currentMode = mode);
// }
// }
@override
void dispose() {
// _animationController.dispose();
_reactionSubscription?.cancel();
_contentChangedSubscription?.cancel();
_scrollController?.dispose();
scrollController?.dispose();
MatrixState.pangeaController.matrixState.audioPlayer
?..stop()
..dispose();
super.dispose();
}
// void _setCenteredMessageSize(RenderBox renderBox) {
// if (_centeredMessageCompleter.isCompleted) return;
// _centeredMessageSize = renderBox.size;
// final offset = renderBox.localToGlobal(Offset.zero);
// _centeredMessageOffset = Offset(
// offset.dx - _columnWidth - _horizontalPadding - 2.0,
// _mediaQuery!.size.height -
// (offset.dy -
// ((AppConfig.practiceModeInputBarHeight -
// AppConfig.selectModeInputBarHeight) *
// 0.75)) -
// renderBox.size.height -
// _reactionsHeight,
// );
// setState(() {});
// if (!_centeredMessageCompleter.isCompleted) {
// _centeredMessageCompleter.complete();
// }
// }
// void _setTooltipSize(RenderBox renderBox) {
// setState(() {
// _tooltipSize = renderBox.size;
// });
// if (!_tooltipCompleter.isCompleted) {
// _tooltipCompleter.complete();
// }
// }
// Future<void> _setReadingAssistanceMode(ReadingAssistanceMode mode) async {
// if (mode == _readingAssistanceMode) {
// return;
// }
// await _centeredMessageCompleter.future;
// if (mode == ReadingAssistanceMode.practiceMode) {
// setState(
// () => widget.overlayController.readingAssistanceMode =
// ReadingAssistanceMode.transitionMode,
// );
// } else if (mode == ReadingAssistanceMode.selectMode) {
// setState(
// () => widget.overlayController.readingAssistanceMode =
// ReadingAssistanceMode.selectMode,
// );
// }
// if (mode == ReadingAssistanceMode.selectMode) {
// _resetOffsetAnimation(_adjustedOriginalMessageOffset);
// } else if (mode == ReadingAssistanceMode.practiceMode) {
// _resetOffsetAnimation(_centeredMessageOffset!);
// _messageSizeAnimation = Tween<Size>(
// begin: Size(
// _originalMessageSize.width,
// _originalMessageSize.height,
// ),
// end: _adjustedCenteredMessageSize,
// ).animate(
// CurvedAnimation(
// parent: _animationController,
// curve: FluffyThemes.animationCurve,
// ),
// );
// }
// await _animationController.forward(from: 0);
// if (mounted) {
// setState(() => widget.overlayController.readingAssistanceMode = mode);
// }
// }
void _onContentSizeChanged(_) {
Future.delayed(FluffyThemes.animationDuration, () {
setState(() {});
// 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);
// }
// });
// }
// double get _inputBarSize =>
// _readingAssistanceMode == ReadingAssistanceMode.practiceMode ||
// _readingAssistanceMode == ReadingAssistanceMode.transitionMode
// ? AppConfig.practiceModeInputBarHeight
// : AppConfig.selectModeInputBarHeight;
// /// Available vertical space not taken up by the header and footer
// double? get _verticalSpace {
// if (_mediaQuery == null) return null;
// return _mediaQuery!.size.height - _headerHeight - _footerHeight;
// }
// original message size and offset
// Offset? get _overlayMessageOffset =>
// _overlayMessageRenderBox?.localToGlobal(Offset.zero);
// double? get _buttonsTopOffset {
// if (_overlayMessageOffset == null ||
// _overlayMessageSize == null ||
// _mediaQuery == null) {
// return null;
// }
// const buttonsHeight = 300.0;
// final availableSpace = _mediaQuery!.size.height -
// _overlayMessageOffset!.dy -
// _overlayMessageSize!.height -
// _reactionsHeight -
// 4.0;
// if (availableSpace >= buttonsHeight) {
// return _overlayMessageOffset!.dy + _overlayMessageSize!.height + 4.0;
// }
// return _mediaQuery!.size.height - buttonsHeight - 4.0;
// }
// Centered message size and offset
// bool get _centeredMessageHasOverflow {
// if (_verticalSpace == null ||
// _centeredMessageSize == null ||
// _centeredMessageOffset == null) {
// return false;
// }
// final finalMessageHeight = _centeredMessageSize!.height + _reactionsHeight;
// return finalMessageHeight > _verticalSpace!;
// }
// /// Size of the centered overlay message adjusted for overflow
// Size? get _adjustedCenteredMessageSize {
// if (_centeredMessageHasOverflow) {
// return Size(
// _centeredMessageSize!.width,
// _verticalSpace! - (AppConfig.toolbarSpacing * 2),
// );
// }
// return _centeredMessageSize;
// }
// Offset? get _adjustedCenteredMessageOffset {
// if (_centeredMessageHasOverflow) {
// return Offset(
// _centeredMessageOffset!.dx,
// _footerHeight + AppConfig.toolbarSpacing,
// );
// }
// return _centeredMessageOffset;
// }
// message offset
// Offset get _adjustedOriginalMessageOffset {
// return _adjustedMessageOffset(
// _originalMessageSize,
// _originalMessageOffset,
// );
// }
// Offset _adjustedMessageOffset(
// Size messageSize,
// Offset messageOffset,
// ) {
// if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
// return _defaultMessageOffset;
// }
// final topOffset = messageOffset.dy;
// final bottomOffset =
// (_mediaQuery!.size.height - topOffset - messageSize.height) -
// _reactionsHeight -
// _selectionButtonsHeight;
// final hasHeaderOverflow =
// topOffset < (_headerHeight + AppConfig.toolbarSpacing);
// final hasFooterOverflow =
// bottomOffset < (_footerHeight + AppConfig.toolbarSpacing);
// if (!hasHeaderOverflow && !hasFooterOverflow) {
// return Offset(
// _ownMessage ? _messageRightOffset : _messageLeftOffset,
// bottomOffset,
// );
// }
// if (hasHeaderOverflow) {
// final difference = topOffset - (_headerHeight + AppConfig.toolbarSpacing);
// double newBottomOffset = _mediaQuery!.size.height -
// topOffset +
// difference -
// messageSize.height -
// _selectionButtonsHeight;
// if (newBottomOffset < _footerHeight + AppConfig.toolbarSpacing) {
// newBottomOffset = _footerHeight + AppConfig.toolbarSpacing;
// }
// return Offset(
// _ownMessage ? _messageRightOffset : _messageLeftOffset,
// newBottomOffset,
// );
// } else {
// return Offset(
// _ownMessage ? _messageRightOffset : _messageLeftOffset,
// _footerHeight + (AppConfig.toolbarSpacing * 2),
// );
// }
// }
// double get _originalMessageBottomOffset =>
// _mediaQuery!.size.height -
// _originalMessageOffset.dy -
// _originalMessageSize.height;
// double? get _centeredMessageTopOffset {
// if (_mediaQuery == null ||
// _adjustedCenteredMessageOffset == null ||
// _adjustedCenteredMessageSize == null) {
// return null;
// }
// return _mediaQuery!.size.height -
// _adjustedCenteredMessageOffset!.dy -
// _adjustedCenteredMessageSize!.height -
// _reactionsHeight;
// }
// double get _headerHeight {
// return (Theme.of(context).appBarTheme.toolbarHeight ??
// AppConfig.defaultHeaderHeight) +
// (_mediaQuery?.padding.top ?? 0);
// }
// double get _footerHeight {
// return _inputBarSize + (_mediaQuery?.padding.bottom ?? 0);
// }
// measurement for items in the toolbar
// bool get _showButtons {
// if (!(widget.pangeaMessageEvent?.shouldShowToolbar ?? false)) {
// return false;
// }
// final type = widget.pangeaMessageEvent?.event.messageType;
// if (![MessageTypes.Text, MessageTypes.Audio].contains(type)) {
// return false;
// }
// if (type == MessageTypes.Text) {
// return widget.pangeaMessageEvent?.messageDisplayLangIsL2 ?? false;
// }
// return true;
// }
// bool get showPracticeButtons =>
// _showButtons &&
// widget.overlayController.readingAssistanceMode ==
// ReadingAssistanceMode.practiceMode;
// bool get showSelectionButtons =>
// _showButtons &&
// [ReadingAssistanceMode.selectMode, null]
// .contains(widget.overlayController.readingAssistanceMode);
// double get _selectionButtonsHeight {
// return showSelectionButtons ? AppConfig.toolbarButtonsHeight : 0;
// }
// double get _readingAssistanceModeOpacity {
// switch (_readingAssistanceMode) {
// case ReadingAssistanceMode.practiceMode:
// case ReadingAssistanceMode.transitionMode:
// return 0.8;
// case ReadingAssistanceMode.selectMode:
// case null:
// return 0.6;
// }
// }
T _runWithLogging<T>(
Function runner,
String errorMessage,
@ -498,10 +134,16 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
}
final Duration transitionAnimationDuration =
const Duration(milliseconds: 300);
final Offset _defaultMessageOffset =
const Offset(Avatar.defaultSize + 16 + 8, 300);
double get _horizontalPadding =>
FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
bool get _hasReactions {
bool get hasReactions {
final reactionsEvents = widget.event.aggregatedEvents(
widget.chatController.timeline!,
RelationshipTypes.reaction,
@ -509,23 +151,23 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return reactionsEvents.where((e) => !e.redacted).isNotEmpty;
}
double get _reactionsHeight => _hasReactions ? 32.0 : 0.0;
double get reactionsHeight => hasReactions ? 32.0 : 0.0;
bool get _ownMessage =>
bool get ownMessage =>
widget.event.senderId == widget.event.room.client.userID;
bool get _showDetails =>
bool get showDetails =>
AppSettings.displayChatDetailsColumn.getItem(Matrix.of(context).store) &&
FluffyThemes.isThreeColumnMode(context) &&
widget.chatController.room.membership == Membership.join;
MediaQueryData? get _mediaQuery => _runWithLogging<MediaQueryData?>(
MediaQueryData? get mediaQuery => _runWithLogging<MediaQueryData?>(
() => MediaQuery.of(context),
"Error getting media query",
null,
);
double get _columnWidth => FluffyThemes.isColumnMode(context)
double get columnWidth => FluffyThemes.isColumnMode(context)
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth + 1.0)
: 0;
@ -538,8 +180,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
messageMargin;
double? maxWidth;
if (_mediaQuery != null) {
final chatViewWidth = _mediaQuery!.size.width - _columnWidth;
if (mediaQuery != null) {
final chatViewWidth = mediaQuery!.size.width - columnWidth;
maxWidth = chatViewWidth - (2 * _horizontalPadding) - messageMargin;
}
@ -550,9 +192,6 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
return maxWidth;
}
static const Offset _defaultMessageOffset =
Offset(Avatar.defaultSize + 16 + 8, 300);
Size get _defaultMessageSize => const Size(FluffyThemes.columnWidth / 2, 100);
RenderBox? get _overlayMessageRenderBox => _runWithLogging<RenderBox?>(
@ -565,6 +204,18 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
Size? get _overlayMessageSize => _overlayMessageRenderBox?.size;
Offset? get overlayMessageOffset {
if (_overlayMessageRenderBox == null ||
!_overlayMessageRenderBox!.hasSize) {
return null;
}
return _runWithLogging(
() => _overlayMessageRenderBox?.localToGlobal(Offset.zero),
"Error getting overlay message offset",
null,
);
}
RenderBox? get _messageRenderBox => _runWithLogging<RenderBox?>(
() => MatrixState.pAnyState.getRenderBox(
widget.event.eventId,
@ -585,7 +236,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
}
/// The size of the message in the chat list (as opposed to the expanded size in the center overlay)
Size get _originalMessageSize {
Size get originalMessageSize {
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
return _defaultMessageSize;
}
@ -597,25 +248,23 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
);
}
double? get _messageLeftOffset {
if (_ownMessage) return null;
return max(_originalMessageOffset.dx - _columnWidth, 0);
double? get messageLeftOffset {
if (ownMessage) return null;
return max(_originalMessageOffset.dx - columnWidth, 0);
}
double? get _messageRightOffset {
if (_mediaQuery == null || !_ownMessage) return null;
return _mediaQuery!.size.width -
double? get messageRightOffset {
if (mediaQuery == null || !ownMessage) return null;
return mediaQuery!.size.width -
_originalMessageOffset.dx -
_originalMessageSize.width -
(_showDetails ? FluffyThemes.columnWidth : 0);
originalMessageSize.width -
(showDetails ? FluffyThemes.columnWidth : 0);
}
double? get _contentHeight {
if (_overlayMessageSize == null) return null;
return _overlayMessageSize!.height +
_reactionsHeight +
AppConfig.toolbarMenuHeight +
4.0;
double get _contentHeight {
final messageHeight =
_overlayMessageSize?.height ?? originalMessageSize.height;
return messageHeight + reactionsHeight + AppConfig.toolbarMenuHeight + 4.0;
}
double get _overheadContentHeight {
@ -625,44 +274,86 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
: 40.0;
}
double? get _availableSpaceAboveContent {
if (_contentHeight == null || _mediaQuery == null) return null;
return max(
0,
(_mediaQuery!.size.height -
_mediaQuery!.padding.top -
_mediaQuery!.padding.bottom -
_contentHeight!) /
2,
);
}
double? get _wordCardTopOffset {
if (_contentHeight == null || _availableSpaceAboveContent == null) {
return null;
}
if (_availableSpaceAboveContent! >= _overheadContentHeight) {
return _availableSpaceAboveContent! - _overheadContentHeight - 4.0;
}
return 0;
}
double? get _wordCardLeftOffset {
if (_ownMessage) return null;
if (ownMessage) return null;
if (widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken != null &&
_mediaQuery != null &&
(_mediaQuery!.size.width < _toolbarMaxWidth + _messageLeftOffset!)) {
return _mediaQuery!.size.width - _toolbarMaxWidth - 8.0;
mediaQuery != null &&
(mediaQuery!.size.width < _toolbarMaxWidth + messageLeftOffset!)) {
return mediaQuery!.size.width - _toolbarMaxWidth - 8.0;
}
return messageLeftOffset;
}
double get _fullContentHeight {
return _contentHeight + _overheadContentHeight;
}
bool get shouldScroll {
if (mediaQuery == null) return false;
return _fullContentHeight >
(mediaQuery!.size.height -
mediaQuery!.padding.bottom -
mediaQuery!.padding.top);
}
bool get _hasFooterOverflow {
if (mediaQuery == null || _overlayMessageSize == null) return false;
final bottomOffset = _originalMessageOffset.dy +
originalMessageSize.height +
reactionsHeight +
AppConfig.toolbarMenuHeight +
4.0;
return bottomOffset >
(mediaQuery!.size.height -
mediaQuery!.padding.bottom -
mediaQuery!.padding.top);
}
double get spaceAboveContent {
if (shouldScroll) return _overheadContentHeight;
if (_hasFooterOverflow) {
return mediaQuery!.size.height -
mediaQuery!.padding.top -
_fullContentHeight;
}
return _originalMessageOffset.dy -
mediaQuery!.padding.top -
_overheadContentHeight;
}
void _onContentSizeChanged(_) {
Future.delayed(FluffyThemes.animationDuration, () {
setState(() {});
});
}
void onStartedTransition() {
if (mounted) {
setState(() {
startedTransition = true;
});
}
}
void onFinishedTransition() {
if (mounted) {
setState(() {
finishedTransition = true;
});
}
}
void setReadingAssistanceMode(ReadingAssistanceMode mode) {
if (mounted) {
setState(() => readingAssistanceMode = mode);
}
return _messageLeftOffset;
}
@override
Widget build(BuildContext context) {
if (_messageRenderBox == null || _mediaQuery == null) {
if (_messageRenderBox == null || mediaQuery == null) {
return const SizedBox.shrink();
}
@ -672,119 +363,55 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
children: [
Column(
children: [
Expanded(
child: SizedBox(
width: _mediaQuery!.size.width -
_columnWidth -
(_showDetails ? FluffyThemes.columnWidth : 0),
child: Stack(
alignment: _ownMessage
? Alignment.centerRight
: Alignment.centerLeft,
children: [
GestureDetector(
onTap: widget.chatController.clearSelectedEvents,
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.only(
left: _messageLeftOffset ?? 0.0,
right: _messageRightOffset ?? 0.0,
),
child: Column(
crossAxisAlignment: _ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (_contentHeight != null &&
_mediaQuery != null &&
_availableSpaceAboveContent != null &&
_availableSpaceAboveContent! <
_overheadContentHeight)
AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: _contentHeight! +
_overheadContentHeight >
_mediaQuery!.size.height
? _overheadContentHeight
: (_overheadContentHeight -
_availableSpaceAboveContent!) *
2,
),
CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
'overlay_message_${widget.event.eventId}',
)
.link,
child: OverlayCenterContent(
event: widget.event,
messageHeight: _originalMessageSize.height,
messageWidth: widget
.overlayController.showingExtraContent
? max(_originalMessageSize.width, 150)
: _originalMessageSize.width,
overlayController: widget.overlayController,
chatController: widget.chatController,
nextEvent: widget.nextEvent,
prevEvent: widget.prevEvent,
hasReactions: _hasReactions,
// sizeAnimation: _messageSizeAnimation,
isTransitionAnimation: true,
readingAssistanceMode: widget
.overlayController.readingAssistanceMode,
),
),
const SizedBox(height: 4.0),
SelectModeButtons(
controller: widget.chatController,
overlayController: widget.overlayController,
lauchPractice: () {},
// lauchPractice: () {
// _setReadingAssistanceMode(
// ReadingAssistanceMode.practiceMode,
// );
// widget.overlayController
// .updateSelectedSpan(null);
// },
),
],
),
SizedBox(
width: mediaQuery!.size.width -
columnWidth -
(showDetails ? FluffyThemes.columnWidth : 0),
height: mediaQuery!.size.height -
mediaQuery!.padding.top -
mediaQuery!.padding.bottom,
child: Stack(
alignment:
ownMessage ? Alignment.centerRight : Alignment.centerLeft,
children: [
if (!startedTransition) ...[
OverMessageOverlay(controller: this),
if (shouldScroll)
Positioned(
top: 0,
left: _wordCardLeftOffset,
right: messageRightOffset,
child: WordCardSwitcher(controller: this),
),
],
if (readingAssistanceMode ==
ReadingAssistanceMode.practiceMode) ...[
CenteredMessage(
targetId:
"overlay_center_message_${widget.event.eventId}",
controller: this,
),
AnimatedPositioned(
top: _wordCardTopOffset,
left: _wordCardLeftOffset,
right: _messageRightOffset,
duration: FluffyThemes.animationDuration,
child: AnimatedSize(
alignment: _ownMessage
? Alignment.bottomRight
: Alignment.bottomLeft,
duration: FluffyThemes.animationDuration,
child: _wordCardTopOffset == null
? const SizedBox()
: widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken !=
null
? ReadingAssistanceContent(
pangeaMessageEvent:
widget.pangeaMessageEvent!,
overlayController:
widget.overlayController,
)
: MessageReactionPicker(
chatController: widget.chatController,
),
PracticeModeTransitionAnimation(
targetId:
"overlay_center_message_${widget.event.eventId}",
controller: this,
),
Positioned(
left: 0,
right: 0,
bottom: 20,
child: ReadingAssistanceInputBar(
widget.chatController,
widget.overlayController,
),
),
],
),
],
),
),
],
),
if (_showDetails)
if (showDetails)
const SizedBox(
width: FluffyThemes.columnWidth,
),

View file

@ -0,0 +1,87 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_card_switcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
class OverMessageOverlay extends StatelessWidget {
final MessageSelectionPositionerState controller;
const OverMessageOverlay({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Align(
alignment: controller.ownMessage ? Alignment.topRight : Alignment.topLeft,
child: Padding(
padding: EdgeInsets.only(
left: controller.messageLeftOffset ?? 0.0,
right: controller.messageRightOffset ?? 0.0,
),
child: GestureDetector(
onTap: controller.widget.chatController.clearSelectedEvents,
child: SingleChildScrollView(
controller: controller.scrollController,
child: Column(
crossAxisAlignment: controller.ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: max(0, controller.spaceAboveContent),
width: controller.mediaQuery!.size.width -
controller.columnWidth -
(controller.showDetails ? FluffyThemes.columnWidth : 0),
),
if (!controller.shouldScroll)
WordCardSwitcher(controller: controller),
CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
'overlay_message_${controller.widget.event.eventId}',
)
.link,
child: OverlayCenterContent(
event: controller.widget.event,
messageHeight: controller.originalMessageSize.height,
messageWidth:
controller.widget.overlayController.showingExtraContent
? max(controller.originalMessageSize.width, 150)
: controller.originalMessageSize.width,
overlayController: controller.widget.overlayController,
chatController: controller.widget.chatController,
nextEvent: controller.widget.nextEvent,
prevEvent: controller.widget.prevEvent,
hasReactions: controller.hasReactions,
isTransitionAnimation: true,
readingAssistanceMode: controller.readingAssistanceMode,
overlayKey: MatrixState.pAnyState
.layerLinkAndKey(
'overlay_message_${controller.widget.event.eventId}',
)
.key,
),
),
const SizedBox(height: 4.0),
SelectModeButtons(
controller: controller.widget.chatController,
overlayController: controller.widget.overlayController,
lauchPractice: () => controller.setReadingAssistanceMode(
ReadingAssistanceMode.practiceMode,
),
),
],
),
),
),
),
);
}
}

View file

@ -8,7 +8,6 @@ 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;
@ -29,10 +28,12 @@ class OverlayCenterContent extends StatelessWidget {
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
final LabeledGlobalKey? overlayKey;
const OverlayCenterContent({
required this.event,
required this.messageHeight,
required this.messageWidth,
this.messageHeight,
this.messageWidth,
required this.overlayController,
required this.chatController,
required this.nextEvent,
@ -42,6 +43,7 @@ class OverlayCenterContent extends StatelessWidget {
this.sizeAnimation,
this.isTransitionAnimation = false,
this.readingAssistanceMode,
this.overlayKey,
super.key,
});
@ -63,11 +65,7 @@ class OverlayCenterContent extends StatelessWidget {
MeasureRenderBox(
onChange: onChangeMessageSize,
child: OverlayMessage(
key: isTransitionAnimation
? MatrixState.pAnyState
.layerLinkAndKey('overlay_message_${event.eventId}')
.key
: null,
key: overlayKey,
event,
immersionMode: chatController.choreographer.immersionMode,
controller: chatController,
@ -78,13 +76,8 @@ class OverlayCenterContent extends StatelessWidget {
sizeAnimation: sizeAnimation,
// there's a split seconds between when the transition animation starts and
// when the sizeAnimation is set when the original dimensions need to be enforced
messageWidth: (sizeAnimation == null && isTransitionAnimation)
? messageWidth
: null,
messageHeight:
(sizeAnimation == null && isTransitionAnimation)
? messageHeight
: null,
messageWidth: messageWidth,
messageHeight: messageHeight,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
),

View file

@ -1,171 +0,0 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
class OverlayHeader extends StatefulWidget {
final ChatController controller;
const OverlayHeader({
required this.controller,
super.key,
});
@override
State<OverlayHeader> createState() => OverlayHeaderState();
}
class OverlayHeaderState extends State<OverlayHeader> {
ChatController get controller => widget.controller;
final ScrollController _scrollController = ScrollController();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context);
final theme = Theme.of(context);
final pinned = controller.selectedEvents.length == 1 &&
controller.room.pinnedEventIds.contains(
controller.selectedEvents.first.eventId,
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(AppConfig.borderRadius),
bottomRight: Radius.circular(AppConfig.borderRadius),
),
color: theme.appBarTheme.backgroundColor ??
theme.colorScheme.surfaceContainerHighest,
),
height: theme.appBarTheme.toolbarHeight ?? AppConfig.defaultHeaderHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: Scrollbar(
thumbVisibility: true,
controller: _scrollController,
child: Align(
alignment: Alignment.centerRight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _scrollController,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
// #Pangea
// if (controller.selectedEvents.length == 1)
if (controller.selectedEvents.length == 1 &&
controller.room.canSendDefaultMessages)
// Pangea#
IconButton(
icon: const Icon(Symbols.reply_all),
tooltip: l10n.reply,
onPressed: controller.replyAction,
color: theme.colorScheme.primary,
),
IconButton(
icon: const Icon(Symbols.forward),
tooltip: l10n.forward,
onPressed: controller.forwardEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1 &&
controller.selectedEvents.single.messageType ==
MessageTypes.Text)
IconButton(
icon: const Icon(Icons.copy_outlined),
tooltip: l10n.copy,
onPressed: controller.copyEventsAction,
color: theme.colorScheme.primary,
),
if (controller.canSaveSelectedEvent)
// Use builder context to correctly position the share dialog on iPad
Builder(
builder: (context) => IconButton(
icon: const Icon(Symbols.download),
tooltip: L10n.of(context).download,
onPressed: () =>
controller.saveSelectedEvent(context),
color: theme.colorScheme.primary,
),
),
if (controller.canPinSelectedEvents)
IconButton(
icon: pinned
? const Icon(Icons.push_pin)
: const Icon(Icons.push_pin_outlined),
onPressed: () {
controller
.pinEvent()
.then((_) => setState(() {}));
},
tooltip: pinned ? l10n.unpin : l10n.pinMessage,
color: theme.colorScheme.primary,
),
// if (controller.canEditSelectedEvents &&
// !controller.selectedEvents.first.isActivityMessage)
// IconButton(
// icon: const Icon(Icons.edit_outlined),
// tooltip: l10n.edit,
// onPressed: controller.editSelectedEventAction,
// color: theme.colorScheme.primary,
// ),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: l10n.redactMessage,
onPressed: controller.redactEventsAction,
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.shield_outlined),
tooltip: l10n.reportMessage,
onPressed: () {
final event = controller.selectedEvents.first;
controller.clearSelectedEvents();
reportEvent(
event,
controller,
controller.context,
);
},
color: theme.colorScheme.primary,
),
if (controller.selectedEvents.length == 1)
IconButton(
icon: const Icon(Icons.info_outlined),
tooltip: l10n.messageInfo,
color: theme.colorScheme.primary,
onPressed: () {
controller.showEventInfo();
controller.clearSelectedEvents();
},
),
],
),
),
),
),
),
),
],
),
);
}
}

View file

@ -334,21 +334,23 @@ class OverlayMessage extends StatelessWidget {
);
},
),
MessageContent(
displayEvent,
textColor: textColor,
linkColor: linkColor,
borderRadius: borderRadius,
timeline: timeline,
pangeaMessageEvent: overlayController.pangeaMessageEvent,
immersionMode: immersionMode,
overlayController: overlayController,
controller: controller,
nextEvent: nextEvent,
prevEvent: previousEvent,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
selected: true,
Flexible(
child: MessageContent(
displayEvent,
textColor: textColor,
linkColor: linkColor,
borderRadius: borderRadius,
timeline: timeline,
pangeaMessageEvent: overlayController.pangeaMessageEvent,
immersionMode: immersionMode,
overlayController: overlayController,
controller: controller,
nextEvent: nextEvent,
prevEvent: previousEvent,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
selected: true,
),
),
if (event.hasAggregatedEvents(
timeline,

View file

@ -1,33 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/emojis/emoji_stack.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
class EmojiPracticeButton extends StatelessWidget {
final PangeaToken token;
final VoidCallback onPressed;
final bool isSelected;
const EmojiPracticeButton({
required this.token,
required this.onPressed,
this.isSelected = false,
super.key,
});
@override
Widget build(BuildContext context) {
final emoji = token.getEmoji();
return WordZoomActivityButton(
icon: emoji.isEmpty
? const Icon(Icons.add_reaction_outlined)
: EmojiStack(
emoji: emoji,
style: const TextStyle(fontSize: 24),
),
isSelected: isSelected,
onPressed: onPressed,
);
}
}

View file

@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
import 'package:fluffychat/widgets/matrix.dart';
class PracticeModeTransitionAnimation extends StatefulWidget {
final String targetId;
final MessageSelectionPositionerState controller;
const PracticeModeTransitionAnimation({
super.key,
required this.targetId,
required this.controller,
});
@override
State<PracticeModeTransitionAnimation> createState() =>
PracticeModeTransitionAnimationState();
}
class PracticeModeTransitionAnimationState
extends State<PracticeModeTransitionAnimation>
with SingleTickerProviderStateMixin {
AnimationController? _animationController;
Animation<Offset>? _offsetAnimation;
Animation<Size>? _sizeAnimation;
bool _finishedAnimation = false;
RenderBox? get _centerMessageRenderBox {
try {
return MatrixState.pAnyState.getRenderBox(widget.targetId);
} catch (e) {
return null;
}
}
Offset? get _centerMessageOffset {
final renderBox = _centerMessageRenderBox;
if (renderBox == null) {
return null;
}
return renderBox.localToGlobal(Offset.zero);
}
Size? get _centerMessageSize {
final renderBox = _centerMessageRenderBox;
if (renderBox == null) {
return null;
}
return renderBox.size;
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final startOffset = Offset(
widget.controller.ownMessage
? widget.controller.messageRightOffset!
: widget.controller.messageLeftOffset!,
widget.controller.overlayMessageOffset!.dy,
);
final endOffset = Offset(
_centerMessageOffset!.dx - widget.controller.columnWidth,
_centerMessageOffset!.dy,
);
_animationController = AnimationController(
vsync: this,
duration: widget.controller.transitionAnimationDuration,
// duration: const Duration(seconds: 3),
);
_offsetAnimation = Tween<Offset>(
begin: startOffset,
end: endOffset,
).animate(
CurvedAnimation(
parent: _animationController!,
curve: FluffyThemes.animationCurve,
),
);
final startSize = Size(
widget.controller.originalMessageSize.width,
widget.controller.originalMessageSize.height,
);
_sizeAnimation = Tween<Size>(
begin: startSize,
end: _centerMessageSize!,
).animate(
CurvedAnimation(
parent: _animationController!,
curve: FluffyThemes.animationCurve,
),
);
widget.controller.onStartedTransition();
_animationController!.forward().then((_) {
widget.controller.onFinishedTransition();
if (mounted) {
setState(() {
_finishedAnimation = true;
});
}
});
});
}
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_offsetAnimation == null || _finishedAnimation) {
return const SizedBox();
}
return AnimatedBuilder(
animation: _offsetAnimation!,
builder: (context, child) {
return Positioned(
top: _offsetAnimation!.value.dy,
left:
widget.controller.ownMessage ? null : _offsetAnimation!.value.dx,
right:
widget.controller.ownMessage ? _offsetAnimation!.value.dx : null,
child: OverlayCenterContent(
event: widget.controller.widget.event,
overlayController: widget.controller.widget.overlayController,
chatController: widget.controller.widget.chatController,
nextEvent: widget.controller.widget.nextEvent,
prevEvent: widget.controller.widget.prevEvent,
hasReactions: widget.controller.hasReactions,
sizeAnimation: _sizeAnimation,
readingAssistanceMode: widget.controller.readingAssistanceMode,
),
);
},
);
}
}
class CenteredMessage extends StatelessWidget {
final String targetId;
final MessageSelectionPositionerState controller;
const CenteredMessage({
super.key,
required this.targetId,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Opacity(
opacity: controller.finishedTransition ? 1.0 : 0.0,
child: GestureDetector(
onTap: controller.widget.chatController.clearSelectedEvents,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width:
controller.mediaQuery!.size.width - controller.columnWidth,
height: 20.0,
),
OverlayCenterContent(
event: controller.widget.event,
overlayController: controller.widget.overlayController,
chatController: controller.widget.chatController,
nextEvent: controller.widget.nextEvent,
prevEvent: controller.widget.prevEvent,
hasReactions: controller.hasReactions,
overlayKey: MatrixState.pAnyState
.layerLinkAndKey(
"overlay_center_message_${controller.widget.event.eventId}",
)
.key,
readingAssistanceMode: controller.readingAssistanceMode,
),
const SizedBox(
height: AppConfig.readingAssistanceInputBarHeight + 60.0,
),
],
),
),
),
);
}
}

View file

@ -3,7 +3,6 @@ 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;
@ -55,29 +54,21 @@ class SttTranscriptTokens extends StatelessWidget {
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),
),
child: MouseRegion(
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),
),
),
),

View file

@ -1,104 +0,0 @@
import 'dart:math';
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/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
class ToolbarButtonAndProgressColumn extends StatelessWidget {
final Event event;
final MessageOverlayController overlayController;
final double height;
final double width;
const ToolbarButtonAndProgressColumn({
required this.event,
required this.overlayController,
required this.height,
required this.width,
super.key,
});
double? get proportionOfActivitiesCompleted =>
overlayController.pangeaMessageEvent?.proportionOfActivitiesCompleted;
static const double iconWidth = 36.0;
static const double buttonSize = 40.0;
static const barMargin =
EdgeInsets.symmetric(horizontal: iconWidth / 2, vertical: buttonSize / 2);
@override
Widget build(BuildContext context) {
if (event.messageType == MessageTypes.Audio ||
!(overlayController.pangeaMessageEvent?.messageDisplayLangIsL2 ??
false)) {
return SizedBox(height: height, width: width);
}
return SizedBox(
height: height,
width: width,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Stack(
alignment: Alignment.bottomCenter,
children: [
Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: MessageModeExtension.barAndLockedButtonColor(context),
),
margin: barMargin,
),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
width: width,
height: overlayController.isPracticeComplete
? height
: min(
height,
height * proportionOfActivitiesCompleted!,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: AppConfig.gold,
),
margin: barMargin,
),
Positioned(
bottom: height * MessageMode.noneSelected.pointOnBar -
buttonSize / 2 -
barMargin.vertical / 2,
child: Container(
decoration: BoxDecoration(
color: overlayController.isPracticeComplete
? AppConfig.gold
: MessageModeExtension.barAndLockedButtonColor(context),
shape: BoxShape.circle,
),
height: buttonSize,
width: buttonSize,
alignment: Alignment.center,
child: Icon(
Icons.star_rounded,
color: overlayController.isPracticeComplete
? Colors.white
: Theme.of(context).colorScheme.onSurface,
size: 30,
),
),
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
class WordCardSwitcher extends StatelessWidget {
final MessageSelectionPositionerState controller;
const WordCardSwitcher({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return AnimatedSize(
alignment:
controller.ownMessage ? Alignment.bottomRight : Alignment.bottomLeft,
duration: FluffyThemes.animationDuration,
child: controller.widget.pangeaMessageEvent != null &&
controller.widget.overlayController.selectedToken != null
? ReadingAssistanceContent(
pangeaMessageEvent: controller.widget.pangeaMessageEvent!,
overlayController: controller.widget.overlayController,
)
: MessageReactionPicker(
chatController: controller.widget.chatController,
),
);
}
}