refactor(ui): refactor overlays
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user