diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index eb95d6d5..90d4306d 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -4,6 +4,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:keyboard_visibility/keyboard_visibility.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -82,6 +83,10 @@ class _MessageInputState extends State { final _focusNode = FocusNode(); + bool _modalOn = false; + + OverlayEntry _commandsOverlay; + @override Widget build(BuildContext context) { return SafeArea( @@ -140,6 +145,19 @@ class _MessageInputState extends State { setState(() { _messageIsPresent = s.trim().isNotEmpty; }); + + if (s.startsWith('/')) { + _modalOn = true; + + _commandsOverlay?.remove(); + _commandsOverlay = null; + _commandsOverlay = _buildOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } else if (!s.startsWith('/') && _modalOn) { + _modalOn = false; + _commandsOverlay?.remove(); + _commandsOverlay = null; + } }, onTap: () { setState(() { @@ -175,6 +193,79 @@ class _MessageInputState extends State { ); } + OverlayEntry _buildOverlayEntry() { + final text = _textController.text; + final commands = StreamChannel.of(context) + .channel + .config + .commands + .where((c) => c.name.startsWith(text.substring(1))) + .toList(); + + RenderBox renderBox = context.findRenderObject(); + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + return OverlayEntry(builder: (_) { + return Positioned( + top: offset.dy - size.height, + left: 0, + right: 0, + child: Material( + color: StreamChatTheme.of(context).primaryColor, + child: Flex( + mainAxisSize: MainAxisSize.min, + direction: Axis.vertical, + children: [ + ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: commands + .map((c) => ListTile( + title: Text.rich( + TextSpan( + text: '${c.name}', + style: TextStyle(fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' ${c.args}', + style: TextStyle( + fontWeight: FontWeight.w300, + ), + ), + ], + ), + ), + subtitle: Text(c.description), + onTap: () { + _setCommand(c); + }, + )) + .toList(), + ), + ], + ), + ), + ); + }); + } + + void _setCommand(Command c) { + setState(() { + _textController.value = TextEditingValue( + text: '/${c.name}', + selection: TextSelection.fromPosition( + TextPosition( + offset: c.name.length + 1, + ), + ), + ); + }); + _modalOn = false; + _commandsOverlay?.remove(); + _commandsOverlay = null; + } + Gradient _getGradient(BuildContext context) { if (_typingStarted) { if (widget.editMessage == null) { @@ -438,6 +529,12 @@ class _MessageInputState extends State { _messageIsPresent = false; _typingStarted = false; }); + + _modalOn = false; + + _commandsOverlay?.remove(); + _commandsOverlay = null; + FocusScope.of(context).unfocus(); if (widget.editMessage != null) { @@ -486,10 +583,24 @@ class _MessageInputState extends State { }); } + int _keyboardListener; + @override void initState() { super.initState(); + _keyboardListener = KeyboardVisibilityNotification().addNewListener( + onChange: (_) { + if (_modalOn) { + _commandsOverlay?.remove(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _commandsOverlay = _buildOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + }); + } + }, + ); + if (widget.editMessage != null) { _textController = TextEditingController(text: widget.editMessage.text); @@ -510,12 +621,19 @@ class _MessageInputState extends State { } } - bool _focused = false; + @override + void dispose() { + _commandsOverlay?.remove(); + KeyboardVisibilityNotification().removeListener(_keyboardListener); + super.dispose(); + } + + bool _initialized = false; @override void didChangeDependencies() { - if (widget.editMessage != null && !_focused) { + if (widget.editMessage != null && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); - _focused = true; + _initialized = true; } super.didChangeDependencies(); } diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 7053b482..f8b58398 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -233,74 +233,124 @@ class _MessageWidgetState extends State .copyWith(color: Color(0xffebebeb)); return Padding( padding: const EdgeInsets.only(bottom: 2.0), - child: ClipRRect( - borderRadius: boxDecoration.borderRadius, - child: Container( - decoration: boxDecoration, - constraints: BoxConstraints.loose(Size.fromWidth(300)), - child: Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: boxDecoration.borderRadius, + child: Container( + decoration: boxDecoration, + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: Stack( children: [ - attachmentWidget, - attachment.title != null - ? Container( - constraints: - BoxConstraints.loose(Size(300, 500)), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - attachment.title, - overflow: TextOverflow.ellipsis, - style: - messageTheme.messageText.copyWith( - color: Colors.blue, - fontWeight: FontWeight.bold, - ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + attachmentWidget, + attachment.title != null + ? Container( + constraints: + BoxConstraints.loose(Size(300, 500)), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + attachment.title, + overflow: TextOverflow.ellipsis, + style: messageTheme.messageText + .copyWith( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + Text( + Uri.parse(attachment.thumbUrl) + .authority + .split('.') + .reversed + .take(2) + .toList() + .reversed + .join('.'), + overflow: TextOverflow.ellipsis, + style: messageTheme.createdAt, + ), + ], ), - Text( - Uri.parse(attachment.thumbUrl) - .authority - .split('.') - .reversed - .take(2) - .toList() - .reversed - .join('.'), - overflow: TextOverflow.ellipsis, - style: messageTheme.createdAt, - ), - ], + ), + color: Color(0xffebebeb), + ) + : SizedBox(), + ], + ), + attachment.type == 'image' && + attachment.titleLink != null + ? Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => + _launchURL(attachment.titleLink), ), ), - color: Color(0xffebebeb), ) : SizedBox(), ], ), - attachment.type == 'image' && attachment.titleLink != null - ? Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _launchURL(attachment.titleLink), - ), + margin: EdgeInsets.only( + top: nOfAttachmentWidgets > 1 ? 5 : 0, + ), + ), + ), + if (attachment.actions != null) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: attachment.actions?.map((action) { + if (action.style == 'primary') { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: FlatButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - ) - : SizedBox(), - ], - ), - margin: EdgeInsets.only( - top: nOfAttachmentWidgets > 1 ? 5 : 0, - ), - ), + child: Text('${action.text}'), + color: action.style == 'primary' + ? StreamChatTheme.of(context).accentColor + : null, + textColor: Colors.white, + onPressed: () { + streamChannel.channel + .sendAction(widget.message.id, { + action.name: action.value, + }); + }, + ), + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Text('${action.text}'), + color: StreamChatTheme.of(context).accentColor, + onPressed: () { + streamChannel.channel + .sendAction(widget.message.id, { + action.name: action.value, + }); + }, + ), + ); + })?.toList(), + ), + ], ), ); } @@ -313,80 +363,96 @@ class _MessageWidgetState extends State if (widget.message.text.trim().isNotEmpty) { column.children.addAll( [ - IntrinsicWidth( - child: Column( - crossAxisAlignment: isMyMessage - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - streamChannel.channel.config.reactions - ? Align( - child: _buildReactions(), - alignment: isMyMessage - ? Alignment.centerLeft - : Alignment.centerRight, - ) - : SizedBox(), - Stack( - overflow: Overflow.visible, - children: [ - widget.message.reactionCounts?.isNotEmpty == true - ? Positioned( - left: isMyMessage ? 8 : null, - right: !isMyMessage ? 8 : null, - top: -6, - child: CustomPaint( - painter: ReactionBubblePainter(), - ), - ) - : SizedBox(), - Padding( - padding: EdgeInsets.only( - right: isMyMessage ? 0.0 : 8.0, - left: isMyMessage ? 8.0 : 0.0, - ), - child: Container( - decoration: _buildBoxDecoration( - isLastUser || nOfAttachmentWidgets > 0), - padding: EdgeInsets.all(10), - constraints: BoxConstraints.loose(Size.fromWidth(300)), - child: MarkdownBody( - data: '${widget.message.text}', - onTapLink: (link) { - _launchURL(link); - }, - styleSheet: MarkdownStyleSheet.fromTheme( - Theme.of(context).copyWith( - textTheme: Theme.of(context).textTheme.apply( - bodyColor: messageTheme.messageText.color, - decoration: - messageTheme.messageText.decoration, - decorationColor: messageTheme - .messageText.decorationColor, - decorationStyle: messageTheme - .messageText.decorationStyle, - fontFamily: - messageTheme.messageText.fontFamily, - ), - ), - ).copyWith( - p: messageTheme.messageText, + Column( + crossAxisAlignment: + isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + streamChannel.channel.config.reactions + ? Align( + child: _buildReactions(), + alignment: isMyMessage + ? Alignment.centerLeft + : Alignment.centerRight, + ) + : SizedBox(), + Stack( + overflow: Overflow.visible, + children: [ + _buildReactionPaint(), + Padding( + padding: EdgeInsets.only( + right: isMyMessage ? 0.0 : 8.0, + left: isMyMessage ? 8.0 : 0.0, + ), + child: Container( + decoration: _buildBoxDecoration( + isLastUser || nOfAttachmentWidgets > 0), + padding: EdgeInsets.all(10), + constraints: BoxConstraints.loose(Size.fromWidth(300)), + child: MarkdownBody( + data: '${widget.message.text}', + onTapLink: (link) { + _launchURL(link); + }, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context).copyWith( + textTheme: Theme.of(context).textTheme.apply( + bodyColor: messageTheme.messageText.color, + decoration: + messageTheme.messageText.decoration, + decorationColor: + messageTheme.messageText.decorationColor, + decorationStyle: + messageTheme.messageText.decorationStyle, + fontFamily: + messageTheme.messageText.fontFamily, + ), ), + ).copyWith( + p: messageTheme.messageText, ), ), ), - ], - ), - ], - ), + ), + ], + ), + ], ), ], ); + } else { + column.children.insert( + 0, + streamChannel.channel.config.reactions + ? Align( + child: _buildReactions(), + alignment: + isMyMessage ? Alignment.centerLeft : Alignment.centerRight, + ) + : SizedBox(), + ); + column.children[1] = Stack( + overflow: Overflow.visible, + children: [ + Padding( + padding: EdgeInsets.only( + right: isMyMessage ? 0.0 : 8.0, + left: isMyMessage ? 8.0 : 0.0, + ), + child: column.children[1], + ), + _buildReactionPaint(), + ], + ); } return GestureDetector( - child: column, + child: IntrinsicWidth(child: column), onLongPress: () { + if (widget.message.type == 'ephemeral') { + return; + } + if (widget.onMessageActions != null) { widget.onMessageActions(context, widget.message); } else { @@ -395,6 +461,19 @@ class _MessageWidgetState extends State }); } + Widget _buildReactionPaint() { + return widget.message.reactionCounts?.isNotEmpty == true + ? Positioned( + left: isMyMessage ? 8 : null, + right: !isMyMessage ? 8 : null, + top: -6, + child: CustomPaint( + painter: _ReactionBubblePainter(), + ), + ) + : SizedBox(); + } + void _showMessageBottomSheet(BuildContext context) { if (!streamChannel.channel.config.reactions && !streamChannel.channel.config.replies) { @@ -640,6 +719,15 @@ class _MessageWidgetState extends State imageUrl: attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl, fit: BoxFit.cover, + placeholder: (_, __) { + return SizedBox( + height: 200, + width: 300, + child: Center( + child: CircularProgressIndicator(), + ), + ); + }, ); } @@ -743,7 +831,7 @@ class _MessageWidgetState extends State } } -class ReactionBubblePainter extends CustomPainter { +class _ReactionBubblePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.black;