diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index a060db61..b4a7df41 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -41,5 +41,14 @@ UIViewControllerBasedStatusBarAppearance + UIBackgroundModes + + fetch + remote-notification + + NSAppleMusicUsageDescription + Used to send message attachments + NSPhotoLibraryUsageDescription + Used to send message attachments diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index d6cb0f00..a586c704 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -22,79 +26,186 @@ class _MessageInputState extends State { final _textController = TextEditingController(); bool _messageIsPresent = false; bool _typingStarted = false; + final List<_SendingAttachment> _attachments = []; @override Widget build(BuildContext context) { return SafeArea( child: Padding( padding: const EdgeInsets.all(8.0), - child: Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: _typingStarted - ? StreamChatTheme.of(context).channelTheme.inputGradient - : null, - ), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10.0), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).channelTheme.inputBackground, - borderRadius: BorderRadius.circular(10.0), - border: Border.all( - color: _typingStarted - ? Colors.transparent - : Colors.black.withOpacity(.2)), - ), - child: Flex( - direction: Axis.horizontal, - children: [ - Expanded( - child: TextField( - minLines: null, - maxLines: null, - onSubmitted: (_) { - _sendMessage(context); - }, - controller: _textController, - onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - }); - }, - onTap: () { - setState(() { - _typingStarted = true; - }); - }, - style: Theme.of(context).textTheme.body1, - autofocus: false, - decoration: InputDecoration( - hintText: 'Write a message', - prefixText: ' ', - border: InputBorder.none, - ), + child: Stack( + overflow: Overflow.visible, + children: [ + Positioned.fill( + child: Container( + width: MediaQuery.of(context).size.width, + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: _typingStarted + ? StreamChatTheme.of(context).channelTheme.inputGradient + : null, + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + decoration: BoxDecoration( + color: StreamChatTheme.of(context) + .channelTheme + .inputBackground, + borderRadius: BorderRadius.circular(10.0), + border: Border.all( + color: _typingStarted + ? Colors.transparent + : Colors.black.withOpacity(.2)), ), ), - AnimatedCrossFade( - crossFadeState: _messageIsPresent - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: _buildSendButton(context), - secondChild: SizedBox(), - duration: Duration(milliseconds: 300), - alignment: Alignment.center, - ), - ], + ), ), ), - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAttachments(), + Flex( + direction: Axis.horizontal, + children: [ + _buildAttachmentButton(), + Expanded( + child: TextField( + minLines: null, + maxLines: null, + onSubmitted: (_) { + _sendMessage(context); + }, + controller: _textController, + onChanged: (s) { + StreamChannel.of(context).channel.keyStroke(); + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + }); + }, + onTap: () { + setState(() { + _typingStarted = true; + }); + }, + style: Theme.of(context).textTheme.body1, + autofocus: false, + decoration: InputDecoration( + hintText: 'Write a message', + prefixText: ' ', + border: InputBorder.none, + ), + ), + ), + AnimatedCrossFade( + crossFadeState: + (_messageIsPresent || _attachments.isNotEmpty) + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: _buildSendButton(context), + secondChild: SizedBox(), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + 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: Image.file(attachment.file), + ), + Positioned( + height: 16, + width: 16, + top: 4, + right: 4, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + disabledElevation: 0, + hoverElevation: 0, + onPressed: () { + setState(() { + _attachments.remove(attachment); + }); + }, + fillColor: Colors.white.withOpacity(.5), + child: Center( + child: Icon( + Icons.close, + size: 15, + color: Colors.black, + ), + ), + ), + ), + ], + ), + ), + ), + ) + .toList(), + ); + } + + Material _buildAttachmentButton() { + return Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + ), + color: Colors.transparent, + child: IconButton( + onPressed: () async { + final file = await FilePicker.getFile(type: FileType.IMAGE); + print(file.path); + + final channel = StreamChannel.of(context).channel; + + final bytes = await file.readAsBytes(); + final res = await channel.sendFile(MultipartFile.fromBytes( + bytes, + filename: file.path.split('/').last, + )); + + setState(() { + _attachments.add(_SendingAttachment( + url: res.file, + extension: file.path.split('.').last, + file: file, + )); + }); + }, + icon: Icon( + Icons.add_circle_outline, ), ), ); @@ -104,12 +215,19 @@ class _MessageInputState extends State { return IconTheme( data: StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, - child: IconButton( - onPressed: () { - _sendMessage(context); - }, - icon: Icon( - Icons.send, + child: Material( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(32), + ), + color: Colors.transparent, + child: IconButton( + onPressed: () { + _sendMessage(context); + }, + icon: Icon( + Icons.send, + ), ), ), ); @@ -117,11 +235,15 @@ class _MessageInputState extends State { void _sendMessage(BuildContext context) { final text = _textController.text.trim(); - if (text.isEmpty) { + if (text.isEmpty && _attachments.isEmpty) { return; } + final attachments = List.from(_attachments); + _textController.clear(); + _attachments.clear(); + setState(() { _messageIsPresent = false; _typingStarted = false; @@ -134,6 +256,13 @@ class _MessageInputState extends State { Message( parentId: widget.parentMessage?.id, text: text, + attachments: attachments.map((attachment) { + return Attachment( + imageUrl: attachment.url, + assetUrl: attachment.url, + type: 'image', + ); + }).toList(), ), ) .then((_) { @@ -143,3 +272,15 @@ class _MessageInputState extends State { }); } } + +class _SendingAttachment { + final String url; + final String extension; + final File file; + + _SendingAttachment({ + this.url, + this.extension, + this.file, + }); +} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index e6ecf5e7..6fd6e4ae 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -211,7 +211,7 @@ class _MessageWidgetState extends State borderRadius: boxDecoration.borderRadius, child: Container( decoration: boxDecoration, - constraints: BoxConstraints.loose(Size.fromWidth(300)), + constraints: BoxConstraints.loose(Size(300, 500)), child: Stack( children: [ Column( @@ -222,7 +222,7 @@ class _MessageWidgetState extends State attachment.title != null ? Container( constraints: - BoxConstraints.loose(Size.fromHeight(70)), + BoxConstraints.loose(Size(300, 500)), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -549,7 +549,8 @@ class _MessageWidgetState extends State Attachment attachment, ) { return CachedNetworkImage( - imageUrl: attachment.thumbUrl ?? attachment.imageUrl, + imageUrl: + attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl, fit: BoxFit.cover, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 3f13ea59..cf0220ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,9 @@ dependencies: url_launcher: ^5.4.1 video_player: ^0.10.7 chewie: ^0.9.8+1 - stream_chat: ^0.1.11 + file_picker: ^1.4.3+2 + stream_chat: + path: ../stream_chat_dart dev_dependencies: pedantic: ^1.8.0+1