refactor(ui): use portal
This commit is contained in:
@@ -12,10 +12,6 @@
|
||||
|
||||
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls.
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `MessageInput.customOverlays` property to add custom overlays to the `MessageInput`.
|
||||
|
||||
## 2.2.1
|
||||
|
||||
⚠️ Deprecated
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// Options for building an [Overlay]
|
||||
class OverlayOption {
|
||||
/// Constructor for creating an [OverlayOption].
|
||||
OverlayOption({
|
||||
required this.showOverlay,
|
||||
required this.overlayBuilder,
|
||||
});
|
||||
|
||||
/// Show or hide overlay
|
||||
final bool showOverlay;
|
||||
|
||||
/// Builder for creating overlay widget
|
||||
final Widget Function(BuildContext, Offset anchor) overlayBuilder;
|
||||
}
|
||||
|
||||
/// Class to create an overlay anchored to a child widget
|
||||
class AnchoredOverlay extends StatelessWidget {
|
||||
/// Constructor for creating an [AnchoredOverlay]
|
||||
const AnchoredOverlay({
|
||||
Key? key,
|
||||
required this.overlayOptions,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
/// List of overlay options
|
||||
/// The first one which showOverlay is true will be used
|
||||
final List<OverlayOption> overlayOptions;
|
||||
|
||||
/// Child to which overlay is anchored
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final overlay =
|
||||
overlayOptions.firstWhereOrNull((option) => option.showOverlay);
|
||||
return SizedBox(
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) =>
|
||||
OverlayBuilder(
|
||||
showOverlay: overlay != null,
|
||||
overlayBuilder: (BuildContext overlayContext) {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
if (box != null) {
|
||||
final center =
|
||||
box.size.center(box.localToGlobal(const Offset(0, 0)));
|
||||
return overlay?.overlayBuilder(overlayContext, center) ??
|
||||
const Offstage();
|
||||
} else {
|
||||
return const Offstage();
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Class for creating a declarative-ish overlay
|
||||
class OverlayBuilder extends StatefulWidget {
|
||||
/// Constructor for creating an [OverlayBuilder]
|
||||
const OverlayBuilder({
|
||||
Key? key,
|
||||
this.showOverlay = false,
|
||||
required this.overlayBuilder,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Show or hide overlay
|
||||
final bool showOverlay;
|
||||
|
||||
/// Builder for creating overlay widget
|
||||
final Widget Function(BuildContext) overlayBuilder;
|
||||
|
||||
/// Child to which overlay is anchored
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_OverlayBuilderState createState() => _OverlayBuilderState();
|
||||
}
|
||||
|
||||
class _OverlayBuilderState extends State<OverlayBuilder>
|
||||
with WidgetsBindingObserver {
|
||||
OverlayEntry? overlayEntry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance!.addObserver(this);
|
||||
|
||||
if (widget.showOverlay) {
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) => showOverlay());
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets? _previousInsets;
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
super.didChangeMetrics();
|
||||
|
||||
final newInsets = MediaQuery.of(context).viewPadding;
|
||||
|
||||
if (newInsets.bottom != _previousInsets?.bottom) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
hideOverlay();
|
||||
syncWidgetAndOverlay();
|
||||
});
|
||||
|
||||
_previousInsets = newInsets;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(OverlayBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
WidgetsBinding.instance
|
||||
?.addPostFrameCallback((_) => syncWidgetAndOverlay());
|
||||
}
|
||||
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
WidgetsBinding.instance
|
||||
?.addPostFrameCallback((_) => syncWidgetAndOverlay());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance!.removeObserver(this);
|
||||
if (isShowingOverlay()) {
|
||||
hideOverlay();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool isShowingOverlay() => overlayEntry != null;
|
||||
|
||||
void showOverlay() {
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: widget.overlayBuilder,
|
||||
);
|
||||
if (overlayEntry != null) {
|
||||
addToOverlay(overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
void addToOverlay(OverlayEntry entry) async {
|
||||
Overlay.of(context)?.insert(entry);
|
||||
}
|
||||
|
||||
void hideOverlay() {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
}
|
||||
|
||||
void syncWidgetAndOverlay() {
|
||||
if (isShowingOverlay() && !widget.showOverlay) {
|
||||
hideOverlay();
|
||||
} else if (!isShowingOverlay() && widget.showOverlay) {
|
||||
showOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
/// Class to position Overlay
|
||||
class CenterAbout extends StatelessWidget {
|
||||
/// Constructor for creating an [CenterAbout]
|
||||
const CenterAbout({
|
||||
Key? key,
|
||||
required this.position,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Position of child
|
||||
final Offset position;
|
||||
|
||||
/// Child widget to which overlay is attached
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
return Positioned(
|
||||
bottom: size.height - position.dy,
|
||||
left: position.dx,
|
||||
child: FractionalTranslation(
|
||||
translation: const Offset(-0.5, 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class MentionsOverlay extends StatelessWidget {
|
||||
},
|
||||
child: mentionsTileBuilder != null
|
||||
? mentionsTileBuilder!(context, m)
|
||||
: MentionTile(m),
|
||||
: MentionTile(m, key: ValueKey(m.user?.id)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,11 +6,11 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/anchored_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/commands_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||
@@ -192,7 +192,6 @@ class MessageInput extends StatefulWidget {
|
||||
this.onAttachmentLimitExceed,
|
||||
this.attachmentButtonBuilder,
|
||||
this.commandButtonBuilder,
|
||||
this.customOverlays = const [],
|
||||
}) : assert(
|
||||
initialMessage == null || editMessage == null,
|
||||
"Can't provide both `initialMessage` and `editMessage`",
|
||||
@@ -223,9 +222,6 @@ class MessageInput extends StatefulWidget {
|
||||
/// Use this to transform the message
|
||||
final FutureOr<Message> Function(Message)? preMessageSending;
|
||||
|
||||
/// List of custom overlays
|
||||
final List<OverlayOption> customOverlays;
|
||||
|
||||
/// Parent message in case of a thread
|
||||
final Message? parentMessage;
|
||||
|
||||
@@ -466,40 +462,32 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
return AnchoredOverlay(
|
||||
overlayOptions: [
|
||||
OverlayOption(
|
||||
overlayBuilder: (context, offset) => CenterAbout(
|
||||
position: offset,
|
||||
child: _buildMentionsOverlayEntry(),
|
||||
),
|
||||
showOverlay: _showMentionsOverlay,
|
||||
return PortalEntry(
|
||||
portal: _buildCommandsOverlayEntry(),
|
||||
visible: _showCommandsOverlay,
|
||||
childAnchor: Alignment.topCenter,
|
||||
portalAnchor: Alignment.bottomCenter,
|
||||
child: PortalEntry(
|
||||
portal: _buildEmojiOverlay(),
|
||||
visible: _focusNode.hasFocus &&
|
||||
textEditingController.text.isNotEmpty &&
|
||||
textEditingController.selection.baseOffset > 0 &&
|
||||
textEditingController.text
|
||||
.substring(
|
||||
0,
|
||||
textEditingController.selection.baseOffset,
|
||||
)
|
||||
.contains(':'),
|
||||
childAnchor: Alignment.topCenter,
|
||||
portalAnchor: Alignment.bottomCenter,
|
||||
child: PortalEntry(
|
||||
portal: _buildMentionsOverlayEntry(),
|
||||
visible: _showMentionsOverlay,
|
||||
childAnchor: Alignment.topCenter,
|
||||
portalAnchor: Alignment.bottomCenter,
|
||||
child: child,
|
||||
),
|
||||
OverlayOption(
|
||||
showOverlay: _showCommandsOverlay,
|
||||
overlayBuilder: (context, offset) => CenterAbout(
|
||||
position: offset,
|
||||
child: _buildCommandsOverlayEntry(),
|
||||
),
|
||||
),
|
||||
OverlayOption(
|
||||
showOverlay: _focusNode.hasFocus &&
|
||||
textEditingController.text.isNotEmpty &&
|
||||
textEditingController.selection.baseOffset > 0 &&
|
||||
textEditingController.text
|
||||
.substring(
|
||||
0,
|
||||
textEditingController.selection.baseOffset,
|
||||
)
|
||||
.contains(':'),
|
||||
overlayBuilder: (context, offset) => CenterAbout(
|
||||
position: offset,
|
||||
child: _buildEmojiOverlay(),
|
||||
),
|
||||
),
|
||||
...widget.customOverlays,
|
||||
],
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -946,8 +934,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildCommandsOverlayEntry() {
|
||||
final text = textEditingController.text.trimLeft();
|
||||
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final renderObject = context.findRenderObject() as RenderBox;
|
||||
final renderObject = context.findRenderObject() as RenderBox?;
|
||||
if (renderObject == null) {
|
||||
return const Offstage();
|
||||
}
|
||||
return CommandsOverlay(
|
||||
channel: StreamChannel.of(context).channel,
|
||||
size: Size(renderObject.size.width - 16, 400),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export 'package:jiffy/jiffy.dart';
|
||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
export 'src/anchored_overlay.dart';
|
||||
export 'src/attachment/attachment.dart';
|
||||
export 'src/back_button.dart';
|
||||
export 'src/channel_avatar.dart';
|
||||
|
||||
Reference in New Issue
Block a user