diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index ae68bcb7..659a24eb 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -57,11 +57,19 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it calls [Navigator.pop] final VoidCallback onBackPressed; + /// Callback to call when the header is tapped. + final VoidCallback onTitleTap; + + /// Callback to call when the image is tapped. + final VoidCallback onImageTap; + /// Creates a channel header ChannelHeader({ Key key, this.showBackButton = true, this.onBackPressed, + this.onTitleTap, + this.onImageTap, }) : preferredSize = Size.fromHeight(kToolbarHeight), super(key: key); @@ -76,27 +84,33 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { actions: [ Padding( padding: const EdgeInsets.only(right: 10.0), - child: Stack( - children: [ - Center( - child: ChannelImage(), - ), - ], + child: Center( + child: ChannelImage( + onTap: onImageTap, + ), ), ), ], centerTitle: true, - title: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ChannelName( - textStyle: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .title, + title: InkWell( + onTap: onTitleTap, + child: Container( + height: preferredSize.height, + width: preferredSize.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, + ), + _buildLastActive(context, channel), + ], ), - _buildLastActive(context, channel), - ], + ), ), ); } diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index 4f6c3bf9..a52d9f96 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -48,6 +48,7 @@ class ChannelImage extends StatelessWidget { Key key, this.channel, this.constraints, + this.onTap, }) : super(key: key); /// The channel to show the image of @@ -56,6 +57,9 @@ class ChannelImage extends StatelessWidget { /// The diameter of the image final BoxConstraints constraints; + /// The function called when the image is tapped + final VoidCallback onTap; + @override Widget build(BuildContext context) { final client = StreamChat.of(context); @@ -87,25 +91,36 @@ class ChannelImage extends StatelessWidget { decoration: BoxDecoration( color: StreamChatTheme.of(context).accentColor, ), - child: image != null - ? CachedNetworkImage( - imageUrl: image, - errorWidget: (_, __, ___) { - return Center( - child: Text( - snapshot.data?.containsKey('name') ?? false - ? snapshot.data['name'][0] - : '', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ); - }, - fit: BoxFit.cover, - ) - : SizedBox(), + child: Stack( + fit: StackFit.expand, + children: [ + image != null + ? CachedNetworkImage( + imageUrl: image, + errorWidget: (_, __, ___) { + return Center( + child: Text( + snapshot.data?.containsKey('name') ?? false + ? snapshot.data['name'][0] + : '', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ); + }, + fit: BoxFit.cover, + ) + : SizedBox(), + Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + ), + ), + ], + ), ), ); }); diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index ce338fe4..ec6e99f5 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -58,6 +58,7 @@ class ChannelListView extends StatefulWidget { this.channelWidget, this.channelPreviewBuilder, this.errorBuilder, + this.onImageTap, }) : super(key: key); /// The builder that will be used in case of error @@ -97,6 +98,9 @@ class ChannelListView extends StatefulWidget { /// Builder used to create a custom channel preview final ChannelPreviewBuilder channelPreviewBuilder; + /// The function called when the image is tapped + final Function(Channel) onImageTap; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -219,11 +223,6 @@ class _ChannelListViewState extends State if (i < channels.length) { final channel = channels[i]; - final channelClient = channelsProvider.channels.firstWhere( - (c) => c.cid == channel.cid, - orElse: () => null, - ); - ChannelTapCallback onTap; if (widget.onChannelTap != null) { onTap = widget.onChannelTap; @@ -244,11 +243,11 @@ class _ChannelListViewState extends State } return StreamChannel( - key: ValueKey('CHANNEL-${channelClient.id}'), - channel: channelClient, + key: ValueKey('CHANNEL-${channel.id}'), + channel: channel, child: StreamBuilder( - initialData: channelClient.updatedAt, - stream: channelClient.updatedAtStream, + initialData: channel.updatedAt, + stream: channel.updatedAtStream, builder: (context, snapshot) { Widget child; if (widget.channelPreviewBuilder != null) { @@ -256,14 +255,14 @@ class _ChannelListViewState extends State children: [ widget.channelPreviewBuilder( context, - channelClient, + channel, ), Positioned.fill( child: Material( color: Colors.transparent, child: InkWell( onTap: () { - onTap(channelClient, widget.channelWidget); + onTap(channel, widget.channelWidget); }, ), ), @@ -272,9 +271,14 @@ class _ChannelListViewState extends State ); } else { child = ChannelPreview( - channel: channelClient, - onTap: (channelClient) { - onTap(channelClient, widget.channelWidget); + channel: channel, + onImageTap: widget.onImageTap != null + ? () { + widget.onImageTap(channel); + } + : null, + onTap: (channel) { + onTap(channel, widget.channelWidget); }, ); } diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index c4377ebc..487df6bc 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -25,11 +25,14 @@ class ChannelPreview extends StatelessWidget { /// Channel displayed final Channel channel; - /// Instantiate a new ChannelPreview + /// The function called when the image is tapped + final VoidCallback onImageTap; + ChannelPreview({ @required this.channel, Key key, this.onTap, + this.onImageTap, }) : super(key: key); @override @@ -38,7 +41,9 @@ class ChannelPreview extends StatelessWidget { onTap: () { onTap(channel); }, - leading: ChannelImage(), + leading: ChannelImage( + onTap: onImageTap, + ), title: ChannelName( textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, ), diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index d3d6482e..1c43c232 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -16,6 +16,8 @@ import 'package:stream_chat_flutter/src/user_avatar.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; +typedef FileUploader = Future Function(File, Channel); + /// Inactive state /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) @@ -68,11 +70,17 @@ class MessageInput extends StatefulWidget { this.maxHeight = 150, this.keyboardType = TextInputType.multiline, this.disableAttachments = false, + this.doImageUploadRequest, + this.doFileUploadRequest, + this.initialMessage, }) : super(key: key); /// Message to edit final Message editMessage; + /// Message to start with + final Message initialMessage; + /// Function called after sending the message final void Function(Message) onMessageSent; @@ -88,14 +96,25 @@ class MessageInput extends StatefulWidget { /// If true the attachments button will not be displayed final bool disableAttachments; + /// Override image upload request + final FileUploader doImageUploadRequest; + + /// Override file upload request + final FileUploader doFileUploadRequest; + @override - _MessageInputState createState() => _MessageInputState(); + _MessageInputState createState() => _MessageInputState( + doFileUploadRequest: doFileUploadRequest, + doImageUploadRequest: doImageUploadRequest, + ); } class _MessageInputState extends State { final List<_SendingAttachment> _attachments = []; final _focusNode = FocusNode(); final List _mentionedUsers = []; + FileUploader doImageUploadRequest; + FileUploader doFileUploadRequest; TextEditingController _textController; bool _inputEnabled = true; @@ -103,6 +122,14 @@ class _MessageInputState extends State { bool _typingStarted = false; OverlayEntry _commandsOverlay, _mentionsOverlay; + _MessageInputState({ + this.doImageUploadRequest, + this.doFileUploadRequest, + }) { + doImageUploadRequest ??= _uploadImage; + doFileUploadRequest ??= _uploadFile; + } + @override Widget build(BuildContext context) { return SafeArea( @@ -617,8 +644,6 @@ class _MessageInputState extends State { final channel = StreamChannel.of(context).channel; - final bytes = await file.readAsBytes(); - final attachment = _SendingAttachment( file: file, type: type, @@ -628,28 +653,7 @@ class _MessageInputState extends State { _attachments.add(attachment); }); - String url; - final filename = file.path.split('/').last; - - if (type == FileType.image) { - final res = await channel.sendImage( - MultipartFile.fromBytes( - bytes, - filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), - ), - ); - url = res.file; - } else { - final res = await channel.sendFile( - MultipartFile.fromBytes( - bytes, - filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), - ), - ); - url = res.file; - } + final url = await _uploadAttachment(file, type, channel); attachment.url = url; @@ -658,6 +662,46 @@ class _MessageInputState extends State { }); } + Future _uploadAttachment( + File file, + FileType type, + Channel channel, + ) async { + String url; + if (type == FileType.image) { + url = await doImageUploadRequest(file, channel); + } else { + url = await doFileUploadRequest(file, channel); + } + return url; + } + + Future _uploadImage(File file, Channel channel) async { + final filename = file.path.split('/').last; + final bytes = await file.readAsBytes(); + final res = await channel.sendImage( + MultipartFile.fromBytes( + bytes, + filename: filename, + contentType: MediaType.parse(lookupMimeType(filename)), + ), + ); + return res.file; + } + + Future _uploadFile(File file, Channel channel) async { + final filename = file.path.split('/').last; + final bytes = await file.readAsBytes(); + final res = await channel.sendFile( + MultipartFile.fromBytes( + bytes, + filename: filename, + contentType: MediaType.parse(lookupMimeType(filename)), + ), + ); + return res.file; + } + Widget _buildSendButton(BuildContext context) { return IconTheme( data: @@ -727,7 +771,7 @@ class _MessageInputState extends State { channel.cid, ); } else { - message = Message( + message = (widget.initialMessage ?? Message()).copyWith( parentId: widget.parentMessage?.id, text: text, attachments: _getAttachments(attachments).toList(), @@ -804,40 +848,46 @@ class _MessageInputState extends State { }); if (widget.editMessage != null) { - _textController = TextEditingController(text: widget.editMessage.text); - - _typingStarted = true; - _messageIsPresent = true; - - widget.editMessage.attachments.forEach((attachment) { - if (attachment.type == 'image') { - _attachments.add(_SendingAttachment( - type: FileType.image, - url: attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl ?? - attachment.ogScrapeUrl, - uploaded: true, - )); - } else if (attachment.type == 'video') { - _attachments.add(_SendingAttachment( - type: FileType.video, - url: attachment.assetUrl, - uploaded: true, - )); - } else if (attachment.type != 'giphy') { - _attachments.add(_SendingAttachment( - type: FileType.any, - url: attachment.assetUrl, - uploaded: true, - )); - } - }); + _parseExistingMessage(widget.editMessage); + } else if (widget.initialMessage != null) { + _parseExistingMessage(widget.initialMessage); } else { _textController = TextEditingController(); } } + void _parseExistingMessage(Message message) { + _textController = TextEditingController(text: message.text); + + _typingStarted = true; + _messageIsPresent = true; + + message.attachments?.forEach((attachment) { + if (attachment.type == 'image') { + _attachments.add(_SendingAttachment( + type: FileType.image, + url: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl ?? + attachment.ogScrapeUrl, + uploaded: true, + )); + } else if (attachment.type == 'video') { + _attachments.add(_SendingAttachment( + type: FileType.video, + url: attachment.assetUrl, + uploaded: true, + )); + } else if (attachment.type != 'giphy') { + _attachments.add(_SendingAttachment( + type: FileType.any, + url: attachment.assetUrl, + uploaded: true, + )); + } + }); + } + @override void dispose() { _commandsOverlay?.remove(); diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 7df86b4b..7289678d 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -214,7 +214,7 @@ class _MessageWidgetState extends State var nOfAttachmentWidgets = 0; final column = - List.from(widget.message.attachments?.map((attachment) { + List.from(widget.message.attachments.map((attachment) { nOfAttachmentWidgets++; Widget attachmentWidget; @@ -223,6 +223,21 @@ class _MessageWidgetState extends State } else if (attachment.type == 'image' || attachment.type == 'giphy') { attachmentWidget = _buildImage(attachment); + } else if (attachment.type == 'file') { + attachmentWidget = Material( + child: InkWell( + onTap: () { + _launchURL(attachment.assetUrl); + }, + child: Container( + width: 100, + height: 100, + child: Center( + child: Icon(Icons.attach_file), + ), + ), + ), + ); } if (attachmentWidget != null) {