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