diff --git a/analysis_options.yaml b/analysis_options.yaml index 3723c0af..8d6f9064 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,8 @@ include: package:pedantic/analysis_options.yaml analyzer: + enable-experiment: + - extension-methods exclude: - lib/**/*.g.dart - example/** diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 714966ce..7ee4311c 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -213,41 +213,29 @@ class MessageActionsModal extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + IconButton( + icon: Icon( + StreamIcons.edit, + size: 22, + color: + StreamChatTheme.of(context).primaryIconTheme.color, + ), + onPressed: () {}, + ), Text( 'Edit message', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.headline6.copyWith(fontWeight: FontWeight.bold), ), - Container( - height: 30, - padding: const EdgeInsets.all(2.0), - child: AspectRatio( - aspectRatio: 1, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - disabledElevation: 0, - hoverElevation: 0, - onPressed: () { - Navigator.of(context).pop(); - }, - fillColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.1) - : Colors.black.withOpacity(.1), - padding: EdgeInsets.all(4), - child: Icon( - Icons.close, - size: 15, - color: StreamChatTheme.of(context) - .primaryIconTheme - .color, - ), - ), + IconButton( + icon: Icon( + Icons.cancel_outlined, + size: 22, + color: + StreamChatTheme.of(context).primaryIconTheme.color, ), + onPressed: () { + Navigator.of(context).pop(); + }, ), ], ), diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 5334b82b..30910924 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; @@ -165,8 +166,13 @@ class MessageInputState extends State { bool _inputEnabled = true; bool _messageIsPresent = false; bool _typingStarted = false; + bool _commandEnabled = false; OverlayEntry _commandsOverlay, _mentionsOverlay; + Command _chosenCommand; + bool _actionsShrunk = false; + bool _sendAsDm = false; + /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -179,21 +185,19 @@ class MessageInputState extends State { _focusNode.unfocus(); } }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - clipBehavior: Clip.none, - children: [ - _buildBorder(context), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAttachments(), - _buildTextField(context), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: _buildTextField(context), + ), + if(widget.parentMessage != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _buildDmCheckbox(), ), - ], - ), + ], ), ), ); @@ -202,9 +206,9 @@ class MessageInputState extends State { Flex _buildTextField(BuildContext context) { return Flex( direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (!widget.disableAttachments) _buildAttachmentButton(), + if (!_commandEnabled) _buildExpandActionsButton(), if (widget.actionsLocation == ActionsLocation.left) ...widget.actions ?? [], _buildTextInput(context), @@ -215,6 +219,18 @@ class MessageInputState extends State { ); } + Widget _buildDmCheckbox() { + return Row( + children: [ + Checkbox(value: _sendAsDm, onChanged: (val) => setState(() {_sendAsDm = val;})), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text('Send also as direct message'), + ), + ], + ); + } + AnimatedCrossFade _animateSendButton(BuildContext context) { return AnimatedCrossFade( crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && @@ -222,7 +238,34 @@ class MessageInputState extends State { ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: _buildSendButton(context), - secondChild: SizedBox(), + secondChild: _buildIdleSendButton(context), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ); + } + + Widget _buildExpandActionsButton() { + return AnimatedCrossFade( + crossFadeState: + _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, + firstChild: IconButton( + onPressed: () { + setState(() { + _actionsShrunk = false; + }); + }, + icon: Icon( + StreamIcons.circle_left, + color: StreamChatTheme.of(context).accentColor, + ), + ), + secondChild: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (!widget.disableAttachments) _buildAttachmentButton(), + if (widget.editMessage == null) _buildCommandButton(), + ], + ), duration: Duration(milliseconds: 300), alignment: Alignment.center, ); @@ -230,60 +273,134 @@ class MessageInputState extends State { Expanded _buildTextInput(BuildContext context) { return Expanded( - child: LimitedBox( - maxHeight: widget.maxHeight, - child: TextField( - key: Key('messageInputText'), - enabled: _inputEnabled, - minLines: null, - maxLines: null, - onSubmitted: (_) { - sendMessage(); - }, - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); + child: Center( + child: LimitedBox( + maxHeight: widget.maxHeight, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.0), + border: Border.all( + color: Colors.grey, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildAttachments(), + TextField( + key: Key('messageInputText'), + enabled: _inputEnabled, + minLines: null, + maxLines: null, + onSubmitted: (_) { + sendMessage(); + }, + keyboardType: widget.keyboardType, + controller: textEditingController, + focusNode: _focusNode, + onChanged: (s) { + StreamChannel.of(context).channel.keyStroke(); - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - }); + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + _actionsShrunk = s.trim().isNotEmpty; + }); - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (s.startsWith('/')) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } + if (s.startsWith('/')) { + var matchedCommandsList = StreamChannel.of(context) + .channel + .config + .commands.where((element) => element.name == s.substring(1)).toList(); - if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == '@' || - textEditingController.text - .substring(0, textEditingController.selection.start) - .split(' ') - .last - .contains('@'))) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - } - }, - onTap: () { - setState(() { - _typingStarted = true; - }); - }, - style: Theme.of(context).textTheme.bodyText2, - autofocus: false, - decoration: InputDecoration( - hintText: 'Write a message', - prefixText: ' ', - border: InputBorder.none, + if(matchedCommandsList.length == 1) { + _chosenCommand = matchedCommandsList[0]; + textEditingController.clear(); + _messageIsPresent = false; + setState(() { + _commandEnabled = true; + }); + _commandsOverlay.remove(); + _commandsOverlay = null; + } else { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } + } + + if (textEditingController.selection.isCollapsed && + (s[textEditingController.selection.start - 1] == '@' || + textEditingController.text + .substring( + 0, textEditingController.selection.start) + .split(' ') + .last + .contains('@'))) { + _mentionsOverlay = _buildMentionsOverlayEntry(); + Overlay.of(context).insert(_mentionsOverlay); + } + }, + onTap: () { + setState(() { + _typingStarted = true; + }); + }, + style: Theme.of(context).textTheme.bodyText2, + autofocus: false, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: 'Write a message', + prefixText: _commandEnabled ? null : ' ', + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent)), + contentPadding: EdgeInsets.all(8), + prefixIcon: _commandEnabled + ? Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Chip( + backgroundColor: + StreamChatTheme.of(context).accentColor, + label: Text( + _chosenCommand?.name ?? "", + style: TextStyle(color: Colors.white), + ), + avatar: Icon( + StreamIcons.lightning, + color: Colors.white, + ), + ), + ) + : null, + suffixIcon: _commandEnabled + ? IconButton( + icon: Icon(Icons.cancel_outlined), + onPressed: () { + setState(() { + _commandEnabled = false; + }); + }, + ) + : null, + ), + textCapitalization: TextCapitalization.sentences, + ), + ], + ), ), - textCapitalization: TextCapitalization.sentences, ), ), ); @@ -340,47 +457,79 @@ class MessageInputState extends State { bottom: size.height + MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, - child: Material( - color: StreamChatTheme.of(context).primaryColor, - child: Container( - constraints: BoxConstraints.loose(Size.fromHeight(400)), - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5.0, - offset: Offset(0, -4), - ), - ], - color: StreamChatTheme.of(context).primaryColor, - ), - child: 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, - ), - ), - ], - ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 2.0, + color: StreamChatTheme.of(context).primaryColor, + clipBehavior: Clip.antiAlias, + child: Container( + constraints: BoxConstraints.loose(Size.fromHeight(400)), + decoration: BoxDecoration( + // boxShadow: [ + // BoxShadow( + // spreadRadius: -8, + // blurRadius: 5.0, + // offset: Offset(0, -4), + // ), + // ], + color: StreamChatTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(8.0)), + child: ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + if (commands.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Icon(StreamIcons.lightning, + color: StreamChatTheme.of(context).accentColor), + ), + Text('Instant Commands') + ], ), - subtitle: Text(c.description), - onTap: () { - _setCommand(c); - }, ), - ) - .toList(), + ...commands + .map( + (c) => ListTile( + title: Text.rich( + TextSpan( + text: '${c.name.capitalize()}', + style: TextStyle(fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' /${c.name} ${c.args}', + style: TextStyle( + fontWeight: FontWeight.w300, + ), + ), + ], + ), + ), + trailing: CircleAvatar( + backgroundColor: + StreamChatTheme.of(context).accentColor, + child: Icon( + StreamIcons.lightning, + color: Colors.white, + size: 12.5, + ), + maxRadius: 15, + ), + //subtitle: Text(c.description), + onTap: () { + _setCommand(c); + }, + ), + ) + .toList(), + ], + ), ), ), ), @@ -417,8 +566,11 @@ class MessageInputState extends State { bottom: size.height + MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, - child: Material( + child: Card( + margin: EdgeInsets.all(8.0), + elevation: 2.0, color: StreamChatTheme.of(context).primaryColor, + clipBehavior: Clip.antiAlias, child: Container( constraints: BoxConstraints.loose(Size.fromHeight(400)), decoration: BoxDecoration( @@ -443,7 +595,15 @@ class MessageInputState extends State { leading: UserAvatar( user: m.user, ), - title: Text('${m.user.name}'), + title: Text( + '${m.user.name}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('${m.userId}'), + trailing: Icon( + Icons.alternate_email, + color: StreamChatTheme.of(context).accentColor, + ), onTap: () { _mentionedUsers.add(m.user); @@ -474,12 +634,12 @@ class MessageInputState extends State { } void _setCommand(Command c) { - textEditingController.value = TextEditingValue( - text: '/${c.name} ', - selection: TextSelection.collapsed( - offset: c.name.length + 2, - ), - ); + textEditingController.clear(); + setState(() { + _chosenCommand = c; + _commandEnabled = true; + _messageIsPresent = false; + }); _commandsOverlay?.remove(); _commandsOverlay = null; } @@ -498,39 +658,47 @@ class MessageInputState extends State { } Widget _buildAttachments() { - return Wrap( - direction: Axis.horizontal, - children: _attachments - .map( - (attachment) => Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Stack( - children: [ - Container( - height: 50, - width: 50, - child: _buildAttachment(attachment), - ), - _buildRemoveButton(attachment), - attachment.uploaded - ? SizedBox() - : Positioned.fill( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CircularProgressIndicator(), + return _attachments.isEmpty + ? Container() + : LimitedBox( + maxHeight: 76.0, + child: ListView( + scrollDirection: Axis.horizontal, + children: _attachments + .map( + (attachment) => Padding( + padding: const EdgeInsets.all(8.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: Container( + height: 50, + width: 50, + child: _buildAttachment(attachment), ), ), - ), - ], - ), - ), + _buildRemoveButton(attachment), + attachment.uploaded + ? SizedBox() + : Positioned.fill( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ), + ), + ], + ), + ), + ), + ) + .toList(), ), - ) - .toList(), - ); + ); } Positioned _buildRemoveButton(_SendingAttachment attachment) { @@ -602,20 +770,39 @@ class MessageInputState extends State { } } - Material _buildAttachmentButton() { - return Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), + Widget _buildCommandButton() { + return Center( + child: InkWell( + child: Padding( + padding: const EdgeInsets.only( + left: 4.0, right: 8.0, top: 8.0, bottom: 8.0), + child: Icon(StreamIcons.lightning), + ), + onTap: () { + if (_commandsOverlay == null) { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } else { + _commandsOverlay?.remove(); + _commandsOverlay = null; + } + }, ), - color: Colors.transparent, - child: IconButton( - onPressed: () { + ); + } + + Widget _buildAttachmentButton() { + var padding = widget.editMessage == null ? 4.0 : 8.0; + return Center( + child: InkWell( + child: Padding( + padding: EdgeInsets.only( + left: 8.0, right: padding, top: 8.0, bottom: 8.0), + child: Icon(StreamIcons.attach), + ), + onTap: () { showAttachmentModal(); }, - icon: Icon( - Icons.add_circle_outline, - ), ), ); } @@ -832,24 +1019,44 @@ class MessageInputState extends State { return res.file; } + Widget _buildIdleSendButton(BuildContext context) { + return IconTheme( + data: + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: InkWell( + onTap: () { + sendMessage(); + }, + child: Icon( + StreamIcons.send_message, + color: Colors.grey, + ), + )), + ), + ); + } + Widget _buildSendButton(BuildContext context) { return IconTheme( data: StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, - child: Material( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(32), - ), - color: Colors.transparent, - child: IconButton( - key: Key('sendButton'), - onPressed: () { - sendMessage(); - }, - icon: Icon( - Icons.send, - color: StreamChatTheme.of(context).accentColor, + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + sendMessage(); + }, + child: Transform.rotate( + angle: widget.editMessage == null ? -pi / 2 : 0, + child: Icon( + widget.editMessage == null ? StreamIcons.send_message : StreamIcons.check_send, + color: StreamChatTheme.of(context).accentColor, + ), + ), ), ), ), @@ -858,11 +1065,15 @@ class MessageInputState extends State { /// Sends the current message void sendMessage() async { - final text = textEditingController.text.trim(); + var text = textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; } + if (_commandEnabled) { + text = '/${_chosenCommand.name} ' + text; + } + final attachments = List<_SendingAttachment>.from(_attachments); textEditingController.clear(); @@ -871,6 +1082,7 @@ class MessageInputState extends State { setState(() { _messageIsPresent = false; _typingStarted = false; + _commandEnabled = false; }); _commandsOverlay?.remove(); @@ -896,6 +1108,7 @@ class MessageInputState extends State { attachments: _getAttachments(attachments).toList(), mentionedUsers: _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), + showInChannel: widget.parentMessage != null ? _sendAsDm : null, ); } @@ -1016,3 +1229,9 @@ class _SendingAttachment { this.uploaded = false, }); } + +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${this.substring(1)}"; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 4ae31b50..edd19589 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: ">=2.3.0 <3.0.0" + sdk: ">=2.6.0 <3.0.0" dependencies: flutter: