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:
ggurdin 2025-01-22 11:22:50 -05:00 committed by GitHub
parent cffc697df1
commit 58cfbdeac9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 533 additions and 428 deletions

View file

@ -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,

View file

@ -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 =

View file

@ -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';

View file

@ -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,
);
}
}

View 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,
),
],
),
),
);
}
}

View file

@ -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: [