diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index c2c40018..decc1c7d 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -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 get messages => _channelState.messages; /// Channel message list as a stream. - Stream> get messagesStream => channelStateStream - .map((cs) => cs.messages) - .distinct(const ListEquality().equals); + Stream> get messagesStream => + channelStateStream.map((cs) => cs.messages); /// Channel pinned message list. List get pinnedMessages => _channelState.pinnedMessages.toList(); diff --git a/packages/stream_chat_flutter/lib/src/anchored_overlay.dart b/packages/stream_chat_flutter/lib/src/anchored_overlay.dart index d79e19df..540fe2ec 100644 --- a/packages/stream_chat_flutter/lib/src/anchored_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/anchored_overlay.dart @@ -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 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 { +class _OverlayBuilderState extends State + 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 { @override void dispose() { + WidgetsBinding.instance!.removeObserver(this); if (isShowingOverlay()) { hideOverlay(); } diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index e165fc6d..dacf7fbe 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/mentions_overlay.dart index 57d286d9..b51f8261 100644 --- a/packages/stream_chat_flutter/lib/src/mentions_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/mentions_overlay.dart @@ -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( - 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, - ); + ; } } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 65c23804..2eca6ee7 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -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 { 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 { 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 { 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 { position: Offset(offset.dx, offset.dy), child: _buildEmojiOverlay(), ), - child: child, ), - ), + ], + child: child, ); } @@ -817,7 +811,10 @@ class MessageInputState extends State { 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 { 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 { } 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 { } 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 { } } - StreamSubscription? _keyboardListener; - void _showErrorAlert(String description) { showModalBottomSheet( backgroundColor: _streamChatTheme.colorTheme.barsBg, @@ -1912,7 +1916,6 @@ class MessageInputState extends State { @override void dispose() { - _keyboardListener?.cancel(); textEditingController.dispose(); _stopSlowMode(); super.dispose(); diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 01177603..fc56303f 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -580,11 +580,12 @@ class _MessageWidgetState extends State 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 @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 } Widget _parseAttachments() { + if (widget.message.isEphemeral) { + print('EPHEMERAL'); + } final attachmentGroups = >{}; 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!] = []; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 907e9a7f..54888873 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -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