dev: move toolbar positioning logic into its own file and move some d… (#1519)
* dev: move toolbar positioning logic into its own file and move some dimension values into AppConfig * fix: dart format
This commit is contained in:
parent
cffc697df1
commit
58cfbdeac9
6 changed files with 533 additions and 428 deletions
|
|
@ -27,6 +27,9 @@ abstract class AppConfig {
|
|||
static const double toolbarMinHeight = 175.0;
|
||||
static const double toolbarMinWidth = 350.0;
|
||||
static const double toolbarButtonsHeight = 50.0;
|
||||
static const double defaultHeaderHeight = 56.0;
|
||||
static const double defaultFooterHeight = 48.0;
|
||||
static const double toolbarSpacing = 8.0;
|
||||
static TextStyle messageTextStyle(
|
||||
Event event,
|
||||
Color textColor,
|
||||
|
|
|
|||
|
|
@ -38,8 +38,10 @@ class ChatInputRow extends StatelessWidget {
|
|||
controller.emojiPickerType == EmojiPickerType.reaction) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
const height = 48.0;
|
||||
// #Pangea
|
||||
// const height = 48.0;
|
||||
const height = AppConfig.defaultFooterHeight;
|
||||
|
||||
final activel1 =
|
||||
controller.pangeaController.languageController.activeL1Model();
|
||||
final activel2 =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart';
|
||||
|
|
@ -9,10 +14,6 @@ import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../common/utils/error_handler.dart';
|
||||
import '../../common/utils/overlay.dart';
|
||||
import '../models/span_card_model.dart';
|
||||
|
|
|
|||
|
|
@ -5,13 +5,8 @@ import 'package:flutter/scheduler.dart';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
|
||||
import 'package:fluffychat/pangea/analytics/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
|
|
@ -20,14 +15,10 @@ import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
|
|||
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_buttons.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
final ChatController chatController;
|
||||
final Event _event;
|
||||
|
|
@ -56,10 +47,6 @@ class MessageSelectionOverlay extends StatefulWidget {
|
|||
|
||||
class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
StreamSubscription? _reactionSubscription;
|
||||
Animation<double>? _overlayPositionAnimation;
|
||||
|
||||
MessageMode toolbarMode = MessageMode.noneSelected;
|
||||
PangeaTokenText? _selectedSpan;
|
||||
List<PangeaTokenText>? _highlightedTokens;
|
||||
|
|
@ -107,9 +94,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_initializeTokensAndMode();
|
||||
_setupSubscriptions();
|
||||
}
|
||||
|
||||
void _updateSelectedSpan(PangeaTokenText selectedSpan) {
|
||||
|
|
@ -151,34 +136,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
void _setupSubscriptions() {
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
);
|
||||
|
||||
_reactionSubscription =
|
||||
widget.chatController.room.client.onSync.stream.where(
|
||||
(update) {
|
||||
// check if this sync update has a reaction event or a
|
||||
// redaction (of a reaction event). If so, rebuild the overlay
|
||||
final room = widget.chatController.room;
|
||||
final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events;
|
||||
if (timelineEvents == null) return false;
|
||||
|
||||
final eventID = widget._event.eventId;
|
||||
return timelineEvents.any(
|
||||
(e) =>
|
||||
e.type == EventTypes.Redaction ||
|
||||
(e.type == EventTypes.Reaction &&
|
||||
Event.fromMatrixEvent(e, room).relationshipEventId ==
|
||||
eventID),
|
||||
);
|
||||
},
|
||||
).listen((_) => setState(() {}));
|
||||
}
|
||||
|
||||
MessageAnalyticsEntry? get messageAnalyticsEntry =>
|
||||
pangeaMessageEvent != null && tokens != null
|
||||
? MatrixState.pangeaController.getAnalytics.perMessage.get(
|
||||
|
|
@ -331,23 +288,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
return;
|
||||
}
|
||||
|
||||
// if there's no selected span, then select the token
|
||||
// PangeaTokenText? newSelectedSpan;
|
||||
// if (_selectedSpan == null) {
|
||||
// newSelectedSpan = token.text;
|
||||
// } else {
|
||||
// // if there is a selected span, then deselect the token if it's the same
|
||||
// if (isTokenSelected(token)) {
|
||||
// newSelectedSpan = null;
|
||||
// } else {
|
||||
// // if there is a selected span but it is not the same, then select the token
|
||||
// newSelectedSpan = token.text;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (newSelectedSpan != null) {
|
||||
// updateToolbarMode(MessageMode.practiceActivity);
|
||||
// }
|
||||
_updateSelectedSpan(token.text);
|
||||
setState(() {});
|
||||
}
|
||||
|
|
@ -373,374 +313,22 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
PangeaTokenText? get selectedSpan => _selectedSpan;
|
||||
|
||||
bool get _hasReactions {
|
||||
final reactionsEvents = widget._event.aggregatedEvents(
|
||||
widget.chatController.timeline!,
|
||||
RelationshipTypes.reaction,
|
||||
);
|
||||
return reactionsEvents.where((e) => !e.redacted).isNotEmpty;
|
||||
}
|
||||
|
||||
double get _toolbarButtonsHeight =>
|
||||
showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0;
|
||||
double get _reactionsHeight => _hasReactions ? 28 : 0;
|
||||
double get _belowMessageHeight => _toolbarButtonsHeight + _reactionsHeight;
|
||||
double get _totalMessageHeight => _messageHeight + _belowMessageHeight;
|
||||
double get _messageHeight => _adjustedMessageHeight != null &&
|
||||
_adjustedMessageHeight! < _messageSize!.height
|
||||
? _adjustedMessageHeight!
|
||||
: _messageSize!.height;
|
||||
|
||||
void setIsPlayingAudio(bool isPlaying) {
|
||||
if (mounted) {
|
||||
setState(() => isPlayingAudio = isPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_messageSize == null ||
|
||||
_messageOffset == null ||
|
||||
_screenHeight == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// position the overlay directly over the underlying message
|
||||
final headerBottomOffset = _screenHeight! - _headerHeight;
|
||||
final footerBottomOffset = _footerHeight;
|
||||
final currentBottomOffset = _screenHeight! -
|
||||
_messageOffset!.dy -
|
||||
_messageHeight -
|
||||
_belowMessageHeight;
|
||||
|
||||
final bool hasHeaderOverflow =
|
||||
_messageOffset!.dy < (AppConfig.toolbarMaxHeight + _headerHeight + 10);
|
||||
final bool hasFooterOverflow = (_footerHeight + 5) > currentBottomOffset;
|
||||
|
||||
if (!hasHeaderOverflow && !hasFooterOverflow) return;
|
||||
|
||||
double scrollOffset = 0;
|
||||
double animationEndOffset = 0;
|
||||
|
||||
final midpoint = (headerBottomOffset + footerBottomOffset) / 2;
|
||||
|
||||
// if the overlay would have a footer overflow for this message,
|
||||
// check if shifting the overlay up could cause a header overflow
|
||||
final bottomOffsetDifference = _footerHeight - currentBottomOffset;
|
||||
final newTopOffset =
|
||||
_messageOffset!.dy - bottomOffsetDifference - _belowMessageHeight;
|
||||
final bool upshiftCausesHeaderOverflow = hasFooterOverflow &&
|
||||
newTopOffset < (_headerHeight + AppConfig.toolbarMaxHeight);
|
||||
|
||||
if (hasHeaderOverflow || upshiftCausesHeaderOverflow) {
|
||||
animationEndOffset = midpoint - _totalMessageHeight;
|
||||
final totalTopOffset = midpoint + AppConfig.toolbarMaxHeight;
|
||||
final remainingSpace = _screenHeight! - totalTopOffset;
|
||||
if (remainingSpace < _headerHeight) {
|
||||
// the overlay could run over the header, so it needs to be shifted down
|
||||
animationEndOffset -= (_headerHeight - remainingSpace + 10);
|
||||
}
|
||||
scrollOffset = animationEndOffset - currentBottomOffset;
|
||||
} else if (hasFooterOverflow) {
|
||||
scrollOffset = (_footerHeight + 5) - currentBottomOffset;
|
||||
animationEndOffset = (_footerHeight + 5);
|
||||
}
|
||||
|
||||
// If, after ajusting the overlay position, the message still overflows the footer,
|
||||
// update the message height to fit the screen. The message is scrollable, so
|
||||
// this will make the both the toolbar box and the toolbar buttons visible.
|
||||
if (animationEndOffset < _footerHeight + _belowMessageHeight) {
|
||||
final double remainingSpace = _screenHeight! -
|
||||
AppConfig.toolbarMaxHeight -
|
||||
_headerHeight -
|
||||
_footerHeight -
|
||||
_belowMessageHeight;
|
||||
|
||||
if (remainingSpace < _messageHeight) {
|
||||
_adjustedMessageHeight = remainingSpace;
|
||||
}
|
||||
|
||||
animationEndOffset = _footerHeight;
|
||||
}
|
||||
|
||||
_overlayPositionAnimation = Tween<double>(
|
||||
begin: currentBottomOffset,
|
||||
end: animationEndOffset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
|
||||
widget.chatController.scrollController.animateTo(
|
||||
widget.chatController.scrollController.offset - scrollOffset,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
RenderBox? get _messageRenderBox {
|
||||
try {
|
||||
return MatrixState.pAnyState.getRenderBox(
|
||||
widget._event.eventId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: "Error getting message render box: $e",
|
||||
s: s,
|
||||
data: {
|
||||
"eventID": widget._event.eventId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Size? get _messageSize {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return _messageRenderBox?.size;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: "Error getting message size: $e",
|
||||
s: s,
|
||||
data: {},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Offset? get _messageOffset {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return _messageRenderBox?.localToGlobal(Offset.zero);
|
||||
} catch (e) {
|
||||
Sentry.addBreadcrumb(Breadcrumb(message: "Error getting message offset"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double? _adjustedMessageHeight;
|
||||
|
||||
// height of the reply/forward bar + the reaction picker + contextual padding
|
||||
double get _footerHeight {
|
||||
return 56 +
|
||||
16 +
|
||||
(FluffyThemes.isColumnMode(context) ? 16.0 : 8.0) +
|
||||
(_mediaQuery?.padding.bottom ?? 0);
|
||||
}
|
||||
|
||||
MediaQueryData? get _mediaQuery {
|
||||
try {
|
||||
return MediaQuery.of(context);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: "Error getting media query: $e",
|
||||
s: s,
|
||||
data: {},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
double get _headerHeight {
|
||||
return (Theme.of(context).appBarTheme.toolbarHeight ?? 56) +
|
||||
(_mediaQuery?.padding.top ?? 0);
|
||||
}
|
||||
|
||||
double? get _screenHeight => _mediaQuery?.size.height;
|
||||
|
||||
double? get _screenWidth => _mediaQuery?.size.width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_messageSize == null) return const SizedBox.shrink();
|
||||
|
||||
final bool showDetails = (Matrix.of(context)
|
||||
.store
|
||||
.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false) &&
|
||||
FluffyThemes.isThreeColumnMode(context) &&
|
||||
widget.chatController.room.membership == Membership.join;
|
||||
|
||||
// the default spacing between the side of the screen and the message bubble
|
||||
const double messageMargin = Avatar.defaultSize + 16 + 8;
|
||||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin;
|
||||
double? maxWidth;
|
||||
if (_screenWidth != null) {
|
||||
final chatViewWidth = _screenWidth! -
|
||||
(FluffyThemes.isColumnMode(context)
|
||||
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
|
||||
: 0);
|
||||
maxWidth = chatViewWidth - (2 * horizontalPadding) - messageMargin;
|
||||
}
|
||||
if (maxWidth == null || maxWidth > totalMaxWidth) {
|
||||
maxWidth = totalMaxWidth;
|
||||
}
|
||||
|
||||
final overlayMessage = Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
widget._event.senderId == widget._event.room.client.userID
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent != null)
|
||||
MessageToolbar(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: this,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: _adjustedMessageHeight,
|
||||
child: OverlayMessage(
|
||||
widget._event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode:
|
||||
widget.chatController.choreographer.immersionMode,
|
||||
controller: widget.chatController,
|
||||
overlayController: this,
|
||||
nextEvent: widget._nextEvent,
|
||||
prevEvent: widget._prevEvent,
|
||||
timeline: widget.chatController.timeline!,
|
||||
messageWidth: _messageSize!.width,
|
||||
messageHeight: _messageHeight,
|
||||
),
|
||||
),
|
||||
if (_hasReactions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: _reactionsHeight - 8,
|
||||
child: MessageReactions(
|
||||
widget._event,
|
||||
widget.chatController.timeline!,
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolbarButtons(
|
||||
event: widget._event,
|
||||
overlayController: this,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final columnOffset = FluffyThemes.isColumnMode(context)
|
||||
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
|
||||
: 0;
|
||||
|
||||
final double? leftPadding =
|
||||
(widget._event.senderId == widget._event.room.client.userID ||
|
||||
_messageOffset == null)
|
||||
? null
|
||||
: _messageOffset!.dx - horizontalPadding - columnOffset;
|
||||
|
||||
final double? rightPadding =
|
||||
(widget._event.senderId == widget._event.room.client.userID &&
|
||||
_screenWidth != null &&
|
||||
_messageOffset != null &&
|
||||
_messageSize != null)
|
||||
? _screenWidth! -
|
||||
_messageOffset!.dx -
|
||||
_messageSize!.width -
|
||||
horizontalPadding
|
||||
: null;
|
||||
|
||||
final positionedOverlayMessage = (_overlayPositionAnimation == null)
|
||||
? (_screenHeight == null ||
|
||||
_messageSize == null ||
|
||||
_messageOffset == null)
|
||||
? const SizedBox.shrink()
|
||||
: Positioned(
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
bottom: _screenHeight! -
|
||||
_messageOffset!.dy -
|
||||
_messageHeight -
|
||||
_belowMessageHeight,
|
||||
child: overlayMessage,
|
||||
)
|
||||
: AnimatedBuilder(
|
||||
animation: _overlayPositionAnimation!,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
bottom: _overlayPositionAnimation!.value,
|
||||
child: overlayMessage,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
positionedOverlayMessage,
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(
|
||||
controller: widget.chatController,
|
||||
overlayController: this,
|
||||
),
|
||||
SizedBox(height: _mediaQuery?.padding.bottom ?? 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showDetails)
|
||||
const SizedBox(
|
||||
width: FluffyThemes.columnWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: _mediaQuery?.padding.top ?? 0),
|
||||
OverlayHeader(controller: widget.chatController),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return MessageSelectionPositioner(
|
||||
overlayController: this,
|
||||
chatController: widget.chatController,
|
||||
event: widget._event,
|
||||
nextEvent: widget._nextEvent,
|
||||
prevEvent: widget._prevEvent,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
initialSelectedToken: widget._initialSelectedToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
510
lib/pangea/toolbar/widgets/message_selection_positioner.dart
Normal file
510
lib/pangea/toolbar/widgets/message_selection_positioner.dart
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_reactions.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/widgets/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/message_toolbar_buttons.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/widgets/overlay_message.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Controls positioning of the message overlay.
|
||||
class MessageSelectionPositioner extends StatefulWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
final ChatController chatController;
|
||||
final Event event;
|
||||
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final PangeaToken? initialSelectedToken;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
||||
const MessageSelectionPositioner({
|
||||
required this.overlayController,
|
||||
required this.chatController,
|
||||
required this.event,
|
||||
this.pangeaMessageEvent,
|
||||
this.initialSelectedToken,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageSelectionPositionerState createState() =>
|
||||
MessageSelectionPositionerState();
|
||||
}
|
||||
|
||||
class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
Animation<double>? _overlayPositionAnimation;
|
||||
|
||||
StreamSubscription? _reactionSubscription;
|
||||
double? _adjustedMessageHeight;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
);
|
||||
|
||||
_reactionSubscription =
|
||||
widget.chatController.room.client.onSync.stream.where(
|
||||
(update) {
|
||||
// check if this sync update has a reaction event or a
|
||||
// redaction (of a reaction event). If so, rebuild the overlay
|
||||
final room = widget.chatController.room;
|
||||
final timelineEvents = update.rooms?.join?[room.id]?.timeline?.events;
|
||||
if (timelineEvents == null) return false;
|
||||
|
||||
final eventID = widget.event.eventId;
|
||||
return timelineEvents.any(
|
||||
(e) =>
|
||||
e.type == EventTypes.Redaction ||
|
||||
(e.type == EventTypes.Reaction &&
|
||||
Event.fromMatrixEvent(e, room).relationshipEventId ==
|
||||
eventID),
|
||||
);
|
||||
},
|
||||
).listen((_) => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_reactionSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
dynamic _runWithLogging(
|
||||
Function runner,
|
||||
String errorMessage,
|
||||
) {
|
||||
try {
|
||||
return runner();
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: "$errorMessage: $e",
|
||||
s: s,
|
||||
data: {
|
||||
"eventID": widget.event.eventId,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get showDetails =>
|
||||
(Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false) &&
|
||||
FluffyThemes.isThreeColumnMode(context) &&
|
||||
widget.chatController.room.membership == Membership.join;
|
||||
|
||||
// screen size
|
||||
|
||||
MediaQueryData? get _mediaQuery => _runWithLogging(
|
||||
() => MediaQuery.of(context),
|
||||
"Error getting media query",
|
||||
);
|
||||
|
||||
double get _columnWidth => FluffyThemes.isColumnMode(context)
|
||||
? (FluffyThemes.columnWidth + FluffyThemes.navRailWidth)
|
||||
: 0;
|
||||
|
||||
// message size
|
||||
|
||||
RenderBox? get _messageRenderBox => _runWithLogging(
|
||||
() => MatrixState.pAnyState.getRenderBox(
|
||||
widget.event.eventId,
|
||||
),
|
||||
"Error getting message render box",
|
||||
);
|
||||
|
||||
Size? get _messageSize {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runWithLogging(
|
||||
() => _messageRenderBox?.size,
|
||||
"Error getting message size",
|
||||
);
|
||||
}
|
||||
|
||||
double get _messageHeight =>
|
||||
_adjustedMessageHeight ?? _messageSize?.height ?? 0;
|
||||
|
||||
double get _messageMaxWidth {
|
||||
const double messageMargin = Avatar.defaultSize + 16 + 8;
|
||||
const totalMaxWidth = (FluffyThemes.columnWidth * 2.5) - messageMargin;
|
||||
double? maxWidth;
|
||||
|
||||
if (_mediaQuery != null) {
|
||||
final chatViewWidth = _mediaQuery!.size.width - _columnWidth;
|
||||
maxWidth = chatViewWidth - (2 * _horizontalPadding) - messageMargin;
|
||||
}
|
||||
|
||||
if (maxWidth == null || maxWidth > totalMaxWidth) {
|
||||
maxWidth = totalMaxWidth;
|
||||
}
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
// message offset
|
||||
|
||||
Offset? get _messageOffset {
|
||||
if (_messageRenderBox == null || !_messageRenderBox!.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _runWithLogging(
|
||||
() => _messageRenderBox?.localToGlobal(Offset.zero),
|
||||
"Error getting message offset",
|
||||
);
|
||||
}
|
||||
|
||||
double? get _messageTopOffset {
|
||||
if (_messageOffset == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _messageOffset!.dy -
|
||||
(_mediaQuery?.padding.top ?? 0) +
|
||||
(_mediaQuery?.viewPadding.top ?? 0);
|
||||
}
|
||||
|
||||
double? get _messageBottomOffset {
|
||||
if (_messageOffset == null || _messageSize == null || _mediaQuery == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _mediaQuery!.size.height - _messageOffset!.dy - _messageHeight;
|
||||
}
|
||||
|
||||
double? get _messageLeftOffset {
|
||||
if (_messageOffset == null ||
|
||||
widget.event.senderId == widget.event.room.client.userID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _messageOffset!.dx - _columnWidth - _horizontalPadding;
|
||||
}
|
||||
|
||||
double? get _messageRightOffset {
|
||||
if (_messageOffset == null ||
|
||||
_mediaQuery == null ||
|
||||
_messageSize == null ||
|
||||
widget.event.senderId != widget.event.room.client.userID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _mediaQuery!.size.width -
|
||||
_messageOffset!.dx -
|
||||
_messageSize!.width -
|
||||
_horizontalPadding;
|
||||
}
|
||||
|
||||
// measurements for items around the toolbar
|
||||
|
||||
double get _horizontalPadding =>
|
||||
FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
|
||||
double get _headerHeight {
|
||||
return (Theme.of(context).appBarTheme.toolbarHeight ??
|
||||
AppConfig.defaultHeaderHeight) +
|
||||
(_mediaQuery?.padding.top ?? 0);
|
||||
}
|
||||
|
||||
double get _footerHeight {
|
||||
return AppConfig.defaultFooterHeight +
|
||||
16 +
|
||||
(FluffyThemes.isColumnMode(context) ? 16.0 : 8.0) +
|
||||
(_mediaQuery?.padding.bottom ?? 0);
|
||||
}
|
||||
|
||||
double? get _totalVerticalSpace {
|
||||
if (_mediaQuery == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _mediaQuery!.size.height - _headerHeight - _footerHeight;
|
||||
}
|
||||
|
||||
// measurement for items in the toolbar
|
||||
|
||||
bool get showToolbarButtons =>
|
||||
widget.pangeaMessageEvent != null &&
|
||||
widget.pangeaMessageEvent!.event.messageType == MessageTypes.Text;
|
||||
|
||||
double get _toolbarButtonsHeight =>
|
||||
showToolbarButtons ? AppConfig.toolbarButtonsHeight : 0;
|
||||
|
||||
bool get _hasReactions {
|
||||
final reactionsEvents = widget.event.aggregatedEvents(
|
||||
widget.chatController.timeline!,
|
||||
RelationshipTypes.reaction,
|
||||
);
|
||||
return reactionsEvents.where((e) => !e.redacted).isNotEmpty;
|
||||
}
|
||||
|
||||
double get _reactionsHeight => _hasReactions ? 28 : 0;
|
||||
|
||||
double get _maxTotalToolbarHeight =>
|
||||
_toolbarButtonsHeight +
|
||||
_reactionsHeight +
|
||||
_messageHeight +
|
||||
AppConfig.toolbarSpacing +
|
||||
AppConfig.toolbarMaxHeight;
|
||||
|
||||
double get _totalToolbarTopOffset =>
|
||||
(_messageTopOffset ?? 0) -
|
||||
(AppConfig.toolbarSpacing + AppConfig.toolbarMaxHeight);
|
||||
|
||||
double get _totalToolbarBottomOffset =>
|
||||
(_messageBottomOffset ?? 0) - (_toolbarButtonsHeight + _reactionsHeight);
|
||||
|
||||
/// The remaining space between the top of the screen and the top of the toolbar.
|
||||
/// Negative if the toolbar is overflowing the top of the screen.
|
||||
double get _remainingTopSpace => _totalToolbarTopOffset - _headerHeight;
|
||||
|
||||
/// The remaining space between the bottom of the screen and the bottom of the toolbar.
|
||||
/// Negative if the toolbar is overflowing the bottom of the screen.
|
||||
double get _remainingBottomSpace => _totalToolbarBottomOffset - _footerHeight;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_messageSize == null || _messageOffset == null || _mediaQuery == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool hasHeaderOverflow =
|
||||
_remainingTopSpace < AppConfig.toolbarSpacing;
|
||||
final bool hasFooterOverflow =
|
||||
_remainingBottomSpace < AppConfig.toolbarSpacing;
|
||||
|
||||
if (!hasHeaderOverflow && !hasFooterOverflow || _mediaQuery == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double adjustedBottomOffset = _totalToolbarBottomOffset;
|
||||
double scrollOffset = 0;
|
||||
|
||||
// if the message height is too tall to fit, adjust the message height
|
||||
if (_totalVerticalSpace! < _maxTotalToolbarHeight) {
|
||||
_adjustedMessageHeight = _totalVerticalSpace! -
|
||||
// one for within the toolbar itself, one for the top, and one for the bottom
|
||||
((AppConfig.toolbarSpacing * 3) +
|
||||
_reactionsHeight +
|
||||
_toolbarButtonsHeight +
|
||||
AppConfig.toolbarMaxHeight);
|
||||
}
|
||||
|
||||
// if the overlay could have header overflow if the message wasn't shifted, we want to shift
|
||||
// it down so the bottom to give it enough space.
|
||||
if (hasHeaderOverflow) {
|
||||
// what is the distance between the current top offset of the toolbar and the desired top offset?
|
||||
final double currentTopOffset =
|
||||
(_messageTopOffset ?? 0) - AppConfig.toolbarMaxHeight;
|
||||
final double neededShift =
|
||||
(_headerHeight - currentTopOffset) + AppConfig.toolbarSpacing;
|
||||
adjustedBottomOffset = _totalToolbarBottomOffset - neededShift;
|
||||
} else if (hasFooterOverflow) {
|
||||
adjustedBottomOffset = _footerHeight + AppConfig.toolbarSpacing;
|
||||
}
|
||||
|
||||
scrollOffset = adjustedBottomOffset - _totalToolbarBottomOffset;
|
||||
|
||||
_overlayPositionAnimation = Tween<double>(
|
||||
begin: _totalToolbarBottomOffset,
|
||||
end: adjustedBottomOffset,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
),
|
||||
);
|
||||
|
||||
widget.chatController.scrollController.animateTo(
|
||||
widget.chatController.scrollController.offset - scrollOffset,
|
||||
duration:
|
||||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_messageSize == null) return const SizedBox.shrink();
|
||||
|
||||
final positionedOverlayMessage = AnimatedBuilder(
|
||||
animation: _overlayPositionAnimation ?? _animationController,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: _messageLeftOffset,
|
||||
right: _messageRightOffset,
|
||||
bottom: _overlayPositionAnimation?.value ?? _totalToolbarBottomOffset,
|
||||
child: ToolbarOverlay(
|
||||
messageHeight: _messageHeight,
|
||||
messageWidth: _messageSize!.width,
|
||||
maxWidth: _messageMaxWidth,
|
||||
event: widget.event,
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
nextEvent: widget.nextEvent,
|
||||
prevEvent: widget.prevEvent,
|
||||
overlayController: widget.overlayController,
|
||||
chatController: widget.chatController,
|
||||
hasReactions: _hasReactions,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: _horizontalPadding,
|
||||
right: _horizontalPadding,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
positionedOverlayMessage,
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(
|
||||
controller: widget.chatController,
|
||||
overlayController: widget.overlayController,
|
||||
),
|
||||
SizedBox(height: _mediaQuery?.padding.bottom ?? 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showDetails)
|
||||
const SizedBox(
|
||||
width: FluffyThemes.columnWidth,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: _mediaQuery?.padding.top ?? 0),
|
||||
OverlayHeader(controller: widget.chatController),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarOverlay extends StatelessWidget {
|
||||
final double messageHeight;
|
||||
final double messageWidth;
|
||||
final double maxWidth;
|
||||
|
||||
final Event event;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
||||
final bool hasReactions;
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final ChatController chatController;
|
||||
|
||||
const ToolbarOverlay({
|
||||
super.key,
|
||||
required this.messageHeight,
|
||||
required this.messageWidth,
|
||||
required this.maxWidth,
|
||||
required this.event,
|
||||
required this.overlayController,
|
||||
required this.chatController,
|
||||
required this.hasReactions,
|
||||
this.pangeaMessageEvent,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: event.senderId == event.room.client.userID
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (pangeaMessageEvent != null)
|
||||
MessageToolbar(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
const SizedBox(height: AppConfig.toolbarSpacing),
|
||||
SizedBox(
|
||||
height: messageHeight,
|
||||
child: OverlayMessage(
|
||||
event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: chatController.choreographer.immersionMode,
|
||||
controller: chatController,
|
||||
overlayController: overlayController,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
timeline: chatController.timeline!,
|
||||
messageWidth: messageWidth,
|
||||
messageHeight: messageHeight,
|
||||
),
|
||||
),
|
||||
if (hasReactions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: MessageReactions(
|
||||
event,
|
||||
chatController.timeline!,
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolbarButtons(
|
||||
event: event,
|
||||
overlayController: overlayController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,8 @@ class OverlayHeader extends StatelessWidget {
|
|||
color: Theme.of(context).appBarTheme.backgroundColor ??
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
height: Theme.of(context).appBarTheme.toolbarHeight ?? 56,
|
||||
height: Theme.of(context).appBarTheme.toolbarHeight ??
|
||||
AppConfig.defaultHeaderHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue