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