refactor: new message selection mode

This commit is contained in:
ggurdin 2025-07-03 12:36:21 -04:00
parent 74e1032c1c
commit aeb92b1b89
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
10 changed files with 988 additions and 384 deletions

View file

@ -27,9 +27,10 @@ abstract class AppConfig {
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
// #Pangea
static const double toolbarMaxHeight = 250.0;
static const double toolbarMaxHeight = 225.0;
static const double toolbarMinHeight = 150.0;
static const double toolbarMinWidth = 350.0;
static const double toolbarMenuHeight = 300.0;
static const double defaultHeaderHeight = 56.0;
static const double toolbarButtonsHeight = 50.0;
static const double toolbarSpacing = 8.0;
@ -89,6 +90,8 @@ abstract class AppConfig {
static String _privacyUrl = "https://www.pangeachat.com/privacy";
//Pangea#
static const Set<String> defaultReactions = {'👍', '❤️', '😂', '😮', '😢'};
static String get privacyUrl => _privacyUrl;
// #Pangea
// static const String website = 'https://fluffychat.im';

View file

@ -5031,5 +5031,6 @@
}
},
"failedToFetchTranscription": "Failed to fetch transcription",
"deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone."
"deleteEmptySpaceDesc": "The space will be deleted for all participants. This action cannot be undone.",
"customReaction": "Custom reaction"
}

View file

@ -2099,10 +2099,10 @@ class ChatController extends State<ChatPageWithRoom>
OverlayUtil.showOverlay(
context: context,
child: overlayEntry!,
transformTargetId: "",
position: OverlayPositionEnum.centered,
onDismiss: clearSelectedEvents,
blurBackground: true,
backgroundColor: Colors.black,
);
// select the message

View file

