Toolbar placed over selected message
This commit is contained in:
parent
a911b9f852
commit
bb263c71c2
3 changed files with 199 additions and 142 deletions
|
|
@ -1,26 +1,99 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageSelectionOverlay extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final ToolbarDisplayController toolbarController;
|
||||
final Function closeToolbar;
|
||||
final Widget toolbar;
|
||||
final Widget overlayMessage;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final bool ownMessage;
|
||||
final bool immersionMode;
|
||||
final String targetId;
|
||||
|
||||
const MessageSelectionOverlay({
|
||||
required this.controller,
|
||||
required this.closeToolbar,
|
||||
required this.toolbar,
|
||||
required this.overlayMessage,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.immersionMode,
|
||||
required this.ownMessage,
|
||||
required this.targetId,
|
||||
required this.toolbarController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
final targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext?.findRenderObject();
|
||||
|
||||
double center = 290;
|
||||
double left = 0;
|
||||
bool showDown = false;
|
||||
final double footerSize = PlatformInfos.isMobile
|
||||
? PlatformInfos.isIOS
|
||||
? 138
|
||||
: 140
|
||||
: 154;
|
||||
final double headerSize = PlatformInfos.isMobile
|
||||
? PlatformInfos.isIOS
|
||||
? 122
|
||||
: 86
|
||||
: 80;
|
||||
final double stackSize =
|
||||
MediaQuery.of(context).size.height - footerSize - headerSize;
|
||||
|
||||
if (targetRenderBox != null) {
|
||||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
|
||||
left = targetOffset.dx;
|
||||
showDown = targetOffset.dy + transformTargetSize.height <=
|
||||
headerSize + stackSize / 2;
|
||||
|
||||
center = targetOffset.dy -
|
||||
headerSize +
|
||||
(showDown ? transformTargetSize.height + 3 : (-3));
|
||||
// If top of selected message extends below header
|
||||
if (targetOffset.dy <= headerSize) {
|
||||
center = transformTargetSize.height + 3;
|
||||
showDown = true;
|
||||
}
|
||||
// If bottom of selected message extends below footer
|
||||
else if (targetOffset.dy + transformTargetSize.height >=
|
||||
headerSize + stackSize) {
|
||||
center = stackSize - transformTargetSize.height - 3;
|
||||
}
|
||||
// If message is too long, or awkwardly positioned,
|
||||
// center to avoid hitting edges of stack
|
||||
if (transformTargetSize.height >= stackSize / 2 - 3 ||
|
||||
(targetOffset.dy < headerSize + stackSize / 2 &&
|
||||
targetOffset.dy + transformTargetSize.height >
|
||||
headerSize + stackSize / 2)) {
|
||||
center = stackSize / 2;
|
||||
}
|
||||
}
|
||||
|
||||
final Widget overlayMessage = OverlayMessage(
|
||||
pangeaMessageEvent.event,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
toolbarController: toolbarController,
|
||||
width: 290,
|
||||
showDown: showDown,
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
|
@ -35,14 +108,28 @@ class MessageSelectionOverlay extends StatelessWidget {
|
|||
height: 7,
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
child: Stack(
|
||||
children: [
|
||||
toolbar,
|
||||
const SizedBox(height: 9),
|
||||
overlayMessage,
|
||||
Positioned(
|
||||
left: ownMessage ? null : left,
|
||||
right: ownMessage
|
||||
? PlatformInfos.isMobile
|
||||
? 8
|
||||
: 16
|
||||
: null,
|
||||
bottom: stackSize - center + 3,
|
||||
child: showDown ? overlayMessage : toolbar,
|
||||
),
|
||||
Positioned(
|
||||
left: ownMessage ? null : left,
|
||||
right: ownMessage
|
||||
? PlatformInfos.isMobile
|
||||
? 8
|
||||
: 16
|
||||
: null,
|
||||
top: center + 3,
|
||||
child: showDown ? toolbar : overlayMessage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
|
|
@ -14,10 +13,10 @@ import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart'
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -75,47 +74,6 @@ class ToolbarDisplayController {
|
|||
}
|
||||
focusNode.requestFocus();
|
||||
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
final targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext?.findRenderObject();
|
||||
if (targetRenderBox != null) {
|
||||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
messageWidth = transformTargetSize.width;
|
||||
final Offset targetOffset = (targetRenderBox).localToGlobal(Offset.zero);
|
||||
|
||||
final double minValue =
|
||||
controller.scrollController.position.minScrollExtent;
|
||||
final double maxValue =
|
||||
controller.scrollController.position.maxScrollExtent;
|
||||
final double middlePoint = controller.scrollController.offset -
|
||||
targetOffset.dy +
|
||||
MediaQuery.of(context).size.height / 2 +
|
||||
37;
|
||||
|
||||
// Scroll so message is right under half point of screen
|
||||
controller.scrollController.animateTo(
|
||||
middlePoint < minValue
|
||||
? minValue
|
||||
: middlePoint > maxValue
|
||||
? maxValue
|
||||
: middlePoint,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
}
|
||||
|
||||
final Widget overlayMessage = OverlayMessage(
|
||||
pangeaMessageEvent.event,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
toolbarController: this,
|
||||
width: 300,
|
||||
nextEvent: nextEvent,
|
||||
previousEvent: previousEvent,
|
||||
);
|
||||
|
||||
// I'm not sure why I put this here, but it causes the toolbar
|
||||
// not to open immediately after clicking (user has to scroll or move their cursor)
|
||||
// so I'm commenting it out for now
|
||||
|
|
@ -127,8 +85,11 @@ class ToolbarDisplayController {
|
|||
controller: controller,
|
||||
closeToolbar: closeToolbar,
|
||||
toolbar: toolbar!,
|
||||
overlayMessage: overlayMessage,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
targetId: targetId,
|
||||
toolbarController: this,
|
||||
);
|
||||
} catch (err) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -372,6 +333,10 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double maxHeight = (MediaQuery.of(context).size.height -
|
||||
(PlatformInfos.isIOS ? 254 : 232)) /
|
||||
2;
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
|
|
@ -386,18 +351,18 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
Radius.circular(25),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
maxHeight: 300,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 290,
|
||||
minWidth: 290,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 300,
|
||||
maxHeight: 228,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 290,
|
||||
maxHeight: maxHeight - 72,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/enum/use_type.dart';
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -11,8 +12,6 @@ import '../../../config/app_config.dart';
|
|||
|
||||
class OverlayMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
final Event? nextEvent;
|
||||
final Event? previousEvent;
|
||||
final bool selected;
|
||||
final Timeline timeline;
|
||||
// final LanguageModel? selectedDisplayLang;
|
||||
|
|
@ -21,16 +20,16 @@ class OverlayMessage extends StatelessWidget {
|
|||
final bool ownMessage;
|
||||
final ToolbarDisplayController toolbarController;
|
||||
final double? width;
|
||||
final bool showDown;
|
||||
|
||||
const OverlayMessage(
|
||||
this.event, {
|
||||
this.nextEvent,
|
||||
this.previousEvent,
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
required this.immersionMode,
|
||||
required this.ownMessage,
|
||||
required this.toolbarController,
|
||||
required this.showDown,
|
||||
this.width,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -49,9 +48,13 @@ class OverlayMessage extends StatelessWidget {
|
|||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
const hardCorner = Radius.circular(4);
|
||||
const roundedCorner = Radius.circular(AppConfig.borderRadius);
|
||||
const borderRadius = BorderRadius.all(
|
||||
roundedCorner,
|
||||
final borderRadius = BorderRadius.only(
|
||||
topLeft: !showDown && !ownMessage ? hardCorner : roundedCorner,
|
||||
topRight: !showDown && ownMessage ? hardCorner : roundedCorner,
|
||||
bottomLeft: showDown && !ownMessage ? hardCorner : roundedCorner,
|
||||
bottomRight: showDown && ownMessage ? hardCorner : roundedCorner,
|
||||
);
|
||||
|
||||
final noBubble = {
|
||||
|
|
@ -83,93 +86,95 @@ class OverlayMessage extends StatelessWidget {
|
|||
: (color.blue * lightness).round(),
|
||||
);
|
||||
|
||||
final double maxHeight = (MediaQuery.of(context).size.height -
|
||||
(PlatformInfos.isIOS ? 254 : 232)) /
|
||||
2;
|
||||
|
||||
final pangeaMessageEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: ownMessage,
|
||||
);
|
||||
|
||||
return Flexible(
|
||||
// Make overlay message scrollable so long messages don't run offscreen
|
||||
child: SingleChildScrollView(
|
||||
child: Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
return SingleChildScrollView(
|
||||
child: Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
) ||
|
||||
(pangeaMessageEvent.showUseType))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: MessageContent(
|
||||
event.getDisplayEvent(timeline),
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
toolbarController: toolbarController,
|
||||
isOverlay: true,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pangeaMessageEvent.showUseType) ...[
|
||||
pangeaMessageEvent.msgUseType.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(164),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
) ||
|
||||
(pangeaMessageEvent.showUseType))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (pangeaMessageEvent.showUseType) ...[
|
||||
pangeaMessageEvent.msgUseType.iconView(
|
||||
context,
|
||||
textColor.withAlpha(164),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (event.hasAggregatedEvents(
|
||||
timeline,
|
||||
RelationshipTypes.edit,
|
||||
)) ...[
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
color: textColor.withAlpha(164),
|
||||
size: 14,
|
||||
),
|
||||
Text(
|
||||
' - ${event.getDisplayEvent(timeline).originServerTs.localizedTimeShort(context)}',
|
||||
style: TextStyle(
|
||||
color: textColor.withAlpha(164),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue