refactor(ui): refactor overlays

This commit is contained in:
Salvatore Giordano
2021-09-20 11:33:42 +02:00
parent d8774a2bbd
commit 92f5153b6b
7 changed files with 125 additions and 90 deletions
@@ -1081,10 +1081,10 @@ class Channel {
// remove regular message if present // remove regular message if present
if (oldIndex != -1) { if (oldIndex != -1) {
final oldMessage = state!.messages[oldIndex]; final oldMessage = state!.messages[oldIndex];
state!.updateChannelState(state!._channelState.copyWith( state!._channelState = state!._channelState.copyWith(
messages: state?.messages?..remove(oldMessage), messages: [...(state!.messages..remove(oldMessage))],
channel: state?._channelState.channel, channel: state?._channelState.channel,
)); );
} else { } else {
// remove thread message if present // remove thread message if present
// also reduces total reply count // also reduces total reply count
@@ -1773,9 +1773,8 @@ class ChannelClientState {
List<Message> get messages => _channelState.messages; List<Message> get messages => _channelState.messages;
/// Channel message list as a stream. /// Channel message list as a stream.
Stream<List<Message>> get messagesStream => channelStateStream Stream<List<Message>> get messagesStream =>
.map((cs) => cs.messages) channelStateStream.map((cs) => cs.messages);
.distinct(const ListEquality().equals);
/// Channel pinned message list. /// Channel pinned message list.
List<Message> get pinnedMessages => _channelState.pinnedMessages.toList(); List<Message> get pinnedMessages => _channelState.pinnedMessages.toList();
@@ -1,43 +1,62 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// Class to create an overlay anchored to a child widget /// Options for building an [Overlay]
class AnchoredOverlay extends StatelessWidget { class OverlayOption {
/// Constructor for creating an [AnchoredOverlay] /// Constructor for creating an [OverlayOption].
const AnchoredOverlay({ OverlayOption({
Key? key, required this.showOverlay,
this.showOverlay = false,
required this.overlayBuilder, required this.overlayBuilder,
required this.child, });
}) : super(key: key);
/// Show or hide overlay /// Show or hide overlay
final bool showOverlay; final bool showOverlay;
/// Builder for creating overlay widget /// Builder for creating overlay widget
final Widget Function(BuildContext, Offset anchor) overlayBuilder; 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);
/// Builder for creating overlay widget
final List<OverlayOption> overlayOptions;
/// Child to which overlay is anchored /// Child to which overlay is anchored
final Widget child; final Widget child;
@override @override
Widget build(BuildContext context) => SizedBox( Widget build(BuildContext context) {
child: LayoutBuilder( final overlay =
builder: (BuildContext context, BoxConstraints constraints) => overlayOptions.firstWhereOrNull((option) => option.showOverlay);
OverlayBuilder( return SizedBox(
showOverlay: showOverlay, child: LayoutBuilder(
overlayBuilder: (BuildContext overlayContext) { builder: (BuildContext context, BoxConstraints constraints) =>
final box = context.findRenderObject() as RenderBox?; OverlayBuilder(
if (box != null) { showOverlay: overlay != null,
final center = box.size overlayBuilder: (BuildContext overlayContext) {
.center(box.localToGlobal(const Offset(0, 0))); final box = context.findRenderObject() as RenderBox?;
return overlayBuilder(overlayContext, center); if (box != null) {
} else { final center =
return const SizedBox(); box.size.center(box.localToGlobal(const Offset(0, 0)));
} return overlay?.overlayBuilder(overlayContext, center) ??
}, const Offstage();
child: child, } else {
)), return const Offstage();
); }
},
child: child,
),
),
);
}
} }
/// Class for creating a declarative-ish overlay /// Class for creating a declarative-ish overlay
@@ -63,18 +82,31 @@ class OverlayBuilder extends StatefulWidget {
_OverlayBuilderState createState() => _OverlayBuilderState(); _OverlayBuilderState createState() => _OverlayBuilderState();
} }
class _OverlayBuilderState extends State<OverlayBuilder> { class _OverlayBuilderState extends State<OverlayBuilder>
with WidgetsBindingObserver {
OverlayEntry? overlayEntry; OverlayEntry? overlayEntry;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance!.addObserver(this);
if (widget.showOverlay) { if (widget.showOverlay) {
WidgetsBinding.instance?.addPostFrameCallback((_) => showOverlay()); WidgetsBinding.instance?.addPostFrameCallback((_) => showOverlay());
} }
} }
@override
void didChangeMetrics() {
super.didChangeMetrics();
SchedulerBinding.instance!.addPostFrameCallback((_) {
hideOverlay();
syncWidgetAndOverlay();
});
}
@override @override
void didUpdateWidget(OverlayBuilder oldWidget) { void didUpdateWidget(OverlayBuilder oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@@ -91,6 +123,7 @@ class _OverlayBuilderState extends State<OverlayBuilder> {
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance!.removeObserver(this);
if (isShowingOverlay()) { if (isShowingOverlay()) {
hideOverlay(); hideOverlay();
} }
@@ -2,10 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/visible_footnote.dart'; import 'package:stream_chat_flutter/src/visible_footnote.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget for showing a GIF attachment /// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget { class GiphyAttachment extends AttachmentWidget {
@@ -48,6 +48,7 @@ class GiphyAttachment extends AttachmentWidget {
} }
Widget _buildSendingAttachment(BuildContext context, String imageUrl) { Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
print('sending attachment GIPHY $imageUrl');
final streamChannel = StreamChannel.of(context); final streamChannel = StreamChannel.of(context);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -49,7 +49,7 @@ class MentionsOverlay extends StatelessWidget {
return const SizedBox(); return const SizedBox();
} }
final child = Card( return Card(
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
elevation: 2, elevation: 2,
color: _streamChatTheme.colorTheme.barsBg, color: _streamChatTheme.colorTheme.barsBg,
@@ -96,15 +96,6 @@ class MentionsOverlay extends StatelessWidget {
), ),
), ),
); );
return TweenAnimationBuilder<double>( ;
tween: Tween(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutExpo,
builder: (context, val, child) => Transform.scale(
scale: val,
child: child,
),
child: child,
);
} }
} }
@@ -6,18 +6,17 @@ 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_keyboard_visibility/flutter_keyboard_visibility.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/commands_overlay.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/mentions_overlay.dart';
import 'package:stream_chat_flutter/src/anchored_overlay.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/emoji.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/mentions_overlay.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
@@ -334,8 +333,6 @@ class MessageInputState extends State<MessageInput> {
bool _openFilePickerSection = false; bool _openFilePickerSection = false;
int _filePickerIndex = 0; int _filePickerIndex = 0;
final _keyboardVisibilityController = KeyboardVisibilityController();
/// The editing controller passed to the input TextField /// The editing controller passed to the input TextField
late final TextEditingController textEditingController; late final TextEditingController textEditingController;
@@ -351,15 +348,6 @@ class MessageInputState extends State<MessageInput> {
super.initState(); super.initState();
_focusNode = widget.focusNode ?? FocusNode(); _focusNode = widget.focusNode ?? FocusNode();
if (!kIsWeb) {
_keyboardListener =
_keyboardVisibilityController.onChange.listen((visible) {
if (_focusNode.hasFocus) {
_onChanged(context, textEditingController.text);
}
});
}
textEditingController = textEditingController =
widget.textEditingController ?? TextEditingController(); widget.textEditingController ?? TextEditingController();
if (widget.editMessage != null || widget.initialMessage != null) { if (widget.editMessage != null || widget.initialMessage != null) {
@@ -474,20 +462,26 @@ class MessageInputState extends State<MessageInput> {
child: child, child: child,
); );
} }
return AnchoredOverlay( return AnchoredOverlay(
showOverlay: _showMentionsOverlay, overlayOptions: [
overlayBuilder: (context, offset) => CenterAbout( OverlayOption(
position: Offset(offset.dx, offset.dy), overlayBuilder: (context, offset) => CenterAbout(
child: _buildMentionsOverlayEntry(), position: Offset(offset.dx, offset.dy),
), child: _buildMentionsOverlayEntry(),
child: AnchoredOverlay( ),
showOverlay: _showCommandsOverlay, showOverlay: _showMentionsOverlay,
overlayBuilder: (context, offset) => CenterAbout(
position: Offset(offset.dx, offset.dy),
child: _buildCommandsOverlayEntry(),
), ),
child: AnchoredOverlay( OverlayOption(
showOverlay: textEditingController.text.isNotEmpty && showOverlay: _showCommandsOverlay,
overlayBuilder: (context, offset) => CenterAbout(
position: Offset(offset.dx, offset.dy),
child: _buildCommandsOverlayEntry(),
),
),
OverlayOption(
showOverlay: _focusNode.hasFocus &&
textEditingController.text.isNotEmpty &&
textEditingController.selection.baseOffset > 0 && textEditingController.selection.baseOffset > 0 &&
textEditingController.text textEditingController.text
.substring( .substring(
@@ -499,9 +493,9 @@ class MessageInputState extends State<MessageInput> {
position: Offset(offset.dx, offset.dy), position: Offset(offset.dx, offset.dy),
child: _buildEmojiOverlay(), child: _buildEmojiOverlay(),
), ),
child: child,
), ),
), ],
child: child,
); );
} }
@@ -817,7 +811,10 @@ class MessageInputState extends State<MessageInput> {
String? _previousValue; String? _previousValue;
void _onChanged(BuildContext context, String s) { void _onChanged(
BuildContext context,
String s,
) {
if (s == _previousValue) { if (s == _previousValue) {
return; return;
} }
@@ -830,10 +827,13 @@ class MessageInputState extends State<MessageInput> {
if (!mounted) { if (!mounted) {
return; return;
} }
StreamChannel.of(context)
.channel if (s.isNotEmpty) {
.keyStroke(widget.parentMessage?.id) StreamChannel.of(context)
.catchError((e) {}); .channel
.keyStroke(widget.parentMessage?.id)
.catchError((e) {});
}
_checkCommands(s.trim(), context); _checkCommands(s.trim(), context);
@@ -999,7 +999,9 @@ class MessageInputState extends State<MessageInput> {
} }
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: _openFilePickerSection
? const Duration(milliseconds: 300)
: const Duration(),
curve: Curves.easeOut, curve: Curves.easeOut,
height: _openFilePickerSection ? _kMinMediaPickerSize : 0, height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -1215,8 +1217,12 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildEmojiOverlay() { Widget _buildEmojiOverlay() {
if (textEditingController.value.selection.baseOffset < 0) {
return const Offstage();
}
final splits = textEditingController.text final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start) .substring(0, textEditingController.value.selection.baseOffset)
.split(':'); .split(':');
final query = splits.last.toLowerCase(); final query = splits.last.toLowerCase();
@@ -1837,8 +1843,6 @@ class MessageInputState extends State<MessageInput> {
} }
} }
StreamSubscription? _keyboardListener;
void _showErrorAlert(String description) { void _showErrorAlert(String description) {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: _streamChatTheme.colorTheme.barsBg, backgroundColor: _streamChatTheme.colorTheme.barsBg,
@@ -1912,7 +1916,6 @@ class MessageInputState extends State<MessageInput> {
@override @override
void dispose() { void dispose() {
_keyboardListener?.cancel();
textEditingController.dispose(); textEditingController.dispose();
_stopSlowMode(); _stopSlowMode();
super.dispose(); super.dispose();
@@ -580,11 +580,12 @@ class _MessageWidgetState extends State<MessageWidget>
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
bool get hasNonUrlAttachments => bool get hasNonUrlAttachments => widget.message.attachments
widget.message.attachments.where((it) => it.titleLink == null).isNotEmpty; .where((it) => it.titleLink == null || it.type == 'giphy')
.isNotEmpty;
bool get hasUrlAttachments => bool get hasUrlAttachments => widget.message.attachments
widget.message.attachments.any((it) => it.titleLink != null) == true; .any((it) => it.titleLink != null && it.type != 'giphy');
bool get showBottomRow => bool get showBottomRow =>
showThreadReplyIndicator || showThreadReplyIndicator ||
@@ -603,6 +604,9 @@ class _MessageWidgetState extends State<MessageWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
if (widget.message.isEphemeral) {
print('EPHEMERAL');
}
final avatarWidth = final avatarWidth =
widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40;
final bottomRowPadding = final bottomRowPadding =
@@ -1153,10 +1157,15 @@ class _MessageWidgetState extends State<MessageWidget>
} }
Widget _parseAttachments() { Widget _parseAttachments() {
if (widget.message.isEphemeral) {
print('EPHEMERAL');
}
final attachmentGroups = <String, List<Attachment>>{}; final attachmentGroups = <String, List<Attachment>>{};
widget.message.attachments widget.message.attachments
.where((element) => element.titleLink == null && element.type != null) .where((element) =>
(element.titleLink == null && element.type != null) ||
element.type == 'giphy')
.forEach((e) { .forEach((e) {
if (attachmentGroups[e.type] == null) { if (attachmentGroups[e.type] == null) {
attachmentGroups[e.type!] = []; attachmentGroups[e.type!] = [];
@@ -19,7 +19,6 @@ dependencies:
file_picker: ^3.0.1 file_picker: ^3.0.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_keyboard_visibility: ^5.0.1
flutter_markdown: ^0.6.1 flutter_markdown: ^0.6.1
flutter_portal: ^0.4.0 flutter_portal: ^0.4.0
flutter_slidable: ^0.6.0 flutter_slidable: ^0.6.0