scroll to center of screen if toolbar causes header overflow, scroll the overlay up if there's a footer overflow

This commit is contained in:
ggurdin 2024-09-11 12:14:23 -04:00
parent 2e139be658
commit 36537b0dc7
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
3 changed files with 153 additions and 121 deletions

View file

@ -22,6 +22,7 @@ abstract class AppConfig {
static const double messageFontSize = 15.75;
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
static const double toolbarMaxHeight = 315.0;
// #Pangea
// static const Color primaryColor = Color(0xFF5625BA);
// static const Color primaryColorLight = Color(0xFFCCBDEA);

View file

@ -1,3 +1,4 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/message.dart';
@ -105,9 +106,19 @@ class ChatEventList extends StatelessWidget {
// Request history button or progress indicator:
if (i == events.length + 1) {
if (controller.timeline!.isRequestingHistory) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
// #Pangea
// return const Center(
// child: CircularProgressIndicator.adaptive(strokeWidth: 2),
// );
return const Column(
children: [
SizedBox(height: AppConfig.toolbarMaxHeight),
Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
],
);
// Pangea#
}
if (controller.timeline!.canRequestHistory) {
return Builder(
@ -117,17 +128,31 @@ class ChatEventList extends StatelessWidget {
.addPostFrameCallback((_) => controller.requestHistory);
// WidgetsBinding.instance
// .addPostFrameCallback(controller.requestHistory);
// Pangea#
return Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
return Column(
children: [
const SizedBox(height: AppConfig.toolbarMaxHeight),
Center(
child: IconButton(
onPressed: controller.requestHistory,
icon: const Icon(Icons.refresh_outlined),
),
),
],
);
// return Center(
// child: IconButton(
// onPressed: controller.requestHistory,
// icon: const Icon(Icons.refresh_outlined),
// ),
// );
// Pangea#
},
);
}
return const SizedBox.shrink();
// #Pangea
// return const SizedBox.shrink();
return const SizedBox(height: AppConfig.toolbarMaxHeight);
// Pangea#
}
// #Pangea
// i--;

View file

@ -1,5 +1,4 @@
import 'dart:async';
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';
@ -18,6 +17,8 @@ import 'package:matrix/matrix.dart';
class MessageSelectionOverlay extends StatefulWidget {
final ChatController controller;
final Event event;
final Event? nextEvent;
final Event? prevEvent;
final PangeaMessageEvent pangeaMessageEvent;
final MessageMode? initialMode;
final MessageTextSelection textSelection;
@ -28,6 +29,8 @@ class MessageSelectionOverlay extends StatefulWidget {
required this.pangeaMessageEvent,
required this.textSelection,
this.initialMode,
this.nextEvent,
this.prevEvent,
super.key,
});
@ -35,83 +38,81 @@ class MessageSelectionOverlay extends StatefulWidget {
MessageSelectionOverlayState createState() => MessageSelectionOverlayState();
}
class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
double overlayBottomOffset = -1;
double adjustedOverlayBottomOffset = -1;
Size? messageSize;
Offset? messageOffset;
final StreamController _completeAnimationStream =
StreamController.broadcast();
class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
Animation<double>? _overlayPositionAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: FluffyThemes.animationDuration,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (messageSize == null || messageOffset == null) {
return;
}
// position the overlay directly over the underlying message
setOverlayBottomOffset();
final headerBottomOffset = screenHeight - headerHeight;
final footerBottomOffset = footerHeight;
final currentBottomOffset =
screenHeight - messageOffset!.dy - messageSize!.height;
// wait for the toolbar to animate to full height
_completeAnimationStream.stream.first.then((_) {
if (toolbarHeight == null ||
messageSize == null ||
messageOffset == null) {
return;
}
final bool hasHeaderOverflow =
messageOffset!.dy < AppConfig.toolbarMaxHeight;
final bool hasFooterOverflow = footerHeight > currentBottomOffset;
// Once the toolbar has fully expanded, adjust
// the overlay's position if there's an overflow
final overlayTopOffset = messageOffset!.dy - toolbarHeight!;
if (!hasHeaderOverflow && !hasFooterOverflow) return;
final bool hasHeaderOverflow = overlayTopOffset < headerHeight;
final bool hasFooterOverflow = overlayBottomOffset < footerHeight;
double scrollOffset = 0;
double animationEndOffset = 0;
if (hasHeaderOverflow) {
final overlayHeight = toolbarHeight! + messageSize!.height;
adjustedOverlayBottomOffset = screenHeight -
overlayHeight -
footerHeight -
MediaQuery.of(context).padding.bottom;
} else if (hasFooterOverflow) {
adjustedOverlayBottomOffset = footerHeight;
}
if (hasHeaderOverflow) {
final midpoint = (headerBottomOffset + footerBottomOffset) / 2;
animationEndOffset = midpoint - messageSize!.height;
scrollOffset = animationEndOffset - currentBottomOffset;
} else if (hasFooterOverflow) {
scrollOffset = footerHeight - currentBottomOffset;
animationEndOffset = currentBottomOffset + scrollOffset;
}
setState(() {});
});
_overlayPositionAnimation = Tween<double>(
begin: currentBottomOffset,
end: animationEndOffset,
).animate(
CurvedAnimation(
parent: _animationController,
curve: FluffyThemes.animationCurve,
),
);
widget.controller.scrollController.animateTo(
widget.controller.scrollController.offset - scrollOffset,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
);
_animationController.forward();
}
@override
void dispose() {
_completeAnimationStream.close();
_animationController.dispose();
super.dispose();
}
void setOverlayBottomOffset() {
// Try to get the offset and size of the original message bubble.
// If it fails, return an empty SizedBox. For instance, this can fail if
// you change the screen size while the overlay is open.
try {
final messageRenderBox = MatrixState.pAnyState.getRenderBox(
RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox(
widget.event.eventId,
);
if (messageRenderBox != null && messageRenderBox.hasSize) {
messageSize = messageRenderBox.size;
messageOffset = messageRenderBox.localToGlobal(Offset.zero);
final messageTopOffset = messageOffset!.dy;
overlayBottomOffset =
screenHeight - messageTopOffset - messageSize!.height;
}
} catch (err) {
overlayBottomOffset = adjustedOverlayBottomOffset = -1;
} finally {
setState(() {});
}
}
Size? get messageSize => messageRenderBox?.size;
Offset? get messageOffset => messageRenderBox?.localToGlobal(Offset.zero);
// height of the reply/forward bar + the reaction picker + contextual padding
double get footerHeight =>
@ -123,23 +124,14 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
double get screenHeight => MediaQuery.of(context).size.height;
double? get toolbarHeight {
try {
final toolbarRenderBox = MatrixState.pAnyState.getRenderBox(
'${widget.pangeaMessageEvent.eventId}-toolbar',
);
return toolbarRenderBox?.size.height;
} catch (e) {
return null;
}
}
@override
Widget build(BuildContext context) {
if (overlayBottomOffset == -1) {
return const SizedBox.shrink();
}
final bool showDetails = (Matrix.of(context)
.store
.getBool(SettingKeys.displayChatDetailsColumn) ??
false) &&
FluffyThemes.isThreeColumnMode(context) &&
widget.controller.room.membership == Membership.join;
final overlayMessage = ConstrainedBox(
constraints: const BoxConstraints(
@ -166,7 +158,6 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
pangeaMessageEvent: widget.pangeaMessageEvent,
controller: widget.controller,
textSelection: widget.textSelection,
completeAnimationStream: _completeAnimationStream,
initialMode: widget.initialMode,
),
),
@ -184,57 +175,72 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay> {
timeline: widget.controller.timeline!,
isOverlay: true,
animateIn: false,
nextEvent: widget.nextEvent,
previousEvent: widget.prevEvent,
),
],
),
),
);
final bool showDetails = (Matrix.of(context)
.store
.getBool(SettingKeys.displayChatDetailsColumn) ??
false) &&
FluffyThemes.isThreeColumnMode(context) &&
widget.controller.room.membership == Membership.join;
final positionedOverlayMessage = _overlayPositionAnimation == null
? Positioned(
left: 0,
right: showDetails ? FluffyThemes.columnWidth : 0,
bottom: screenHeight - messageOffset!.dy - messageSize!.height,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
)
: AnimatedBuilder(
animation: _overlayPositionAnimation!,
builder: (context, child) {
return Positioned(
left: 0,
right: showDetails ? FluffyThemes.columnWidth : 0,
bottom: _overlayPositionAnimation!.value,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
);
},
);
return Stack(
children: [
AnimatedPositioned(
duration: FluffyThemes.animationDuration,
left: 0,
right: showDetails ? FluffyThemes.columnWidth : 0,
bottom: adjustedOverlayBottomOffset == -1
? overlayBottomOffset
: adjustedOverlayBottomOffset,
child: Align(
alignment: Alignment.center,
child: overlayMessage,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
OverlayFooter(controller: widget.controller),
],
return Padding(
padding: EdgeInsets.only(
left: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0,
right: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0,
),
child: Stack(
children: [
positionedOverlayMessage,
Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
OverlayFooter(controller: widget.controller),
],
),
),
),
if (showDetails)
const SizedBox(
width: FluffyThemes.columnWidth,
),
],
if (showDetails)
const SizedBox(
width: FluffyThemes.columnWidth,
),
],
),
),
),
Material(
child: OverlayHeader(controller: widget.controller),
),
],
Material(
child: OverlayHeader(controller: widget.controller),
),
],
),
);
}
}