@ -21,7 +21,7 @@ class OverlayUtil {
static showOverlay({
required BuildContext context,
required Widget child,
required String transformTargetId,
String? transformTargetId,
backDropToDismiss = true,
blurBackground = false,
Color? borderColor,
@ -37,6 +37,13 @@ class OverlayUtil {
bool canPop = true,
}) {
try {
if (position == OverlayPositionEnum.transform) {
assert(
transformTargetId != null,
"transformTargetId must be provided when position is OverlayPositionEnum.transform",
);
}
if (closePrevOverlay) {
MatrixState.pAnyState.closeOverlay();
}
@ -77,7 +84,7 @@ class OverlayUtil {
followerAnchor:
followerAnchor ?? Alignment.bottomCenter,
link: MatrixState.pAnyState
.layerLinkAndKey(transformTargetId)
.layerLinkAndKey(transformTargetId!)
.link,
showWhenUnlinked: false,
offset: offset ?? Offset.zero,

View file

@ -217,16 +217,16 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
updateSelectedSpan(widget._initialSelectedToken!.text);
int retries = 0;
while (retries < 5 &&
selectedToken != null &&
!MatrixState.pAnyState.isOverlayOpen(
selectedToken!.text.uniqueKey,
)) {
await Future.delayed(const Duration(milliseconds: 100));
_showReadingAssistanceContent();
retries++;
}
// int retries = 0;
// while (retries < 5 &&
// selectedToken != null &&
// !MatrixState.pAnyState.isOverlayOpen(
// selectedToken!.text.uniqueKey,
// )) {
// await Future.delayed(const Duration(milliseconds: 100));
// _showReadingAssistanceContent();
// retries++;
// }
}
/////////////////////////////////////
@ -296,9 +296,9 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
}
if (mounted) setState(() {});
Future.delayed(const Duration(milliseconds: 10), () {
_showReadingAssistanceContent();
});
// Future.delayed(const Duration(milliseconds: 10), () {
// _showReadingAssistanceContent();
// });
}
void _showReadingAssistanceContent() {

View file

@ -8,19 +8,16 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
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/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.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/reading_assistance_input_row/overlay_footer.dart';
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_center_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -73,6 +70,8 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
StreamSubscription? _reactionSubscription;
StreamSubscription? _contentChangedSubscription;
ScrollController? _scrollController;
final _animationDuration = const Duration(
milliseconds: AppConfig.overlayAnimationDuration,
// seconds: 5,
@ -81,6 +80,22 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
@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,
);
},
);
_currentMode = widget.overlayController.toolbarMode;
_animationController = AnimationController(
vsync: this,
@ -144,6 +159,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
_animationController.dispose();
_reactionSubscription?.cancel();
_contentChangedSubscription?.cancel();
_scrollController?.dispose();
MatrixState.pangeaController.matrixState.audioPlayer
?..stop()
..dispose();
@ -349,6 +365,62 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
null,
);
// Offset? get _overlayMessageOffset =>
// _overlayMessageRenderBox?.localToGlobal(Offset.zero);
Size? get _overlayMessageSize => _overlayMessageRenderBox?.size;
// 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;
// }
double get _neededTopSpace =>
(widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken != null
? AppConfig.toolbarMaxHeight
: 40.0) +
4.0;
double? get _occupiedSpace {
if (_overlayMessageSize == null) return null;
return _overlayMessageSize!.height +
_reactionsHeight +
AppConfig.toolbarMenuHeight;
}
double? get _wordCardTopOffset {
if (_overlayMessageSize == null ||
_mediaQuery == null ||
_occupiedSpace == null) {
return null;
}
final availableSpace = (_mediaQuery!.size.height - _occupiedSpace!) / 2;
if (availableSpace >= _neededTopSpace) {
return availableSpace - _neededTopSpace;
}
return 0;
}
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)
@ -491,10 +563,13 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
_reactionsHeight;
}
double get _messageLeftOffset => max(
_originalMessageOffset.dx - _columnWidth - _horizontalPadding,
0,
);
double get _messageLeftOffset {
if (_ownMessage) return 0;
return max(
_originalMessageOffset.dx - _columnWidth - _horizontalPadding,
0,
);
}
double get _messageRightOffset {
if (_mediaQuery == null || !_ownMessage) {
@ -587,225 +662,577 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
widget.overlayController.maxWidth = _toolbarMaxWidth;
return Stack(
debugPrint(
"width: ${_mediaQuery!.size.width - _columnWidth - (_showDetails ? FluffyThemes.columnWidth : 0)}",
);
return Row(
children: [
Positioned.fill(
child: IgnorePointer(
child: AnimatedOpacity(
duration: _animationDuration,
opacity: _readingAssistanceModeOpacity,
child: Container(
height: double.infinity,
width: double.infinity,
color: Colors.black,
),
),
),
),
Padding(
padding: EdgeInsets.only(
left: _horizontalPadding,
right: _horizontalPadding,
),
child: Row(
children: [
Expanded(
Column(
children: [
Expanded(
child: SizedBox(
width: _mediaQuery!.size.width -
_columnWidth -
(_showDetails ? FluffyThemes.columnWidth : 0),
child: Stack(
alignment: Alignment.center,
alignment: _ownMessage
? Alignment.centerRight
: Alignment.centerLeft,
children: [
Column(
children: [
Material(
type: MaterialType.transparency,
child: Column(
children: [
SizedBox(height: _mediaQuery?.padding.top ?? 0),
OverlayHeader(controller: widget.chatController),
],
),
GestureDetector(
onTap: widget.chatController.clearSelectedEvents,
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.only(
left: _messageLeftOffset + _horizontalPadding,
right: _messageRightOffset + _horizontalPadding,
),
const Expanded(
flex: 3,
child: SizedBox.shrink(),
),
Opacity(
opacity: _readingAssistanceMode ==
ReadingAssistanceMode.practiceMode
? 1.0
: 0.0,
child: OverlayCenterContent(
event: widget.event,
messageHeight: null,
messageWidth: null,
maxWidth: widget.overlayController.maxWidth,
overlayController: widget.overlayController,
chatController: widget.chatController,
pangeaMessageEvent: widget.pangeaMessageEvent,
nextEvent: widget.nextEvent,
prevEvent: widget.prevEvent,
hasReactions: _hasReactions,
onChangeMessageSize: _setCenteredMessageSize,
isTransitionAnimation: false,
maxHeight: _mediaQuery!.size.height -
_headerHeight -
_footerHeight -
AppConfig.toolbarSpacing * 2 -
_selectionButtonsHeight,
readingAssistanceMode: _readingAssistanceMode,
),
),
const Expanded(
flex: 1,
child: SizedBox.shrink(),
),
Row(
child: Column(
spacing: 4.0,
crossAxisAlignment: _ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
OverlayFooter(
controller: widget.chatController,
overlayController: widget.overlayController,
showToolbarButtons: showPracticeButtons,
readingAssistanceMode:
_readingAssistanceMode,
),
SizedBox(
height: _mediaQuery?.padding.bottom ?? 0,
),
],
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: _readingAssistanceMode,
),
),
SelectModeButtons(
controller: widget.chatController,
overlayController: widget.overlayController,
lauchPractice: () {
_setReadingAssistanceMode(
ReadingAssistanceMode.practiceMode,
);
widget.overlayController
.updateSelectedSpan(null);
},
),
],
),
],
),
),
if (_readingAssistanceMode !=
ReadingAssistanceMode.practiceMode &&
_readingAssistanceMode != null)
AnimatedBuilder(
animation:
_overlayOffsetAnimation ?? _animationController,
builder: (context, child) {
return Positioned(
left: _ownMessage
? null
: (_overlayOffsetAnimation?.value)?.dx ??
_messageLeftOffset,
right: _ownMessage
? (_overlayOffsetAnimation?.value)?.dx ??
_messageRightOffset
: null,
bottom: (_overlayOffsetAnimation?.value)?.dy ??
_originalMessageBottomOffset -
_reactionsHeight -
_selectionButtonsHeight,
child: Column(
crossAxisAlignment: _ownMessage
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
OverlayCenterContent(
event: widget.event,
messageHeight: _originalMessageSize.height,
messageWidth: widget
.overlayController.showingExtraContent
? max(_originalMessageSize.width, 150)
: _originalMessageSize.width,
maxWidth: widget.overlayController.maxWidth,
if (_wordCardTopOffset != null)
AnimatedPositioned(
top: _wordCardTopOffset,
left: _ownMessage
? null
: _messageLeftOffset + _horizontalPadding,
right: _ownMessage
? _messageRightOffset + _horizontalPadding
: null,
duration: FluffyThemes.animationDuration,
child: AnimatedSize(
duration: FluffyThemes.animationDuration,
child: widget.pangeaMessageEvent != null &&
widget.overlayController.selectedToken != null
? ReadingAssistanceContent(
pangeaMessageEvent:
widget.pangeaMessageEvent!,
overlayController: widget.overlayController,
)
: MessageReactionPicker(
chatController: widget.chatController,
pangeaMessageEvent: widget.pangeaMessageEvent,
nextEvent: widget.nextEvent,
prevEvent: widget.prevEvent,
hasReactions: _hasReactions,
sizeAnimation: _messageSizeAnimation,
isTransitionAnimation: true,
maxHeight: _mediaQuery!.size.height -
_headerHeight -
_footerHeight -
AppConfig.toolbarSpacing * 2 -
_selectionButtonsHeight,
readingAssistanceMode: _readingAssistanceMode,
),
if (showSelectionButtons)
SelectModeButtons(
overlayController: widget.overlayController,
lauchPractice: () {
_setReadingAssistanceMode(
ReadingAssistanceMode.practiceMode,
);
widget.overlayController
.updateSelectedSpan(null);
},
),
],
),
);
},
),
if (showPracticeButtons)
Positioned(
top: 0,
child: IgnorePointer(
child: MeasureRenderBox(
onChange: _setTooltipSize,
child: Opacity(
opacity: 0.0,
child: Container(
constraints: BoxConstraints(
minWidth: 200.0,
maxWidth: _toolbarMaxWidth,
),
child: InstructionsInlineTooltip(
instructionsEnum: widget.overlayController
.toolbarMode.instructionsEnum ??
InstructionsEnum
.readingAssistanceOverview,
bold: true,
),
),
),
),
),
),
if (_centeredMessageTopOffset != null &&
_tooltipSize != null &&
widget.overlayController.toolbarMode !=
MessageMode.noneSelected &&
widget.overlayController.selectedToken == null)
Positioned(
top: max(
((_headerHeight + _centeredMessageTopOffset!) / 2) -
(_tooltipSize!.height / 2),
_headerHeight,
),
child: Container(
constraints: BoxConstraints(
minWidth: 200.0,
maxWidth: widget.overlayController.maxWidth,
),
child: InstructionsInlineTooltip(
instructionsEnum: widget.overlayController
.toolbarMode.instructionsEnum ??
InstructionsEnum.readingAssistanceOverview,
bold: true,
),
),
),
],
),
),
if (_showDetails)
const SizedBox(
width: FluffyThemes.columnWidth,
),
],
),
),
],
),
if (_showDetails)
const SizedBox(
width: FluffyThemes.columnWidth,
),
],
);
// return Align(
// alignment: Alignment.centerLeft,
// child: SizedBox(
// width: _mediaQuery!.size.width -
// _columnWidth -
// (_showDetails ? FluffyThemes.columnWidth : 0),
// height: _mediaQuery!.size.height,
// child: SingleChildScrollView(
// child: Container(
// decoration: BoxDecoration(
// border: Border.all(color: Colors.green),
// ),
// padding: EdgeInsets.only(
// left: _messageLeftOffset + _horizontalPadding,
// right: _messageRightOffset + _horizontalPadding,
// ),
// child: Stack(
// alignment:
// _ownMessage ? Alignment.centerRight : Alignment.centerLeft,
// children: [
// Positioned.fill(
// child: InkWell(
// onTap: widget.chatController.clearSelectedEvents,
// ),
// ),
// Column(
// spacing: 4.0,
// crossAxisAlignment: _ownMessage
// ? CrossAxisAlignment.end
// : CrossAxisAlignment.start,
// children: [
// if (widget.pangeaMessageEvent != null)
// AnimatedSize(
// duration: FluffyThemes.animationDuration,
// child: widget.overlayController.selectedToken != null
// ? ReadingAssistanceContent(
// pangeaMessageEvent: widget.pangeaMessageEvent!,
// overlayController: widget.overlayController,
// )
// : const SizedBox.shrink(),
// ),
// 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: _readingAssistanceMode,
// ),
// ),
// ],
// ),
// if (showSelectionButtons)
// Positioned(
// child: SelectModeButtons(
// overlayController: widget.overlayController,
// lauchPractice: () {
// _setReadingAssistanceMode(
// ReadingAssistanceMode.practiceMode,
// );
// widget.overlayController.updateSelectedSpan(null);
// },
// ),
// ),
// ],
// ),
// ),
// ),
// ),
// );
// return Stack(
// children: [
// Positioned.fill(
// child: IgnorePointer(
// child: AnimatedOpacity(
// duration: _animationDuration,
// opacity: _readingAssistanceModeOpacity,
// child: Container(
// height: double.infinity,
// width: double.infinity,
// color: Colors.black,
// ),
// ),
// ),
// ),
// Padding(
// padding: EdgeInsets.only(
// left: _horizontalPadding,
// right: _horizontalPadding,
// ),
// child: Row(
// children: [
// Expanded(
// child: Stack(
// alignment: Alignment.center,
// children: [
// Column(
// children: [
// Material(
// type: MaterialType.transparency,
// child: Column(
// children: [
// SizedBox(height: _mediaQuery?.padding.top ?? 0),
// OverlayHeader(controller: widget.chatController),
// ],
// ),
// ),
// const Expanded(
// flex: 3,
// child: SizedBox.shrink(),
// ),
// Opacity(
// opacity: _readingAssistanceMode ==
// ReadingAssistanceMode.practiceMode
// ? 1.0
// : 0.0,
// child: OverlayCenterContent(
// event: widget.event,
// messageHeight: null,
// messageWidth: null,
// maxWidth: widget.overlayController.maxWidth,
// overlayController: widget.overlayController,
// chatController: widget.chatController,
// pangeaMessageEvent: widget.pangeaMessageEvent,
// nextEvent: widget.nextEvent,
// prevEvent: widget.prevEvent,
// hasReactions: _hasReactions,
// onChangeMessageSize: _setCenteredMessageSize,
// isTransitionAnimation: false,
// maxHeight: _mediaQuery!.size.height -
// _headerHeight -
// _footerHeight -
// AppConfig.toolbarSpacing * 2 -
// _selectionButtonsHeight,
// readingAssistanceMode: _readingAssistanceMode,
// ),
// ),
// const Expanded(
// flex: 1,
// child: SizedBox.shrink(),
// ),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// OverlayFooter(
// controller: widget.chatController,
// overlayController: widget.overlayController,
// showToolbarButtons: showPracticeButtons,
// readingAssistanceMode:
// _readingAssistanceMode,
// ),
// SizedBox(
// height: _mediaQuery?.padding.bottom ?? 0,
// ),
// ],
// ),
// ),
// ],
// ),
// ],
// ),
// if (_readingAssistanceMode !=
// ReadingAssistanceMode.practiceMode &&
// _readingAssistanceMode != null)
// AnimatedBuilder(
// animation:
// _overlayOffsetAnimation ?? _animationController,
// builder: (context, child) {
// return Positioned(
// left: _ownMessage
// ? null
// : (_overlayOffsetAnimation?.value)?.dx ??
// _messageLeftOffset,
// right: _ownMessage
// ? (_overlayOffsetAnimation?.value)?.dx ??
// _messageRightOffset
// : null,
// bottom: (_overlayOffsetAnimation?.value)?.dy ??
// _originalMessageBottomOffset -
// _reactionsHeight -
// _selectionButtonsHeight,
// child: Column(
// crossAxisAlignment: _ownMessage
// ? CrossAxisAlignment.end
// : CrossAxisAlignment.start,
// children: [
// OverlayCenterContent(
// event: widget.event,
// messageHeight: _originalMessageSize.height,
// messageWidth: widget
// .overlayController.showingExtraContent
// ? max(_originalMessageSize.width, 150)
// : _originalMessageSize.width,
// maxWidth: widget.overlayController.maxWidth,
// overlayController: widget.overlayController,
// chatController: widget.chatController,
// pangeaMessageEvent: widget.pangeaMessageEvent,
// nextEvent: widget.nextEvent,
// prevEvent: widget.prevEvent,
// hasReactions: _hasReactions,
// sizeAnimation: _messageSizeAnimation,
// isTransitionAnimation: true,
// maxHeight: _mediaQuery!.size.height -
// _headerHeight -
// _footerHeight -
// AppConfig.toolbarSpacing * 2 -
// _selectionButtonsHeight,
// readingAssistanceMode: _readingAssistanceMode,
// ),
// if (showSelectionButtons)
// SelectModeButtons(
// overlayController: widget.overlayController,
// lauchPractice: () {
// _setReadingAssistanceMode(
// ReadingAssistanceMode.practiceMode,
// );
// widget.overlayController
// .updateSelectedSpan(null);
// },
// ),
// ],
// ),
// );
// },
// ),
// if (showPracticeButtons)
// Positioned(
// top: 0,
// child: IgnorePointer(
// child: MeasureRenderBox(
// onChange: _setTooltipSize,
// child: Opacity(
// opacity: 0.0,
// child: Container(
// constraints: BoxConstraints(
// minWidth: 200.0,
// maxWidth: _toolbarMaxWidth,
// ),
// child: InstructionsInlineTooltip(
// instructionsEnum: widget.overlayController
// .toolbarMode.instructionsEnum ??
// InstructionsEnum
// .readingAssistanceOverview,
// bold: true,
// ),
// ),
// ),
// ),
// ),
// ),
// if (_centeredMessageTopOffset != null &&
// _tooltipSize != null &&
// widget.overlayController.toolbarMode !=
// MessageMode.noneSelected &&
// widget.overlayController.selectedToken == null)
// Positioned(
// top: max(
// ((_headerHeight + _centeredMessageTopOffset!) / 2) -
// (_tooltipSize!.height / 2),
// _headerHeight,
// ),
// child: Container(
// constraints: BoxConstraints(
// minWidth: 200.0,
// maxWidth: widget.overlayController.maxWidth,
// ),
// child: InstructionsInlineTooltip(
// instructionsEnum: widget.overlayController
// .toolbarMode.instructionsEnum ??
// InstructionsEnum.readingAssistanceOverview,
// bold: true,
// ),
// ),
// ),
// ],
// ),
// ),
// if (_showDetails)
// const SizedBox(
// width: FluffyThemes.columnWidth,
// ),
// ],
// ),
// ),
// ],
// );
}
}
class MessageReactionPicker extends StatelessWidget {
final ChatController chatController;
const MessageReactionPicker({
super.key,
required this.chatController,
});
@override
Widget build(BuildContext context) {
if (chatController.selectedEvents.length != 1) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
final sentReactions = <String>{};
final event = chatController.selectedEvents.first;
sentReactions.addAll(
event
.aggregatedEvents(
chatController.timeline!,
RelationshipTypes.reaction,
)
.where(
(event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction',
)
.map(
(event) => event.content
.tryGetMap<String, Object?>('m.relates_to')
?.tryGet<String>('key'),
)
.whereType<String>(),
);
return Material(
elevation: 4,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
shadowColor: theme.colorScheme.surface.withAlpha(128),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
...AppConfig.defaultReactions.map(
(emoji) => IconButton(
padding: EdgeInsets.zero,
icon: Center(
child: Opacity(
opacity: sentReactions.contains(
emoji,
)
? 0.33
: 1,
child: Text(
emoji,
style: const TextStyle(
fontSize: 20,
),
textAlign: TextAlign.center,
),
),
),
onPressed: sentReactions.contains(emoji)
? null
: () => event.room.sendReaction(
event.eventId,
emoji,
),
),
),
IconButton(
icon: const Icon(
Icons.add_reaction_outlined,
),
tooltip: L10n.of(context).customReaction,
onPressed: () async {
// final emoji = await showAdaptiveBottomSheet<String>(
// context: context,
// builder: (context) => Scaffold(
// appBar: AppBar(
// title: Text(
// L10n.of(context).customReaction,
// ),
// leading: CloseButton(
// onPressed: () => Navigator.of(
// context,
// ).pop(
// null,
// ),
// ),
// ),
// body: SizedBox(
// height: double.infinity,
// child: EmojiPicker(
// onEmojiSelected: (
// _,
// emoji,
// ) =>
// Navigator.of(
// context,
// ).pop(
// emoji.emoji,
// ),
// config: Config(
// emojiViewConfig: const EmojiViewConfig(
// backgroundColor: Colors.transparent,
// ),
// bottomActionBarConfig: const BottomActionBarConfig(
// enabled: false,
// ),
// categoryViewConfig: CategoryViewConfig(
// initCategory: Category.SMILEYS,
// backspaceColor: theme.colorScheme.primary,
// iconColor: theme.colorScheme.primary.withAlpha(
// 128,
// ),
// iconColorSelected: theme.colorScheme.primary,
// indicatorColor: theme.colorScheme.primary,
// backgroundColor: theme.colorScheme.surface,
// ),
// skinToneConfig: SkinToneConfig(
// dialogBackgroundColor: Color.lerp(
// theme.colorScheme.surface,
// theme.colorScheme.primaryContainer,
// 0.75,
// )!,
// indicatorColor: theme.colorScheme.onSurface,
// ),
// ),
// ),
// ),
// ),
// );
// if (emoji == null) {
// return;
// }
// if (sentReactions.contains(
// emoji,
// )) {
// return;
// }
// onSelect(event);
// await event.room.sendReaction(
// event.eventId,
// emoji,
// );
},
),
],
),
),
);
}
}

View file

@ -4,7 +4,6 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -15,7 +14,6 @@ class OverlayCenterContent extends StatelessWidget {
final Event event;
final Event? nextEvent;
final Event? prevEvent;
final PangeaMessageEvent? pangeaMessageEvent;
final MessageOverlayController overlayController;
final ChatController chatController;
@ -25,8 +23,6 @@ class OverlayCenterContent extends StatelessWidget {
final double? messageHeight;
final double? messageWidth;
final double maxWidth;
final double maxHeight;
final bool hasReactions;
@ -37,11 +33,8 @@ class OverlayCenterContent extends StatelessWidget {
required this.event,
required this.messageHeight,
required this.messageWidth,
required this.maxWidth,
required this.maxHeight,
required this.overlayController,
required this.chatController,
required this.pangeaMessageEvent,
required this.nextEvent,
required this.prevEvent,
required this.hasReactions,
@ -58,7 +51,7 @@ class OverlayCenterContent extends StatelessWidget {
ignoring: !isTransitionAnimation &&
readingAssistanceMode != ReadingAssistanceMode.practiceMode,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
constraints: BoxConstraints(maxWidth: overlayController.maxWidth),
child: Material(
type: MaterialType.transparency,
child: Column(
@ -76,7 +69,6 @@ class OverlayCenterContent extends StatelessWidget {
.key
: null,
event,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: chatController.choreographer.immersionMode,
controller: chatController,
overlayController: overlayController,
@ -93,7 +85,6 @@ class OverlayCenterContent extends StatelessWidget {
(sizeAnimation == null && isTransitionAnimation)
? messageHeight
: null,
maxHeight: maxHeight,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
),

View file

@ -1,6 +1,5 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -12,7 +11,6 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message_content.dart';
import 'package:fluffychat/pages/chat/events/reply_content.dart';
import 'package:fluffychat/pangea/bot/utils/bot_name.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';
@ -28,7 +26,6 @@ import 'package:fluffychat/widgets/matrix.dart';
// @ggurdin be great to explain the need/function of a widget like this
class OverlayMessage extends StatelessWidget {
final Event event;
final PangeaMessageEvent? pangeaMessageEvent;
final MessageOverlayController overlayController;
final ChatController controller;
final Event? nextEvent;
@ -39,7 +36,6 @@ class OverlayMessage extends StatelessWidget {
final Animation<Size>? sizeAnimation;
final double? messageWidth;
final double? messageHeight;
final double maxHeight;
final bool isTransitionAnimation;
final ReadingAssistanceMode? readingAssistanceMode;
@ -52,8 +48,6 @@ class OverlayMessage extends StatelessWidget {
required this.timeline,
required this.messageWidth,
required this.messageHeight,
required this.maxHeight,
this.pangeaMessageEvent,
this.nextEvent,
this.previousEvent,
this.sizeAnimation,
@ -146,7 +140,8 @@ class OverlayMessage extends StatelessWidget {
final showTranslation = overlayController.showTranslation &&
overlayController.translation != null;
final showTranscription = pangeaMessageEvent?.isAudioMessage == true;
final showTranscription =
overlayController.pangeaMessageEvent?.isAudioMessage == true;
final showSpeechTranslation = overlayController.showSpeechTranslation &&
overlayController.speechTranslation != null;
@ -284,115 +279,109 @@ class OverlayMessage extends StatelessWidget {
),
width: messageWidth,
height: messageHeight,
constraints: BoxConstraints(
maxHeight: maxHeight,
),
child: SingleChildScrollView(
dragStartBehavior: DragStartBehavior.down,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (event.relationshipType == RelationshipTypes.reply)
FutureBuilder<Event?>(
future: event.getReplyEvent(
timeline,
),
builder: (
BuildContext context,
snapshot,
) {
final replyEvent = snapshot.hasData
? snapshot.data!
: Event(
eventId: event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
senderId: "",
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
originServerTs: DateTime.now(),
);
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
),
child: Material(
color: Colors.transparent,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (event.relationshipType == RelationshipTypes.reply)
FutureBuilder<Event?>(
future: event.getReplyEvent(
timeline,
),
builder: (
BuildContext context,
snapshot,
) {
final replyEvent = snapshot.hasData
? snapshot.data!
: Event(
eventId: event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...',
},
senderId: "",
type: 'm.room.message',
room: event.room,
status: EventStatus.sent,
originServerTs: DateTime.now(),
);
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 8,
),
child: Material(
color: Colors.transparent,
borderRadius: ReplyContent.borderRadius,
child: InkWell(
borderRadius: ReplyContent.borderRadius,
child: InkWell(
borderRadius: ReplyContent.borderRadius,
onTap: () => controller.scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
onTap: () => controller.scrollToEventId(
replyEvent.eventId,
),
child: AbsorbPointer(
child: ReplyContent(
replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
),
),
);
},
),
MessageContent(
displayEvent,
textColor: textColor,
linkColor: linkColor,
borderRadius: borderRadius,
timeline: timeline,
pangeaMessageEvent: pangeaMessageEvent,
immersionMode: immersionMode,
overlayController: overlayController,
controller: controller,
nextEvent: nextEvent,
prevEvent: previousEvent,
isTransitionAnimation: isTransitionAnimation,
readingAssistanceMode: readingAssistanceMode,
selected: true,
),
);
},
),
if (event.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
))
Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4.0,
children: [
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
displayEvent.originServerTs.localizedTimeShort(
context,
),
style: TextStyle(
color: textColor.withAlpha(
164,
),
fontSize: 11,
),
),
],
),
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,
RelationshipTypes.edit,
))
Padding(
padding: const EdgeInsets.only(
bottom: 8.0,
left: 16.0,
right: 16.0,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 4.0,
children: [
Icon(
Icons.edit_outlined,
color: textColor.withAlpha(164),
size: 14,
),
Text(
displayEvent.originServerTs.localizedTimeShort(
context,
),
style: TextStyle(
color: textColor.withAlpha(
164,
),
fontSize: 11,
),
),
],
),
),
],
),
);
@ -404,9 +393,8 @@ class OverlayMessage extends StatelessWidget {
color: noBubble ? Colors.transparent : color,
borderRadius: borderRadius,
),
constraints: BoxConstraints(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5,
maxHeight: maxHeight,
),
child: SingleChildScrollView(
child: Column(

View file

@ -147,14 +147,13 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
),
),
constraints: BoxConstraints(
maxHeight: AppConfig.toolbarMaxHeight,
minWidth: min(
AppConfig.toolbarMinWidth,
widget.overlayController.maxWidth,
),
minHeight: AppConfig.toolbarMinHeight,
maxWidth: widget.overlayController.maxWidth,
),
height: AppConfig.toolbarMaxHeight,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,

View file

@ -11,12 +11,13 @@ import 'package:path_provider/path_provider.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/pressable_button.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/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/utils/report_message.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -45,13 +46,74 @@ enum SelectMode {
}
}
enum MessageActions {
reply,
forward,
edit,
delete,
copy,
download,
pin,
report,
info;
IconData get icon {
switch (this) {
case MessageActions.reply:
return Icons.reply_all;
case MessageActions.forward:
return Symbols.forward;
case MessageActions.edit:
return Symbols.edit;
case MessageActions.delete:
return Symbols.delete;
case MessageActions.copy:
return Icons.copy_outlined;
case MessageActions.download:
return Symbols.download;
case MessageActions.pin:
return Symbols.push_pin;
case MessageActions.report:
return Icons.shield_outlined;
case MessageActions.info:
return Icons.info_outlined;
}
}
String tooltip(BuildContext context) {
final l10n = L10n.of(context);
switch (this) {
case MessageActions.reply:
return l10n.reply;
case MessageActions.forward:
return l10n.forward;
case MessageActions.edit:
return l10n.edit;
case MessageActions.delete:
return l10n.redactMessage;
case MessageActions.copy:
return l10n.copy;
case MessageActions.download:
return l10n.download;
case MessageActions.pin:
return l10n.pinMessage;
case MessageActions.report:
return l10n.reportMessage;
case MessageActions.info:
return l10n.messageInfo;
}
}
}
class SelectModeButtons extends StatefulWidget {
final VoidCallback lauchPractice;
final MessageOverlayController overlayController;
final ChatController controller;
const SelectModeButtons({
required this.lauchPractice,
required this.overlayController,
required this.controller,
super.key,
});
@ -475,46 +537,172 @@ class SelectModeButtonsState extends State<SelectModeButtons> {
);
}
bool _messageActionEnabled(MessageActions action) {
if (messageEvent == null) return false;
switch (action) {
case MessageActions.reply:
return widget.controller.selectedEvents.length == 1 &&
widget.controller.room.canSendDefaultMessages;
case MessageActions.edit:
return widget.controller.canEditSelectedEvents &&
!widget.controller.selectedEvents.first.isActivityMessage;
case MessageActions.delete:
return widget.controller.canRedactSelectedEvents;
case MessageActions.copy:
return widget.controller.selectedEvents.length == 1 &&
widget.controller.selectedEvents.single.messageType ==
MessageTypes.Text;
case MessageActions.download:
return widget.controller.canSaveSelectedEvent;
case MessageActions.pin:
return widget.controller.canPinSelectedEvents;
case MessageActions.forward:
case MessageActions.report:
case MessageActions.info:
return widget.controller.selectedEvents.length == 1;
}
}
void _onActionPressed(MessageActions action) {
switch (action) {
case MessageActions.reply:
widget.controller.replyAction();
break;
case MessageActions.forward:
widget.controller.forwardEventsAction();
break;
case MessageActions.edit:
widget.controller.editSelectedEventAction();
break;
case MessageActions.delete:
widget.controller.redactEventsAction();
break;
case MessageActions.copy:
widget.controller.copyEventsAction();
break;
case MessageActions.download:
widget.controller.saveSelectedEvent(context);
break;
case MessageActions.pin:
widget.controller.pinEvent();
break;
case MessageActions.report:
final event = widget.controller.selectedEvents.first;
widget.controller.clearSelectedEvents();
reportEvent(
event,
widget.controller,
widget.controller.context,
);
break;
case MessageActions.info:
widget.controller.showEventInfo();
widget.controller.clearSelectedEvents();
break;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final modes = messageEvent?.isAudioMessage == true ? audioModes : textModes;
final actions = MessageActions.values.where(_messageActionEnabled);
return Container(
height: AppConfig.toolbarButtonsHeight,
alignment: Alignment.bottomCenter,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 4.0,
children: [
for (final mode in modes)
TooltipVisibility(
visible: (!_isError || mode != _selectedMode),
child: Tooltip(
message: mode.tooltip(context),
child: PressableButton(
depressed: mode == _selectedMode,
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.primaryContainer,
onPressed: () => _updateMode(mode),
playSound: mode != SelectMode.audio,
colorFactor: Theme.of(context).brightness == Brightness.light
? 0.55
: 0.3,
child: Container(
height: buttonSize,
width: buttonSize,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: icon(mode),
),
return Material(
type: MaterialType.transparency,
child: Container(
width: 250,
constraints: const BoxConstraints(
maxHeight: AppConfig.toolbarMenuHeight,
),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
child: ListView.builder(
shrinkWrap: true,
itemCount: modes.length + actions.length + 1,
itemBuilder: (context, index) {
if (index < modes.length) {
final mode = modes[index];
return SizedBox(
height: 50.0,
child: ListTile(
leading: Icon(mode.icon),
title: Text(mode.tooltip(context)),
onTap: () => _updateMode(mode),
),
),
),
],
);
} else if (index == modes.length) {
return const Divider(height: 1.0);
} else {
final action = actions.elementAt(index - modes.length - 1);
return SizedBox(
height: 50.0,
child: ListTile(
leading: Icon(action.icon),
title: Text(action.tooltip(context)),
onTap: () => _onActionPressed(action),
),
);
}
},
),
),
);
// return SizedBox(
// width: 150,
// child: ListView.builder(
// itemCount: modes.length,
// itemBuilder: (context, index) {
// final mode = modes[index];
// return ListTile(
// leading: Icon(mode.icon),
// title: Text(mode.name),
// onTap: () {
// _updateMode(mode);
// },
// );
// },
// ),
// );
// return Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisSize: MainAxisSize.min,
// spacing: 4.0,
// children: [
// for (final mode in modes)
// TooltipVisibility(
// visible: (!_isError || mode != _selectedMode),
// child: Tooltip(
// message: mode.tooltip(context),
// child: PressableButton(
// depressed: mode == _selectedMode,
// borderRadius: BorderRadius.circular(20),
// color: Theme.of(context).colorScheme.primaryContainer,
// onPressed: () => _updateMode(mode),
// playSound: mode != SelectMode.audio,
// colorFactor: Theme.of(context).brightness == Brightness.light
// ? 0.55
// : 0.3,
// child: Container(
// height: buttonSize,
// width: buttonSize,
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.primaryContainer,
// shape: BoxShape.circle,
// ),
// child: icon(mode),
// ),
// ),
// ),
// ),
// ],
// );
}
}