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.
|
- 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
|
## 2.2.1
|
||||||
|
|
||||||
⚠️ Deprecated
|
⚠️ 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
|
child: mentionsTileBuilder != null
|
||||||
? mentionsTileBuilder!(context, m)
|
? 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/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_portal/flutter_portal.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:shimmer/shimmer.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/commands_overlay.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||||
@@ -192,7 +192,6 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.onAttachmentLimitExceed,
|
this.onAttachmentLimitExceed,
|
||||||
this.attachmentButtonBuilder,
|
this.attachmentButtonBuilder,
|
||||||
this.commandButtonBuilder,
|
this.commandButtonBuilder,
|
||||||
this.customOverlays = const [],
|
|
||||||
}) : assert(
|
}) : assert(
|
||||||
initialMessage == null || editMessage == null,
|
initialMessage == null || editMessage == null,
|
||||||
"Can't provide both `initialMessage` and `editMessage`",
|
"Can't provide both `initialMessage` and `editMessage`",
|
||||||
@@ -223,9 +222,6 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Use this to transform the message
|
/// Use this to transform the message
|
||||||
final FutureOr<Message> Function(Message)? preMessageSending;
|
final FutureOr<Message> Function(Message)? preMessageSending;
|
||||||
|
|
||||||
/// List of custom overlays
|
|
||||||
final List<OverlayOption> customOverlays;
|
|
||||||
|
|
||||||
/// Parent message in case of a thread
|
/// Parent message in case of a thread
|
||||||
final Message? parentMessage;
|
final Message? parentMessage;
|
||||||
|
|
||||||
@@ -466,40 +462,32 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnchoredOverlay(
|
return PortalEntry(
|
||||||
overlayOptions: [
|
portal: _buildCommandsOverlayEntry(),
|
||||||
OverlayOption(
|
visible: _showCommandsOverlay,
|
||||||
overlayBuilder: (context, offset) => CenterAbout(
|
childAnchor: Alignment.topCenter,
|
||||||
position: offset,
|
portalAnchor: Alignment.bottomCenter,
|
||||||
child: _buildMentionsOverlayEntry(),
|
child: PortalEntry(
|
||||||
),
|
portal: _buildEmojiOverlay(),
|
||||||
showOverlay: _showMentionsOverlay,
|
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() {
|
Widget _buildCommandsOverlayEntry() {
|
||||||
final text = textEditingController.text.trimLeft();
|
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(
|
return CommandsOverlay(
|
||||||
channel: StreamChannel.of(context).channel,
|
channel: StreamChannel.of(context).channel,
|
||||||
size: Size(renderObject.size.width - 16, 400),
|
size: Size(renderObject.size.width - 16, 400),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export 'package:jiffy/jiffy.dart';
|
export 'package:jiffy/jiffy.dart';
|
||||||
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
export 'src/anchored_overlay.dart';
|
|
||||||
export 'src/attachment/attachment.dart';
|
export 'src/attachment/attachment.dart';
|
||||||
export 'src/back_button.dart';
|
export 'src/back_button.dart';
|
||||||
export 'src/channel_avatar.dart';
|
export 'src/channel_avatar.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user