From f10faee751fb6a3e98631d6d6c6f17fa54512aa3 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Nov 2020 16:02:04 +0530 Subject: [PATCH 01/25] feat: Added file picker section space --- lib/src/message_input.dart | 165 +++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 89 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 9c9cf67f..813ca707 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -172,6 +172,7 @@ class MessageInputState extends State { Command _chosenCommand; bool _actionsShrunk = false; bool _sendAsDm = false; + bool _openFilePickerSection = false; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -197,6 +198,8 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _buildDmCheckbox(), ), + if(_openFilePickerSection) + _buildFilePickerSection(), ], ), ), @@ -420,40 +423,6 @@ class MessageInputState extends State { ); } - Positioned _buildBorder(BuildContext context) { - return Positioned.fill( - child: Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: _getGradient(context), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .channelTheme - .inputBackground - .withAlpha(255), - 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 - : Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.2) - : Colors.black.withOpacity(.2)), - ), - ), - ), - ), - ); - } - OverlayEntry _buildCommandsOverlayEntry() { final text = textEditingController.text; final commands = StreamChannel.of(context) @@ -566,6 +535,12 @@ class MessageInputState extends State { }); } + Widget _buildFilePickerSection() { + return Container( + height: 200.0, + ); + } + OverlayEntry _buildMentionsOverlayEntry() { final splits = textEditingController.text .substring(0, textEditingController.value.selection.start) @@ -839,7 +814,13 @@ class MessageInputState extends State { ), ), onTap: () { - showAttachmentModal(); + if(_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + }); + } else { + showAttachmentModal(); + } }, ), ); @@ -851,73 +832,79 @@ class MessageInputState extends State { _focusNode.unfocus(); } - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), + if(!kIsWeb) { + setState(() { + _openFilePickerSection = true; + }); + } else { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), ), - ), - context: context, - isScrollControlled: true, - builder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text( - 'Add a file', - style: TextStyle( - fontWeight: FontWeight.bold, + context: context, + isScrollControlled: true, + builder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + 'Add a file', + style: TextStyle( + fontWeight: FontWeight.bold, + ), ), ), - ), - ListTile( - leading: Icon(Icons.image), - title: Text('Upload a photo'), - onTap: () { - pickFile(DefaultAttachmentTypes.image, false); - Navigator.pop(context); - }, - ), - ListTile( - leading: Icon(Icons.video_library), - title: Text('Upload a video'), - onTap: () { - pickFile(DefaultAttachmentTypes.video, false); - Navigator.pop(context); - }, - ), - if (!kIsWeb) ListTile( - leading: Icon(Icons.camera_alt), - title: Text('Photo from camera'), + leading: Icon(Icons.image), + title: Text('Upload a photo'), onTap: () { - pickFile(DefaultAttachmentTypes.image, true); + pickFile(DefaultAttachmentTypes.image, false); Navigator.pop(context); }, ), - if (!kIsWeb) ListTile( - leading: Icon(Icons.videocam), - title: Text('Video from camera'), + leading: Icon(Icons.video_library), + title: Text('Upload a video'), onTap: () { - pickFile(DefaultAttachmentTypes.video, true); + pickFile(DefaultAttachmentTypes.video, false); Navigator.pop(context); }, ), - ListTile( - leading: Icon(Icons.insert_drive_file), - title: Text('Upload a file'), - onTap: () { - pickFile(DefaultAttachmentTypes.file, false); - Navigator.pop(context); - }, - ), - ], - ); - }); + if (!kIsWeb) + ListTile( + leading: Icon(Icons.camera_alt), + title: Text('Photo from camera'), + onTap: () { + pickFile(DefaultAttachmentTypes.image, true); + Navigator.pop(context); + }, + ), + if (!kIsWeb) + ListTile( + leading: Icon(Icons.videocam), + title: Text('Video from camera'), + onTap: () { + pickFile(DefaultAttachmentTypes.video, true); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.insert_drive_file), + title: Text('Upload a file'), + onTap: () { + pickFile(DefaultAttachmentTypes.file, false); + Navigator.pop(context); + }, + ), + ], + ); + }); + } } /// Add an attachment to the sending message From 44ab00e1078b56b17abcbd2953738b375cdef000 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Nov 2020 17:21:23 +0530 Subject: [PATCH 02/25] feat: Added file picker sections and image support --- lib/src/message_input.dart | 148 ++++++++++++++++++++++++++++++++++++- pubspec.yaml | 1 + 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 813ca707..85878a40 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'package:file_picker/file_picker.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:http_parser/http_parser.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; +import 'package:photo_gallery/photo_gallery.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'; @@ -173,6 +175,8 @@ class MessageInputState extends State { bool _actionsShrunk = false; bool _sendAsDm = false; bool _openFilePickerSection = false; + int _filePickerIndex = 0; + Album _selectedAlbum; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -198,8 +202,7 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _buildDmCheckbox(), ), - if(_openFilePickerSection) - _buildFilePickerSection(), + if (_openFilePickerSection) _buildFilePickerSection(), ], ), ), @@ -538,9 +541,146 @@ class MessageInputState extends State { Widget _buildFilePickerSection() { return Container( height: 200.0, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + icon: Icon( + StreamIcons.picture, + color: _filePickerIndex == 0 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + setState(() { + _filePickerIndex = 0; + }); + }, + ), + IconButton( + icon: Icon( + StreamIcons.folder, + color: _filePickerIndex == 1 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + setState(() { + _filePickerIndex = 1; + }); + }, + ), + IconButton( + icon: Icon( + StreamIcons.camera, + color: _filePickerIndex == 2 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + setState(() { + _filePickerIndex = 2; + }); + }, + ), + ], + ), + Expanded( + child: _buildPickerSection(), + ), + ], + ), ); } + Widget _buildPickerSection() { + switch (_filePickerIndex) { + case 0: + if (_selectedAlbum != null) { + return FutureBuilder( + future: _selectedAlbum.listMedia(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + return GridView.builder( + itemCount: snapshot.data.total, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3), + itemBuilder: (context, position) { + return FutureBuilder( + future: snapshot.data.items[position].getFile(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return InkWell( + onTap: () { + + }, + child: AspectRatio( + child: Image.file(snapshot.data), + aspectRatio: 1.0, + ), + ); + }); + }, + ); + }); + } + + return FutureBuilder>( + future: PhotoGallery.listAlbums(mediumType: MediumType.image), + builder: (context, albumData) { + if (!albumData.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + return GridView.builder( + itemCount: albumData.data.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3), + itemBuilder: (context, position) { + return FutureBuilder>( + future: albumData.data[position].getThumbnail(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return InkWell( + child: AspectRatio( + child: Image.memory(snapshot.data), + aspectRatio: 1.0, + ), + onTap: () { + setState(() { + _selectedAlbum = albumData.data[position]; + }); + }, + ); + }); + }, + ); + }); + break; + case 1: + break; + case 2: + break; + } + } + OverlayEntry _buildMentionsOverlayEntry() { final splits = textEditingController.text .substring(0, textEditingController.value.selection.start) @@ -814,7 +954,7 @@ class MessageInputState extends State { ), ), onTap: () { - if(_openFilePickerSection) { + if (_openFilePickerSection) { setState(() { _openFilePickerSection = false; }); @@ -832,7 +972,7 @@ class MessageInputState extends State { _focusNode.unfocus(); } - if(!kIsWeb) { + if (!kIsWeb) { setState(() { _openFilePickerSection = true; }); diff --git a/pubspec.yaml b/pubspec.yaml index 452eb69a..70b8188a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 widgets_visibility_provider: ^2.0.2 + photo_gallery: ^0.3.0 flutter: assets: From 0de05143f4c427d71c5a16c3293726cf69926c00 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Nov 2020 19:15:19 +0530 Subject: [PATCH 03/25] feat: Images and video are now dsplayed --- lib/src/message_input.dart | 138 ++++++++++++++++++++++--------------- 1 file changed, 83 insertions(+), 55 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 85878a40..3a8132e2 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -176,7 +176,6 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; - Album _selectedAlbum; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -598,59 +597,23 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - if (_selectedAlbum != null) { - return FutureBuilder( - future: _selectedAlbum.listMedia(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - return GridView.builder( - itemCount: snapshot.data.total, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3), - itemBuilder: (context, position) { - return FutureBuilder( - future: snapshot.data.items[position].getFile(), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - return InkWell( - onTap: () { - - }, - child: AspectRatio( - child: Image.file(snapshot.data), - aspectRatio: 1.0, - ), - ); - }); - }, - ); - }); - } - - return FutureBuilder>( - future: PhotoGallery.listAlbums(mediumType: MediumType.image), - builder: (context, albumData) { - if (!albumData.hasData) { + return FutureBuilder>( + future: _getAllMedia(MediumType.image), + builder: (context, mediaData) { + if (!mediaData.hasData) { return Center( child: CircularProgressIndicator(), ); } + return GridView.builder( - itemCount: albumData.data.length, + itemCount: mediaData.data.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3), itemBuilder: (context, position) { - return FutureBuilder>( - future: albumData.data[position].getThumbnail(), + return FutureBuilder>( + future: PhotoGallery.getThumbnail( + mediumId: mediaData.data[position].id), builder: (context, snapshot) { if (!snapshot.hasData) { return Center( @@ -658,16 +621,22 @@ class MessageInputState extends State { ); } - return InkWell( - child: AspectRatio( - child: Image.memory(snapshot.data), - aspectRatio: 1.0, + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 1.0, vertical: 1.0), + child: InkWell( + child: AspectRatio( + child: Image.memory( + snapshot.data, + fit: mediaData.data[position].height > + mediaData.data[position].width + ? BoxFit.fitWidth + : BoxFit.fitHeight, + ), + aspectRatio: 1.0, + ), + onTap: () {}, ), - onTap: () { - setState(() { - _selectedAlbum = albumData.data[position]; - }); - }, ); }); }, @@ -675,12 +644,71 @@ class MessageInputState extends State { }); break; case 1: + return FutureBuilder>( + future: _getAllMedia(MediumType.video), + builder: (context, mediaData) { + if (!mediaData.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return GridView.builder( + itemCount: mediaData.data.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3), + itemBuilder: (context, position) { + return FutureBuilder>( + future: PhotoGallery.getThumbnail( + mediumId: mediaData.data[position].id), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 1.0, vertical: 1.0), + child: InkWell( + child: AspectRatio( + child: Image.memory( + snapshot.data, + fit: mediaData.data[position].height > + mediaData.data[position].width + ? BoxFit.fitWidth + : BoxFit.fitHeight, + ), + aspectRatio: 1.0, + ), + onTap: () {}, + ), + ); + }); + }, + ); + }); break; case 2: break; } } + Future> _getAllMedia(MediumType type) async { + var allAlbums = await PhotoGallery.listAlbums(mediumType: type); + List resultList = []; + + for (var album in allAlbums) { + var data = await album.listMedia(); + resultList.addAll(data.items); + } + + resultList.sort((a, b) => a.modifiedDate.compareTo(b.modifiedDate)); + + return resultList; + } + OverlayEntry _buildMentionsOverlayEntry() { final splits = textEditingController.text .substring(0, textEditingController.value.selection.start) From 5371eb3aae8f5faa4c3d1b8ac61629ded5b14387 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 6 Nov 2020 15:26:34 +0530 Subject: [PATCH 04/25] feat: Attachments are now supported --- lib/src/message_input.dart | 192 +++++++++++++++++++++++-------------- 1 file changed, 121 insertions(+), 71 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 3a8132e2..be06eb10 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math'; import 'package:file_picker/file_picker.dart'; @@ -176,6 +175,7 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; + double _filePickerSize = 250.0; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -539,7 +539,8 @@ class MessageInputState extends State { Widget _buildFilePickerSection() { return Container( - height: 200.0, + color: Color(0xFFF2F2F2), + height: _filePickerSize, child: Column( children: [ Row( @@ -566,9 +567,7 @@ class MessageInputState extends State { : Colors.black.withOpacity(0.5), ), onPressed: () { - setState(() { - _filePickerIndex = 1; - }); + pickFile(DefaultAttachmentTypes.file, false); }, ), IconButton( @@ -579,15 +578,54 @@ class MessageInputState extends State { : Colors.black.withOpacity(0.5), ), onPressed: () { - setState(() { - _filePickerIndex = 2; - }); + pickFile(DefaultAttachmentTypes.image, true); }, ), ], ), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onVerticalDragUpdate: (update) { + setState(() { + _filePickerSize -= update.delta.dy; + if (_filePickerSize < 100) { + _filePickerSize = 100.0; + } + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 40.0, + height: 4.0, + decoration: BoxDecoration( + color: Color(0xFFF2F2F2), + borderRadius: BorderRadius.circular(4.0), + ), + ), + ), + ), + ], + ), + ), Expanded( - child: _buildPickerSection(), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + ), + child: _buildPickerSection(), + ), ), ], ), @@ -598,7 +636,7 @@ class MessageInputState extends State { switch (_filePickerIndex) { case 0: return FutureBuilder>( - future: _getAllMedia(MediumType.image), + future: _getAllMedia(), builder: (context, mediaData) { if (!mediaData.hasData) { return Center( @@ -635,7 +673,9 @@ class MessageInputState extends State { ), aspectRatio: 1.0, ), - onTap: () {}, + onTap: () { + _addAttachment(mediaData.data[position]); + }, ), ); }); @@ -644,59 +684,70 @@ class MessageInputState extends State { }); break; case 1: - return FutureBuilder>( - future: _getAllMedia(MediumType.video), - builder: (context, mediaData) { - if (!mediaData.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - return GridView.builder( - itemCount: mediaData.data.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3), - itemBuilder: (context, position) { - return FutureBuilder>( - future: PhotoGallery.getThumbnail( - mediumId: mediaData.data[position].id), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, vertical: 1.0), - child: InkWell( - child: AspectRatio( - child: Image.memory( - snapshot.data, - fit: mediaData.data[position].height > - mediaData.data[position].width - ? BoxFit.fitWidth - : BoxFit.fitHeight, - ), - aspectRatio: 1.0, - ), - onTap: () {}, - ), - ); - }); - }, - ); - }); break; case 2: break; } } - Future> _getAllMedia(MediumType type) async { - var allAlbums = await PhotoGallery.listAlbums(mediumType: type); + void _addAttachment(Medium medium) async { + var mediaFile = await PhotoGallery.getFile(mediumId: medium.id); + + var file = PlatformFile( + path: mediaFile.path, + bytes: mediaFile.readAsBytesSync(), + ); + + setState(() { + _inputEnabled = true; + }); + + if (file == null) { + return; + } + + final channel = StreamChannel.of(context).channel; + final attachment = _SendingAttachment( + file: file, + attachment: Attachment( + localUri: file.path != null ? Uri.parse(file.path) : null, + type: medium.mediumType == MediumType.image ? 'image' : 'video', + ), + ); + + setState(() { + _attachments.add(attachment); + }); + + final url = await _uploadAttachment( + file, + medium.mediumType == MediumType.image + ? DefaultAttachmentTypes.image + : DefaultAttachmentTypes.video, + channel); + + var fileType = medium.mediumType == MediumType.image + ? DefaultAttachmentTypes.image + : DefaultAttachmentTypes.video; + + if (fileType == DefaultAttachmentTypes.image) { + attachment.attachment = attachment.attachment.copyWith( + imageUrl: url, + ); + } else { + attachment.attachment = attachment.attachment.copyWith( + assetUrl: url, + ); + } + + setState(() { + attachment.uploaded = true; + }); + } + + Future> _getAllMedia() async { + var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); + //var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; for (var album in allAlbums) { @@ -704,6 +755,11 @@ class MessageInputState extends State { resultList.addAll(data.items); } + // for (var album in allVideoAlbums) { + // var data = await album.listMedia(); + // resultList.addAll(data.items); + // } + resultList.sort((a, b) => a.modifiedDate.compareTo(b.modifiedDate)); return resultList; @@ -819,19 +875,6 @@ class MessageInputState extends State { _commandsOverlay = null; } - Gradient _getGradient(BuildContext context) { - if (_typingStarted) { - if (widget.editMessage == null) { - return StreamChatTheme.of(context).channelTheme.inputGradient; - } - return LinearGradient( - colors: [Colors.lightGreen, Colors.green], - ); - } else { - return null; - } - } - Widget _buildAttachments() { return _attachments.isEmpty ? Container() @@ -985,6 +1028,7 @@ class MessageInputState extends State { if (_openFilePickerSection) { setState(() { _openFilePickerSection = false; + _filePickerSize = 250.0; }); } else { showAttachmentModal(); @@ -1410,6 +1454,12 @@ class MessageInputState extends State { if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage); } + + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + _openFilePickerSection = false; + } + }); } void _parseExistingMessage(Message message) { From 24eeb337edb7055876188384bb1c9fcbab1d0e67 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 6 Nov 2020 15:30:39 +0530 Subject: [PATCH 05/25] feat: Attachments are now supported --- lib/src/message_input.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index be06eb10..5e4c7104 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -587,8 +587,8 @@ class MessageInputState extends State { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0), + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), ), ), child: Row( From 38c06d5de78b7e0be7c48c97aa0121ed9d85c10b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 13:02:46 +0530 Subject: [PATCH 06/25] fix: Fixed overflow bug --- lib/src/message_input.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 5e4c7104..d3b568a6 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -600,6 +600,8 @@ class MessageInputState extends State { _filePickerSize -= update.delta.dy; if (_filePickerSize < 100) { _filePickerSize = 100.0; + } else if (_filePickerSize > 500) { + _filePickerSize = 500; } }); }, From 812f70284552d9827982c6b29d4ebd80f61d5826 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 14:21:32 +0530 Subject: [PATCH 07/25] feat: Added selected image --- lib/src/message_input.dart | 467 +++++++++++++++++++++---------------- 1 file changed, 261 insertions(+), 206 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index d3b568a6..efb3af5e 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -20,9 +20,9 @@ import 'stream_channel.dart'; typedef FileUploader = Future Function(PlatformFile, Channel); typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - _SendingAttachment, -); + BuildContext, + _SendingAttachment, + ); enum ActionsLocation { left, @@ -230,7 +230,7 @@ class MessageInputState extends State { Checkbox( value: _sendAsDm, onChanged: (val) => setState( - () { + () { _sendAsDm = val; }, ), @@ -247,7 +247,7 @@ class MessageInputState extends State { AnimatedCrossFade _animateSendButton(BuildContext context) { return AnimatedCrossFade( crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && - _attachments.every((a) => a.uploaded == true)) + _attachments.every((a) => a.uploaded == true)) ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: _buildSendButton(context), @@ -260,7 +260,7 @@ class MessageInputState extends State { Widget _buildExpandActionsButton() { return AnimatedCrossFade( crossFadeState: - _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, + _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton( onPressed: () { setState(() { @@ -353,7 +353,7 @@ class MessageInputState extends State { (s[textEditingController.selection.start - 1] == '@' || textEditingController.text .substring( - 0, textEditingController.selection.start) + 0, textEditingController.selection.start) .split(' ') .last .contains('@'))) { @@ -371,9 +371,9 @@ class MessageInputState extends State { textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( hintText: - (_commandEnabled && _chosenCommand.name == 'giphy') - ? 'Search GIFs' - : 'Write a message', + (_commandEnabled && _chosenCommand.name == 'giphy') + ? 'Search GIFs' + : 'Write a message', prefixText: _commandEnabled ? null : ' ', border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent)), @@ -388,31 +388,31 @@ class MessageInputState extends State { 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, - ), - ), - ) + 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; - }); - }, - ) + icon: Icon(Icons.cancel_outlined), + onPressed: () { + setState(() { + _commandEnabled = false; + }); + }, + ) : null, ), textCapitalization: TextCapitalization.sentences, @@ -454,13 +454,13 @@ class MessageInputState extends State { child: Container( constraints: BoxConstraints.loose(Size.fromHeight(400)), decoration: BoxDecoration( - // boxShadow: [ - // BoxShadow( - // spreadRadius: -8, - // blurRadius: 5.0, - // offset: Offset(0, -4), - // ), - // ], + // boxShadow: [ + // BoxShadow( + // spreadRadius: -8, + // blurRadius: 5.0, + // offset: Offset(0, -4), + // ), + // ], color: StreamChatTheme.of(context).primaryColor, borderRadius: BorderRadius.circular(8.0)), child: ListView( @@ -474,7 +474,7 @@ class MessageInputState extends State { children: [ Padding( padding: - const EdgeInsets.symmetric(horizontal: 8.0), + const EdgeInsets.symmetric(horizontal: 8.0), child: Icon(StreamIcons.lightning, color: StreamChatTheme.of(context).accentColor), ), @@ -485,48 +485,48 @@ class MessageInputState extends State { ...commands .map( (c) => ListTile( - leading: c.name == 'giphy' - ? CircleAvatar( - backgroundColor: Colors.black, - child: Image.asset( - 'images/giphy_icon.png', - package: 'stream_chat_flutter', - width: 16.0, - height: 16.0, - ), - maxRadius: 12.0, - ) - : null, - 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: 12, - ), - //subtitle: Text(c.description), - onTap: () { - _setCommand(c); - }, + leading: c.name == 'giphy' + ? CircleAvatar( + backgroundColor: Colors.black, + child: Image.asset( + 'images/giphy_icon.png', + package: 'stream_chat_flutter', + width: 16.0, + height: 16.0, ), + maxRadius: 12.0, ) + : null, + 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: 12, + ), + //subtitle: Text(c.description), + onTap: () { + _setCommand(c); + }, + ), + ) .toList(), ], ), @@ -637,7 +637,7 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - return FutureBuilder>( + return FutureBuilder( future: _getAllMedia(), builder: (context, mediaData) { if (!mediaData.hasData) { @@ -647,40 +647,46 @@ class MessageInputState extends State { } return GridView.builder( - itemCount: mediaData.data.length, + itemCount: mediaData.data.item2.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3), itemBuilder: (context, position) { - return FutureBuilder>( - future: PhotoGallery.getThumbnail( - mediumId: mediaData.data[position].id), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, vertical: 1.0), - child: InkWell( - child: AspectRatio( - child: Image.memory( - snapshot.data, - fit: mediaData.data[position].height > - mediaData.data[position].width - ? BoxFit.fitWidth - : BoxFit.fitHeight, - ), - aspectRatio: 1.0, + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 1.0, vertical: 1.0), + child: InkWell( + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: Image.memory( + mediaData.data.item2[position], + fit: mediaData.data.item1[position].height > + mediaData.data.item1[position].width + ? BoxFit.fitWidth + : BoxFit.fitHeight, ), - onTap: () { - _addAttachment(mediaData.data[position]); - }, ), - ); - }); + if (_attachments.any((element) => + element.id == + mediaData.data.item1[position].id)) + Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: Icon( + StreamIcons.check_send, + color: Colors.white, + size: 24.0, + ), + ), + ), + ], + ), + onTap: () { + _addAttachment(mediaData.data.item1[position]); + }, + ), + ); }, ); }); @@ -715,6 +721,7 @@ class MessageInputState extends State { localUri: file.path != null ? Uri.parse(file.path) : null, type: medium.mediumType == MediumType.image ? 'image' : 'video', ), + id: medium.id, ); setState(() { @@ -747,10 +754,11 @@ class MessageInputState extends State { }); } - Future> _getAllMedia() async { + Future, List>> _getAllMedia() async { var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); //var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; + List> resultThumbnailList = []; for (var album in allAlbums) { var data = await album.listMedia(); @@ -764,7 +772,11 @@ class MessageInputState extends State { resultList.sort((a, b) => a.modifiedDate.compareTo(b.modifiedDate)); - return resultList; + for (var e in resultList) { + resultThumbnailList.add(await PhotoGallery.getThumbnail(mediumId: e.id)); + } + + return Tuple2, List>(resultList, resultThumbnailList); } OverlayEntry _buildMentionsOverlayEntry() { @@ -784,8 +796,8 @@ class MessageInputState extends State { } final members = StreamChannel.of(context).channel.state.members?.where((m) { - return m.user.name.toLowerCase().contains(query); - })?.toList() ?? + return m.user.name.toLowerCase().contains(query); + })?.toList() ?? []; RenderBox renderBox = context.findRenderObject(); @@ -825,38 +837,38 @@ class MessageInputState extends State { shrinkWrap: true, children: snapshot.data .map((m) => ListTile( - leading: UserAvatar( - user: m.user, - ), - title: Text( - '${m.user.name}', - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('@${m.userId}'), - trailing: Icon( - StreamIcons.at_mention, - color: StreamChatTheme.of(context).accentColor, - ), - onTap: () { - _mentionedUsers.add(m.user); + leading: UserAvatar( + user: m.user, + ), + title: Text( + '${m.user.name}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('@${m.userId}'), + trailing: Icon( + StreamIcons.at_mention, + color: StreamChatTheme.of(context).accentColor, + ), + onTap: () { + _mentionedUsers.add(m.user); - splits[splits.length - 1] = m.user.name; - final rejoin = splits.join('@'); + splits[splits.length - 1] = m.user.name; + final rejoin = splits.join('@'); - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController - .selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController + .selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - )) + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + }, + )) .toList(), ); }), @@ -881,44 +893,44 @@ class MessageInputState extends State { 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(), - ), - ), - ), - ], - ), + 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) { @@ -954,7 +966,7 @@ class MessageInputState extends State { Widget _buildAttachment(_SendingAttachment attachment) { if (widget.attachmentThumbnailBuilders - ?.containsKey(attachment.attachment.type) == + ?.containsKey(attachment.attachment.type) == true) { return widget.attachmentThumbnailBuilders[attachment.attachment.type]( context, @@ -967,14 +979,14 @@ class MessageInputState extends State { case 'giphy': return attachment.file != null ? Image.memory( - attachment.file.bytes, - fit: BoxFit.cover, - ) + attachment.file.bytes, + fit: BoxFit.cover, + ) : Image.network( - attachment.attachment.imageUrl ?? - attachment.attachment.thumbUrl, - fit: BoxFit.cover, - ); + attachment.attachment.imageUrl ?? + attachment.attachment.thumbUrl, + fit: BoxFit.cover, + ); break; case 'video': return Container( @@ -1020,7 +1032,7 @@ class MessageInputState extends State { child: InkWell( child: Padding( padding: - EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0), + EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0), child: Icon( StreamIcons.attach, color: Color(0xFF000000).withAlpha(128), @@ -1220,10 +1232,10 @@ class MessageInputState extends State { } Future _uploadAttachment( - PlatformFile file, - DefaultAttachmentTypes type, - Channel channel, - ) async { + PlatformFile file, + DefaultAttachmentTypes type, + Channel channel, + ) async { String url; if (type == DefaultAttachmentTypes.image) { if (widget.doImageUploadRequest != null) { @@ -1270,19 +1282,19 @@ class MessageInputState extends State { Widget _buildIdleSendButton(BuildContext context) { return IconTheme( data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, child: Padding( padding: const EdgeInsets.all(8.0), child: Center( child: InkWell( - onTap: () { - sendMessage(); - }, - child: Icon( - _getIdleSendIcon(), - color: Colors.grey, - ), - )), + onTap: () { + sendMessage(); + }, + child: Icon( + _getIdleSendIcon(), + color: Colors.grey, + ), + )), ), ); } @@ -1290,7 +1302,7 @@ class MessageInputState extends State { Widget _buildSendButton(BuildContext context) { return IconTheme( data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, child: Center( child: Padding( padding: const EdgeInsets.all(8.0), @@ -1370,7 +1382,7 @@ class MessageInputState extends State { text: text, attachments: _getAttachments(attachments).toList(), mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), + _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), ); } else { message = (widget.initialMessage ?? Message()).copyWith( @@ -1378,7 +1390,7 @@ class MessageInputState extends State { text: text, attachments: _getAttachments(attachments).toList(), mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), + _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), showInChannel: widget.parentMessage != null ? _sendAsDm : null, ); } @@ -1392,9 +1404,9 @@ class MessageInputState extends State { sendingFuture = channel.sendMessage(message); } else { sendingFuture = StreamChat.of(context).client.updateMessage( - message, - channel.cid, - ); + message, + channel.cid, + ); } return sendingFuture.then((resp) { @@ -1501,11 +1513,13 @@ class _SendingAttachment { PlatformFile file; Attachment attachment; bool uploaded; + String id; _SendingAttachment({ this.file, this.attachment, this.uploaded = false, + this.id, }); } @@ -1514,3 +1528,44 @@ extension StringExtension on String { return "${this[0].toUpperCase()}${this.substring(1)}"; } } + +/// Represents a 2-tuple, or pair. +class Tuple2 { + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Creates a new tuple value with the specified items. + const Tuple2(this.item1, this.item2); + + /// Create a new tuple value with the specified list [items]. + factory Tuple2.fromList(List items) { + if (items.length != 2) { + throw ArgumentError('items must have length 2'); + } + + return Tuple2(items[0] as T1, items[1] as T2); + } + + /// Returns a tuple with the first item set to the specified value. + Tuple2 withItem1(T1 v) => Tuple2(v, item2); + + /// Returns a tuple with the second item set to the specified value. + Tuple2 withItem2(T2 v) => Tuple2(item1, v); + + /// Creates a [List] containing the items of this [Tuple2]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2], growable: growable); + + @override + String toString() => '[$item1, $item2]'; + + @override + bool operator ==(Object other) => + other is Tuple2 && other.item1 == item1 && other.item2 == item2; +} From a06b7c2cd88d7264b769027bfcde506143ad03b5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 14:38:25 +0530 Subject: [PATCH 08/25] feat: Added remove image --- lib/src/message_input.dart | 360 +++++++++++++++++++------------------ 1 file changed, 183 insertions(+), 177 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index efb3af5e..72066008 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -20,9 +20,9 @@ import 'stream_channel.dart'; typedef FileUploader = Future Function(PlatformFile, Channel); typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - _SendingAttachment, - ); + BuildContext, + _SendingAttachment, +); enum ActionsLocation { left, @@ -230,7 +230,7 @@ class MessageInputState extends State { Checkbox( value: _sendAsDm, onChanged: (val) => setState( - () { + () { _sendAsDm = val; }, ), @@ -247,7 +247,7 @@ class MessageInputState extends State { AnimatedCrossFade _animateSendButton(BuildContext context) { return AnimatedCrossFade( crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) && - _attachments.every((a) => a.uploaded == true)) + _attachments.every((a) => a.uploaded == true)) ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: _buildSendButton(context), @@ -260,7 +260,7 @@ class MessageInputState extends State { Widget _buildExpandActionsButton() { return AnimatedCrossFade( crossFadeState: - _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, + _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton( onPressed: () { setState(() { @@ -353,7 +353,7 @@ class MessageInputState extends State { (s[textEditingController.selection.start - 1] == '@' || textEditingController.text .substring( - 0, textEditingController.selection.start) + 0, textEditingController.selection.start) .split(' ') .last .contains('@'))) { @@ -371,9 +371,9 @@ class MessageInputState extends State { textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( hintText: - (_commandEnabled && _chosenCommand.name == 'giphy') - ? 'Search GIFs' - : 'Write a message', + (_commandEnabled && _chosenCommand.name == 'giphy') + ? 'Search GIFs' + : 'Write a message', prefixText: _commandEnabled ? null : ' ', border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent)), @@ -388,31 +388,31 @@ class MessageInputState extends State { 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, - ), - ), - ) + 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; - }); - }, - ) + icon: Icon(Icons.cancel_outlined), + onPressed: () { + setState(() { + _commandEnabled = false; + }); + }, + ) : null, ), textCapitalization: TextCapitalization.sentences, @@ -454,13 +454,13 @@ class MessageInputState extends State { child: Container( constraints: BoxConstraints.loose(Size.fromHeight(400)), decoration: BoxDecoration( - // boxShadow: [ - // BoxShadow( - // spreadRadius: -8, - // blurRadius: 5.0, - // offset: Offset(0, -4), - // ), - // ], + // boxShadow: [ + // BoxShadow( + // spreadRadius: -8, + // blurRadius: 5.0, + // offset: Offset(0, -4), + // ), + // ], color: StreamChatTheme.of(context).primaryColor, borderRadius: BorderRadius.circular(8.0)), child: ListView( @@ -474,7 +474,7 @@ class MessageInputState extends State { children: [ Padding( padding: - const EdgeInsets.symmetric(horizontal: 8.0), + const EdgeInsets.symmetric(horizontal: 8.0), child: Icon(StreamIcons.lightning, color: StreamChatTheme.of(context).accentColor), ), @@ -485,48 +485,48 @@ class MessageInputState extends State { ...commands .map( (c) => ListTile( - leading: c.name == 'giphy' - ? CircleAvatar( - backgroundColor: Colors.black, - child: Image.asset( - 'images/giphy_icon.png', - package: 'stream_chat_flutter', - width: 16.0, - height: 16.0, - ), - maxRadius: 12.0, - ) - : null, - title: Text.rich( - TextSpan( - text: '${c.name.capitalize()}', - style: TextStyle(fontWeight: FontWeight.bold), - children: [ + leading: c.name == 'giphy' + ? CircleAvatar( + backgroundColor: Colors.black, + child: Image.asset( + 'images/giphy_icon.png', + package: 'stream_chat_flutter', + width: 16.0, + height: 16.0, + ), + maxRadius: 12.0, + ) + : null, + title: Text.rich( TextSpan( - text: ' /${c.name} ${c.args}', - style: TextStyle( - fontWeight: FontWeight.w300, - ), + 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: 12, + ), + //subtitle: Text(c.description), + onTap: () { + _setCommand(c); + }, ), - ), - trailing: CircleAvatar( - backgroundColor: - StreamChatTheme.of(context).accentColor, - child: Icon( - StreamIcons.lightning, - color: Colors.white, - size: 12.5, - ), - maxRadius: 12, - ), - //subtitle: Text(c.description), - onTap: () { - _setCommand(c); - }, - ), - ) + ) .toList(), ], ), @@ -662,14 +662,13 @@ class MessageInputState extends State { child: Image.memory( mediaData.data.item2[position], fit: mediaData.data.item1[position].height > - mediaData.data.item1[position].width + mediaData.data.item1[position].width ? BoxFit.fitWidth : BoxFit.fitHeight, ), ), if (_attachments.any((element) => - element.id == - mediaData.data.item1[position].id)) + element.id == mediaData.data.item1[position].id)) Container( color: Colors.black.withOpacity(0.5), child: Center( @@ -683,7 +682,14 @@ class MessageInputState extends State { ], ), onTap: () { - _addAttachment(mediaData.data.item1[position]); + if (!_attachments.any((element) => + element.id == mediaData.data.item1[position].id)) { + _addAttachment(mediaData.data.item1[position]); + } else { + _attachments.removeWhere((element) => + element.id == mediaData.data.item1[position].id); + setState(() {}); + } }, ), ); @@ -796,8 +802,8 @@ class MessageInputState extends State { } final members = StreamChannel.of(context).channel.state.members?.where((m) { - return m.user.name.toLowerCase().contains(query); - })?.toList() ?? + return m.user.name.toLowerCase().contains(query); + })?.toList() ?? []; RenderBox renderBox = context.findRenderObject(); @@ -837,38 +843,38 @@ class MessageInputState extends State { shrinkWrap: true, children: snapshot.data .map((m) => ListTile( - leading: UserAvatar( - user: m.user, - ), - title: Text( - '${m.user.name}', - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('@${m.userId}'), - trailing: Icon( - StreamIcons.at_mention, - color: StreamChatTheme.of(context).accentColor, - ), - onTap: () { - _mentionedUsers.add(m.user); + leading: UserAvatar( + user: m.user, + ), + title: Text( + '${m.user.name}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('@${m.userId}'), + trailing: Icon( + StreamIcons.at_mention, + color: StreamChatTheme.of(context).accentColor, + ), + onTap: () { + _mentionedUsers.add(m.user); - splits[splits.length - 1] = m.user.name; - final rejoin = splits.join('@'); + splits[splits.length - 1] = m.user.name; + final rejoin = splits.join('@'); - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController - .selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController + .selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - )) + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + }, + )) .toList(), ); }), @@ -893,44 +899,44 @@ class MessageInputState extends State { 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(), + 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) { @@ -966,7 +972,7 @@ class MessageInputState extends State { Widget _buildAttachment(_SendingAttachment attachment) { if (widget.attachmentThumbnailBuilders - ?.containsKey(attachment.attachment.type) == + ?.containsKey(attachment.attachment.type) == true) { return widget.attachmentThumbnailBuilders[attachment.attachment.type]( context, @@ -979,14 +985,14 @@ class MessageInputState extends State { case 'giphy': return attachment.file != null ? Image.memory( - attachment.file.bytes, - fit: BoxFit.cover, - ) + attachment.file.bytes, + fit: BoxFit.cover, + ) : Image.network( - attachment.attachment.imageUrl ?? - attachment.attachment.thumbUrl, - fit: BoxFit.cover, - ); + attachment.attachment.imageUrl ?? + attachment.attachment.thumbUrl, + fit: BoxFit.cover, + ); break; case 'video': return Container( @@ -1032,7 +1038,7 @@ class MessageInputState extends State { child: InkWell( child: Padding( padding: - EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0), + EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0), child: Icon( StreamIcons.attach, color: Color(0xFF000000).withAlpha(128), @@ -1232,10 +1238,10 @@ class MessageInputState extends State { } Future _uploadAttachment( - PlatformFile file, - DefaultAttachmentTypes type, - Channel channel, - ) async { + PlatformFile file, + DefaultAttachmentTypes type, + Channel channel, + ) async { String url; if (type == DefaultAttachmentTypes.image) { if (widget.doImageUploadRequest != null) { @@ -1282,19 +1288,19 @@ class MessageInputState extends State { Widget _buildIdleSendButton(BuildContext context) { return IconTheme( data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, child: Padding( padding: const EdgeInsets.all(8.0), child: Center( child: InkWell( - onTap: () { - sendMessage(); - }, - child: Icon( - _getIdleSendIcon(), - color: Colors.grey, - ), - )), + onTap: () { + sendMessage(); + }, + child: Icon( + _getIdleSendIcon(), + color: Colors.grey, + ), + )), ), ); } @@ -1302,7 +1308,7 @@ class MessageInputState extends State { Widget _buildSendButton(BuildContext context) { return IconTheme( data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, child: Center( child: Padding( padding: const EdgeInsets.all(8.0), @@ -1382,7 +1388,7 @@ class MessageInputState extends State { text: text, attachments: _getAttachments(attachments).toList(), mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), + _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), ); } else { message = (widget.initialMessage ?? Message()).copyWith( @@ -1390,7 +1396,7 @@ class MessageInputState extends State { text: text, attachments: _getAttachments(attachments).toList(), mentionedUsers: - _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), + _mentionedUsers.where((u) => text.contains('@${u.name}')).toList(), showInChannel: widget.parentMessage != null ? _sendAsDm : null, ); } @@ -1404,9 +1410,9 @@ class MessageInputState extends State { sendingFuture = channel.sendMessage(message); } else { sendingFuture = StreamChat.of(context).client.updateMessage( - message, - channel.cid, - ); + message, + channel.cid, + ); } return sendingFuture.then((resp) { From 909587a127d5d5e2caa25d7e0a83de60e7ff6849 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 14:50:09 +0530 Subject: [PATCH 09/25] fix: Fixed picture flickering --- lib/src/message_input.dart | 111 ++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 58 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 72066008..74ed535a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -177,6 +177,8 @@ class MessageInputState extends State { int _filePickerIndex = 0; double _filePickerSize = 250.0; + Tuple2, List> _mediaData = Tuple2([], []); + /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -637,65 +639,55 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - return FutureBuilder( - future: _getAllMedia(), - builder: (context, mediaData) { - if (!mediaData.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - return GridView.builder( - itemCount: mediaData.data.item2.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3), - itemBuilder: (context, position) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, vertical: 1.0), - child: InkWell( - child: Stack( - children: [ - AspectRatio( - aspectRatio: 1.0, - child: Image.memory( - mediaData.data.item2[position], - fit: mediaData.data.item1[position].height > - mediaData.data.item1[position].width - ? BoxFit.fitWidth - : BoxFit.fitHeight, - ), - ), - if (_attachments.any((element) => - element.id == mediaData.data.item1[position].id)) - Container( - color: Colors.black.withOpacity(0.5), - child: Center( - child: Icon( - StreamIcons.check_send, - color: Colors.white, - size: 24.0, - ), - ), - ), - ], + return GridView.builder( + itemCount: _mediaData.item2.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3), + itemBuilder: (context, position) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 1.0, vertical: 1.0), + child: InkWell( + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: Image.memory( + _mediaData.item2[position], + fit: _mediaData.item1[position].height > + _mediaData.item1[position].width + ? BoxFit.fitWidth + : BoxFit.fitHeight, ), - onTap: () { - if (!_attachments.any((element) => - element.id == mediaData.data.item1[position].id)) { - _addAttachment(mediaData.data.item1[position]); - } else { - _attachments.removeWhere((element) => - element.id == mediaData.data.item1[position].id); - setState(() {}); - } - }, ), - ); + if (_attachments.any((element) => + element.id == _mediaData.item1[position].id)) + Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: Icon( + StreamIcons.check_send, + color: Colors.white, + size: 24.0, + ), + ), + ), + ], + ), + onTap: () { + if (!_attachments.any((element) => + element.id == _mediaData.item1[position].id)) { + _addAttachment(_mediaData.item1[position]); + } else { + _attachments.removeWhere((element) => + element.id == _mediaData.item1[position].id); + setState(() {}); + } }, - ); - }); + ), + ); + }, + ); break; case 1: break; @@ -760,7 +752,7 @@ class MessageInputState extends State { }); } - Future, List>> _getAllMedia() async { + void _getAllMedia() async { var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); //var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; @@ -782,7 +774,9 @@ class MessageInputState extends State { resultThumbnailList.add(await PhotoGallery.getThumbnail(mediumId: e.id)); } - return Tuple2, List>(resultList, resultThumbnailList); + setState(() { + _mediaData = Tuple2, List>(resultList, resultThumbnailList); + }); } OverlayEntry _buildMentionsOverlayEntry() { @@ -1051,6 +1045,7 @@ class MessageInputState extends State { _filePickerSize = 250.0; }); } else { + _getAllMedia(); showAttachmentModal(); } }, From 5c2de165810e04466bad9218e7033817e1d70a48 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 9 Nov 2020 11:17:39 +0100 Subject: [PATCH 10/25] move gesture detector --- lib/src/message_input.dart | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 2ba93347..ad70f148 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -657,28 +657,28 @@ class MessageInputState extends State { ), ], ), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), + GestureDetector( + onVerticalDragUpdate: (update) { + setState(() { + _filePickerSize -= update.delta.dy; + if (_filePickerSize < 100) { + _filePickerSize = 100.0; + } else if (_filePickerSize > 500) { + _filePickerSize = 500; + } + }); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onVerticalDragUpdate: (update) { - setState(() { - _filePickerSize -= update.delta.dy; - if (_filePickerSize < 100) { - _filePickerSize = 100.0; - } else if (_filePickerSize > 500) { - _filePickerSize = 500; - } - }); - }, + child: Container( + width: double.infinity, + child: Center( child: Padding( padding: const EdgeInsets.all(8.0), child: Container( @@ -691,7 +691,7 @@ class MessageInputState extends State { ), ), ), - ], + ), ), ), Expanded( From 27c3447c2eaa62f70eb2703e9681b9ece8353364 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 9 Nov 2020 11:20:21 +0100 Subject: [PATCH 11/25] fix remove attachment indicator color --- lib/src/message_input.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index ad70f148..ff243bbe 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1169,11 +1169,12 @@ class MessageInputState extends State { _attachments.remove(attachment); }); }, - fillColor: Colors.white.withOpacity(.5), + fillColor: Colors.black.withOpacity(.5), child: Center( child: Icon( - Icons.close, + StreamIcons.close, size: 15, + color: Colors.white, ), ), ), From 0867d7a4dba8d93c217cc941ce5f8dd866aea463 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 17:19:34 +0530 Subject: [PATCH 12/25] fix: Fixed issues --- lib/src/message_input.dart | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index ff243bbe..38ae6eb6 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -737,10 +737,13 @@ class MessageInputState extends State { Container( color: Colors.black.withOpacity(0.5), child: Center( - child: Icon( - StreamIcons.check_send, - color: Colors.white, - size: 24.0, + child: CircleAvatar( + maxRadius: 12.0, + backgroundColor: Colors.white, + child: Icon( + StreamIcons.check, + color: Colors.black, + ), ), ), ), @@ -826,7 +829,7 @@ class MessageInputState extends State { void _getAllMedia() async { var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); - //var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); + var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; List> resultThumbnailList = []; @@ -835,12 +838,13 @@ class MessageInputState extends State { resultList.addAll(data.items); } - // for (var album in allVideoAlbums) { - // var data = await album.listMedia(); - // resultList.addAll(data.items); - // } + for (var album in allVideoAlbums) { + var data = await album.listMedia(); + resultList.addAll(data.items); + } resultList.sort((a, b) => a.modifiedDate.compareTo(b.modifiedDate)); + resultList = resultList.toSet().toList(); for (var e in resultList) { resultThumbnailList.add(await PhotoGallery.getThumbnail(mediumId: e.id)); From 58e66ee28ff4ae3920cc08215df4087212739dfc Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 9 Nov 2020 13:33:29 +0100 Subject: [PATCH 13/25] fix overlay issues --- lib/src/message_input.dart | 44 ++++++-------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 38ae6eb6..de5f9c6a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -377,7 +377,7 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _emojiOverlay = null; - _checkCommands(s.trimLeft(), context); + _checkCommands(s, context); _checkMentions(s, context); @@ -829,7 +829,8 @@ class MessageInputState extends State { void _getAllMedia() async { var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); - var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); + var allVideoAlbums = + await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; List> resultThumbnailList = []; @@ -1659,42 +1660,9 @@ class MessageInputState extends State { if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { if (visible) { - if (_commandsOverlay != null) { - if (textEditingController.text.trimLeft().startsWith('/')) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - }); - } - } - - if (_mentionsOverlay != null) { - if (textEditingController.text.contains('@')) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _mentionsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - }); - } - } - - if (_emojiOverlay != null) { - if (textEditingController.text.contains(':')) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _emojiOverlay = _buildEmojiOverlay(); - Overlay.of(context).insert(_emojiOverlay); - }); - } - } - } else { - if (_commandsOverlay != null) { - _commandsOverlay.remove(); - } - if (_mentionsOverlay != null) { - _mentionsOverlay.remove(); - } - if (_emojiOverlay != null) { - _emojiOverlay.remove(); - } + _checkCommands(textEditingController.text, context); + _checkMentions(textEditingController.text, context); + _checkEmoji(textEditingController.text, context); } }); } From 9399e28869ccf8b29e76db2bc05c69ac9038c089 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Nov 2020 18:11:45 +0530 Subject: [PATCH 14/25] feat, fix: Added animation, fixed modal issues --- lib/src/message_input.dart | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 38ae6eb6..70444ce9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -663,8 +663,9 @@ class MessageInputState extends State { _filePickerSize -= update.delta.dy; if (_filePickerSize < 100) { _filePickerSize = 100.0; - } else if (_filePickerSize > 500) { - _filePickerSize = 500; + } else if (_filePickerSize > + MediaQuery.of(context).size.height / 1.7) { + _filePickerSize = MediaQuery.of(context).size.height / 1.7; } }); }, @@ -732,21 +733,28 @@ class MessageInputState extends State { : BoxFit.fitHeight, ), ), - if (_attachments.any((element) => - element.id == _mediaData.item1[position].id)) - Container( - color: Colors.black.withOpacity(0.5), - child: Center( - child: CircleAvatar( - maxRadius: 12.0, - backgroundColor: Colors.white, - child: Icon( - StreamIcons.check, - color: Colors.black, + IgnorePointer( + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: _attachments.any((element) => + element.id == _mediaData.item1[position].id) + ? 1.0 + : 0.0, + child: Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: CircleAvatar( + maxRadius: 12.0, + backgroundColor: Colors.white, + child: Icon( + StreamIcons.check, + color: Colors.black, + ), ), ), ), ), + ), ], ), onTap: () { @@ -829,7 +837,8 @@ class MessageInputState extends State { void _getAllMedia() async { var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); - var allVideoAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.video); + var allVideoAlbums = + await PhotoGallery.listAlbums(mediumType: MediumType.video); List resultList = []; List> resultThumbnailList = []; From 815b0120bf2eaf58d3d290e98244c494ce0be539 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 9 Nov 2020 14:45:12 +0100 Subject: [PATCH 15/25] fix iconbuttons splash animation --- lib/src/message_input.dart | 160 +++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 61866612..f2f60652 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -613,98 +613,100 @@ class MessageInputState extends State { Widget _buildFilePickerSection() { return Container( - color: Color(0xFFF2F2F2), height: _filePickerSize, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - icon: Icon( - StreamIcons.picture, - color: _filePickerIndex == 0 - ? StreamChatTheme.of(context).accentColor - : Colors.black.withOpacity(0.5), + child: Material( + color: Color(0xFFF2F2F2), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + icon: Icon( + StreamIcons.picture, + color: _filePickerIndex == 0 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + setState(() { + _filePickerIndex = 0; + }); + }, ), - onPressed: () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - IconButton( - icon: Icon( - StreamIcons.folder, - color: _filePickerIndex == 1 - ? StreamChatTheme.of(context).accentColor - : Colors.black.withOpacity(0.5), + IconButton( + icon: Icon( + StreamIcons.folder, + color: _filePickerIndex == 1 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + pickFile(DefaultAttachmentTypes.file, false); + }, ), - onPressed: () { - pickFile(DefaultAttachmentTypes.file, false); - }, - ), - IconButton( - icon: Icon( - StreamIcons.camera, - color: _filePickerIndex == 2 - ? StreamChatTheme.of(context).accentColor - : Colors.black.withOpacity(0.5), + IconButton( + icon: Icon( + StreamIcons.camera, + color: _filePickerIndex == 2 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + pickFile(DefaultAttachmentTypes.image, true); + }, ), - onPressed: () { - pickFile(DefaultAttachmentTypes.image, true); - }, - ), - ], - ), - GestureDetector( - onVerticalDragUpdate: (update) { - setState(() { - _filePickerSize -= update.delta.dy; - if (_filePickerSize < 100) { - _filePickerSize = 100.0; - } else if (_filePickerSize > - MediaQuery.of(context).size.height / 1.7) { - _filePickerSize = MediaQuery.of(context).size.height / 1.7; - } - }); - }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - ), - ), + ], + ), + GestureDetector( + onVerticalDragUpdate: (update) { + setState(() { + _filePickerSize -= update.delta.dy; + if (_filePickerSize < 100) { + _filePickerSize = 100.0; + } else if (_filePickerSize > + MediaQuery.of(context).size.height / 1.7) { + _filePickerSize = MediaQuery.of(context).size.height / 1.7; + } + }); + }, child: Container( - width: double.infinity, - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - width: 40.0, - height: 4.0, - decoration: BoxDecoration( - color: Color(0xFFF2F2F2), - borderRadius: BorderRadius.circular(4.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + ), + child: Container( + width: double.infinity, + child: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 40.0, + height: 4.0, + decoration: BoxDecoration( + color: Color(0xFFF2F2F2), + borderRadius: BorderRadius.circular(4.0), + ), ), ), ), ), ), ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + ), + child: _buildPickerSection(), ), - child: _buildPickerSection(), ), - ), - ], + ], + ), ), ); } From ae0316fe4f969e9ae4da360db4985e63067760bd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 9 Nov 2020 15:15:15 +0100 Subject: [PATCH 16/25] add animation --- lib/src/message_input.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index f2f60652..9fd4e746 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -207,7 +207,7 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: _buildDmCheckbox(), ), - if (_openFilePickerSection) _buildFilePickerSection(), + _buildFilePickerSection(), ], ), ), @@ -612,8 +612,9 @@ class MessageInputState extends State { } Widget _buildFilePickerSection() { - return Container( - height: _filePickerSize, + return AnimatedContainer( + duration: Duration(milliseconds: 300), + height: _openFilePickerSection ? _filePickerSize : 0, child: Material( color: Color(0xFFF2F2F2), child: Column( From 42e746fed7c9049780bf8935b6dc9e4001b0839f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 11 Nov 2020 16:35:58 +0100 Subject: [PATCH 17/25] use media_gallery plugin --- .../android/app/src/main/AndroidManifest.xml | 6 +- example/ios/Podfile.lock | 10 +- example/ios/Runner.xcodeproj/project.pbxproj | 2 + example/pubspec.yaml | 2 +- lib/src/message_input.dart | 117 ++++++++++-------- pubspec.yaml | 4 +- 6 files changed, 83 insertions(+), 58 deletions(-) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a90d828e..6fb8664b 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -6,10 +6,14 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> + + + android:icon="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true" + > 4.7.0) - - firebase_core (0.5.1): + - firebase_core (0.5.2): - Firebase/CoreOnly (~> 6.33.0) - Flutter - firebase_messaging (7.0.3): @@ -113,6 +113,8 @@ PODS: - nanopb/encode (1.30906.0) - path_provider (0.0.1): - Flutter + - photo_gallery (0.0.1): + - Flutter - PromisesObjC (1.2.11) - Protobuf (3.13.0) - SDWebImage (5.9.4): @@ -163,6 +165,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) + - photo_gallery (from `.symlinks/plugins/photo_gallery/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) @@ -214,6 +217,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" + photo_gallery: + :path: ".symlinks/plugins/photo_gallery/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" sqflite: @@ -232,7 +237,7 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 - firebase_core: aa25a5dc6b492ecab37587c53d8420135f0cac90 + firebase_core: 350ba329d1641211bc6183a3236893cafdacfea7 firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 @@ -250,6 +255,7 @@ SPEC CHECKSUMS: image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + photo_gallery: 9f95e57747cd22c10676ece3660d1ffe6c603ee5 PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 6d377561..de8fcb9a 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -328,6 +328,7 @@ "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/photo_gallery/photo_gallery.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", @@ -357,6 +358,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_gallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0cba8d30..79b9638e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,7 +1,7 @@ name: example description: A new Flutter project. -version: 1.0.43+45 +version: 1.0.44+46 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 9fd4e746..c26cd152 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -8,15 +8,17 @@ 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:http_parser/http_parser.dart'; +import 'package:http_parser/http_parser.dart' as httpParser; import 'package:image_picker/image_picker.dart'; +import 'package:media_gallery/media_gallery.dart'; import 'package:mime/mime.dart'; -import 'package:photo_gallery/photo_gallery.dart'; +import 'package:permission_handler/permission_handler.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'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; +import 'package:transparent_image/transparent_image.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -181,7 +183,7 @@ class MessageInputState extends State { int _filePickerIndex = 0; double _filePickerSize = 250.0; - Tuple2, List> _mediaData = Tuple2([], []); + Iterable _media = []; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -716,7 +718,7 @@ class MessageInputState extends State { switch (_filePickerIndex) { case 0: return GridView.builder( - itemCount: _mediaData.item2.length, + itemCount: _media.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemBuilder: (context, position) { @@ -728,24 +730,29 @@ class MessageInputState extends State { children: [ AspectRatio( aspectRatio: 1.0, - child: Image.memory( - _mediaData.item2[position], - fit: _mediaData.item1[position].height > - _mediaData.item1[position].width - ? BoxFit.fitWidth - : BoxFit.fitHeight, + child: FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: MediaThumbnailProvider( + media: _media.elementAt(position), + ), + fit: BoxFit.fill, ), ), - IgnorePointer( - child: AnimatedOpacity( - duration: Duration(milliseconds: 300), - opacity: _attachments.any((element) => - element.id == _mediaData.item1[position].id) - ? 1.0 - : 0.0, - child: Container( - color: Colors.black.withOpacity(0.5), - child: Center( + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: _attachments.any((element) => + element.id == _media.elementAt(position).id) + ? 1.0 + : 0.0, + child: Container( + color: Colors.black.withOpacity(0.5), + alignment: Alignment.topRight, + padding: const EdgeInsets.only( + top: 8, + right: 8, + ), child: CircleAvatar( maxRadius: 12.0, backgroundColor: Colors.white, @@ -762,11 +769,11 @@ class MessageInputState extends State { ), onTap: () { if (!_attachments.any((element) => - element.id == _mediaData.item1[position].id)) { - _addAttachment(_mediaData.item1[position]); + element.id == _media.elementAt(position).id)) { + _addAttachment(_media.elementAt(position)); } else { _attachments.removeWhere((element) => - element.id == _mediaData.item1[position].id); + element.id == _media.elementAt(position).id); setState(() {}); } }, @@ -782,10 +789,10 @@ class MessageInputState extends State { } } - void _addAttachment(Medium medium) async { - var mediaFile = await PhotoGallery.getFile(mediumId: medium.id); + void _addAttachment(Media medium) async { + final mediaFile = await medium.getFile(); - var file = PlatformFile( + final file = PlatformFile( path: mediaFile.path, bytes: mediaFile.readAsBytesSync(), ); @@ -803,7 +810,7 @@ class MessageInputState extends State { file: file, attachment: Attachment( localUri: file.path != null ? Uri.parse(file.path) : null, - type: medium.mediumType == MediumType.image ? 'image' : 'video', + type: medium.mediaType == MediaType.image ? 'image' : 'video', ), id: medium.id, ); @@ -814,12 +821,12 @@ class MessageInputState extends State { final url = await _uploadAttachment( file, - medium.mediumType == MediumType.image + medium.mediaType == MediaType.image ? DefaultAttachmentTypes.image : DefaultAttachmentTypes.video, channel); - var fileType = medium.mediumType == MediumType.image + final fileType = medium.mediaType == MediaType.image ? DefaultAttachmentTypes.image : DefaultAttachmentTypes.video; @@ -839,32 +846,28 @@ class MessageInputState extends State { } void _getAllMedia() async { - var allAlbums = await PhotoGallery.listAlbums(mediumType: MediumType.image); - var allVideoAlbums = - await PhotoGallery.listAlbums(mediumType: MediumType.video); - List resultList = []; - List> resultThumbnailList = []; + final List collections = + await MediaGallery.listMediaCollections(mediaTypes: [ + MediaType.image, + MediaType.video, + ]); - for (var album in allAlbums) { - var data = await album.listMedia(); - resultList.addAll(data.items); + if (collections.isEmpty) { + return; } - for (var album in allVideoAlbums) { - var data = await album.listMedia(); - resultList.addAll(data.items); - } - - resultList.sort((a, b) => a.modifiedDate.compareTo(b.modifiedDate)); - resultList = resultList.toSet().toList(); - - for (var e in resultList) { - resultThumbnailList.add(await PhotoGallery.getThumbnail(mediumId: e.id)); - } + final page = await collections + .firstWhere( + (element) => element.name == 'All', + orElse: () => collections.first, + ) + .getMedias( + take: 50, + ); + print('page.items.length: ${page.items.length}'); setState(() { - _mediaData = - Tuple2, List>(resultList, resultThumbnailList); + _media = page.items; }); } @@ -1271,13 +1274,21 @@ class MessageInputState extends State { color: Color(0xFF000000).withAlpha(128), ), ), - onTap: () { + onTap: () async { if (_openFilePickerSection) { setState(() { _openFilePickerSection = false; _filePickerSize = 250.0; }); } else { + final status = await Permission.storage.request(); + print('status: ${status}'); + if (status.isDenied || status.isPermanentlyDenied) { + final res = await openAppSettings(); + if (!res) { + return; + } + } _getAllMedia(); showAttachmentModal(); } @@ -1494,7 +1505,7 @@ class MessageInputState extends State { MultipartFile.fromBytes( bytes, filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), + contentType: httpParser.MediaType.parse(lookupMimeType(filename)), ), ); return res.file; @@ -1507,7 +1518,7 @@ class MessageInputState extends State { MultipartFile.fromBytes( bytes, filename: filename, - contentType: MediaType.parse(lookupMimeType(filename)), + contentType: httpParser.MediaType.parse(lookupMimeType(filename)), ), ); return res.file; diff --git a/pubspec.yaml b/pubspec.yaml index 6b2aeff1..ce0b3633 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,9 @@ dependencies: flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 - photo_gallery: ^0.3.0 + media_gallery: ^0.1.5 + permission_handler: ^5.0.1+1 + transparent_image: ^1.0.0 flutter: assets: From f756e6e5670c2b37b0d31d527fe2f15d5e9025ae Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 11 Nov 2020 17:29:45 +0100 Subject: [PATCH 18/25] extract media list view --- lib/src/media_list_view.dart | 161 +++++++++++++++++++++++++++++++++++ lib/src/message_input.dart | 125 ++++++--------------------- svgs/video_call_icon.svg | 3 + 3 files changed, 189 insertions(+), 100 deletions(-) create mode 100644 lib/src/media_list_view.dart create mode 100644 svgs/video_call_icon.svg diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart new file mode 100644 index 00000000..5a2fc25f --- /dev/null +++ b/lib/src/media_list_view.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:media_gallery/media_gallery.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; +import 'package:transparent_image/transparent_image.dart'; + +class MediaListView extends StatefulWidget { + final List selectedIds; + final void Function(Media media) onSelect; + + const MediaListView({ + Key key, + this.selectedIds = const [], + this.onSelect, + }) : super(key: key); + @override + _MediaListViewState createState() => _MediaListViewState(); +} + +class _MediaListViewState extends State { + final _media = []; + final ScrollController _scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return GridView.builder( + itemCount: _media.length, + controller: _scrollController, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemBuilder: ( + context, + position, + ) { + final media = _media.elementAt(position); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0), + child: InkWell( + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: MediaThumbnailProvider( + media: media, + ), + fit: BoxFit.cover, + ), + ), + Positioned.fill( + child: IgnorePointer( + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: widget.selectedIds.any((id) => id == media.id) + ? 1.0 + : 0.0, + child: Container( + color: Colors.black.withOpacity(0.5), + alignment: Alignment.topRight, + padding: const EdgeInsets.only( + top: 8, + right: 8, + ), + child: CircleAvatar( + maxRadius: 12.0, + backgroundColor: Colors.white, + child: Icon( + StreamIcons.check, + color: Colors.black, + ), + ), + ), + ), + ), + ), + if (media.mediaType == MediaType.video) + Positioned( + left: 8, + bottom: 10, + child: SvgPicture.asset( + 'svgs/video_call_icon.svg', + package: 'stream_chat_flutter', + ), + ), + ], + ), + onTap: () { + if (widget.onSelect != null) { + widget.onSelect(media); + } + }, + ), + ); + }, + ); + } + + @override + void initState() { + super.initState(); + + _scrollController.addListener(() { + if (_scrollController.offset > + _scrollController.position.maxScrollExtent - 100) { + _getMedia(); + } + }); + + _getMedia(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _getMedia() async { + final List collections = + await MediaGallery.listMediaCollections( + mediaTypes: [ + MediaType.video, + MediaType.image, + ], + ); + + if (collections.isEmpty) { + return; + } + final collection = collections.firstWhere( + (element) => element.isAllCollection, + orElse: () => collections.first, + ); + + final videoPage = await collection.getMedias( + mediaType: MediaType.video, + take: 500, + ); + final imagePage = await collection.getMedias( + mediaType: MediaType.image, + take: 500, + ); + + final allItems = [ + ...videoPage.items, + ...imagePage.items, + ]..sort(( + a, + b, + ) => + b.creationDate.compareTo(a.creationDate)); + + setState(() { + _media.addAll(allItems); + }); + + print( + '_media.where((element) => element.mediaType == MediaType.video).length: ${_media.where((element) => element.mediaType == MediaType.video).length}'); + } +} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index c26cd152..7e8a40c4 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -14,11 +14,11 @@ import 'package:media_gallery/media_gallery.dart'; import 'package:mime/mime.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; -import 'package:transparent_image/transparent_image.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -183,8 +183,6 @@ class MessageInputState extends State { int _filePickerIndex = 0; double _filePickerSize = 250.0; - Iterable _media = []; - /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -699,15 +697,16 @@ class MessageInputState extends State { ), ), ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), + if (_openFilePickerSection) + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + ), + child: _buildPickerSection(), ), - child: _buildPickerSection(), ), - ), ], ), ), @@ -717,68 +716,15 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - return GridView.builder( - itemCount: _media.length, - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), - itemBuilder: (context, position) { - return Padding( - padding: - const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0), - child: InkWell( - child: Stack( - children: [ - AspectRatio( - aspectRatio: 1.0, - child: FadeInImage( - placeholder: MemoryImage(kTransparentImage), - image: MediaThumbnailProvider( - media: _media.elementAt(position), - ), - fit: BoxFit.fill, - ), - ), - Positioned.fill( - child: IgnorePointer( - child: AnimatedOpacity( - duration: Duration(milliseconds: 300), - opacity: _attachments.any((element) => - element.id == _media.elementAt(position).id) - ? 1.0 - : 0.0, - child: Container( - color: Colors.black.withOpacity(0.5), - alignment: Alignment.topRight, - padding: const EdgeInsets.only( - top: 8, - right: 8, - ), - child: CircleAvatar( - maxRadius: 12.0, - backgroundColor: Colors.white, - child: Icon( - StreamIcons.check, - color: Colors.black, - ), - ), - ), - ), - ), - ), - ], - ), - onTap: () { - if (!_attachments.any((element) => - element.id == _media.elementAt(position).id)) { - _addAttachment(_media.elementAt(position)); - } else { - _attachments.removeWhere((element) => - element.id == _media.elementAt(position).id); - setState(() {}); - } - }, - ), - ); + return MediaListView( + selectedIds: _attachments.map((e) => e.id).toList(), + onSelect: (media) { + if (!_attachments.any((element) => element.id == media.id)) { + _addAttachment(media); + } else { + _attachments.removeWhere((element) => element.id == media.id); + setState(() {}); + } }, ); break; @@ -845,32 +791,6 @@ class MessageInputState extends State { }); } - void _getAllMedia() async { - final List collections = - await MediaGallery.listMediaCollections(mediaTypes: [ - MediaType.image, - MediaType.video, - ]); - - if (collections.isEmpty) { - return; - } - - final page = await collections - .firstWhere( - (element) => element.name == 'All', - orElse: () => collections.first, - ) - .getMedias( - take: 50, - ); - - print('page.items.length: ${page.items.length}'); - setState(() { - _media = page.items; - }); - } - CircleAvatar _buildGiphyIcon() { if (kIsWeb) { return CircleAvatar( @@ -1275,6 +1195,13 @@ class MessageInputState extends State { ), ), onTap: () async { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + if (_openFilePickerSection) { setState(() { _openFilePickerSection = false; @@ -1282,14 +1209,12 @@ class MessageInputState extends State { }); } else { final status = await Permission.storage.request(); - print('status: ${status}'); if (status.isDenied || status.isPermanentlyDenied) { final res = await openAppSettings(); if (!res) { return; } } - _getAllMedia(); showAttachmentModal(); } }, diff --git a/svgs/video_call_icon.svg b/svgs/video_call_icon.svg new file mode 100644 index 00000000..1c3832d6 --- /dev/null +++ b/svgs/video_call_icon.svg @@ -0,0 +1,3 @@ + + + From 43d7522b19059ec3f9cd124d85e38cc2a60f9ff0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 11 Nov 2020 17:43:29 +0100 Subject: [PATCH 19/25] highligth attach icon --- lib/src/message_input.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 7e8a40c4..8014ed89 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1191,7 +1191,9 @@ class MessageInputState extends State { EdgeInsets.only(left: 8.0, right: padding, top: 8.0, bottom: 8.0), child: Icon( StreamIcons.attach, - color: Color(0xFF000000).withAlpha(128), + color: _openFilePickerSection + ? StreamChatTheme.of(context).accentColor + : Color(0xFF000000).withAlpha(128), ), ), onTap: () async { From b14819efc5029fbd0282788c119e3305e367a878 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 12 Nov 2020 12:53:37 +0100 Subject: [PATCH 20/25] fix dragging --- lib/src/media_list_view.dart | 17 ----------------- lib/src/message_input.dart | 24 ++++++++---------------- 2 files changed, 8 insertions(+), 33 deletions(-) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 5a2fc25f..627a480c 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -99,23 +99,9 @@ class _MediaListViewState extends State { @override void initState() { super.initState(); - - _scrollController.addListener(() { - if (_scrollController.offset > - _scrollController.position.maxScrollExtent - 100) { - _getMedia(); - } - }); - _getMedia(); } - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - void _getMedia() async { final List collections = await MediaGallery.listMediaCollections( @@ -154,8 +140,5 @@ class _MediaListViewState extends State { setState(() { _media.addAll(allItems); }); - - print( - '_media.where((element) => element.mediaType == MediaType.video).length: ${_media.where((element) => element.mediaType == MediaType.video).length}'); } } diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 8014ed89..e4693244 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -171,7 +171,7 @@ class MessageInputState extends State { final _imagePicker = ImagePicker(); bool _inputEnabled = true; bool _messageIsPresent = false; - bool _typingStarted = false; + bool _animateContainer = true; bool _commandEnabled = false; OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay; Iterable _emojiNames; @@ -383,11 +383,6 @@ class MessageInputState extends State { _checkEmoji(s, context); }, - onTap: () { - setState(() { - _typingStarted = true; - }); - }, style: Theme.of(context).textTheme.bodyText2, autofocus: false, textAlignVertical: TextAlignVertical.center, @@ -613,7 +608,7 @@ class MessageInputState extends State { Widget _buildFilePickerSection() { return AnimatedContainer( - duration: Duration(milliseconds: 300), + duration: _animateContainer ? Duration(milliseconds: 300) : Duration.zero, height: _openFilePickerSection ? _filePickerSize : 0, child: Material( color: Color(0xFFF2F2F2), @@ -662,13 +657,11 @@ class MessageInputState extends State { GestureDetector( onVerticalDragUpdate: (update) { setState(() { - _filePickerSize -= update.delta.dy; - if (_filePickerSize < 100) { - _filePickerSize = 100.0; - } else if (_filePickerSize > - MediaQuery.of(context).size.height / 1.7) { - _filePickerSize = MediaQuery.of(context).size.height / 1.7; - } + _animateContainer = false; + _filePickerSize = (_filePickerSize - update.delta.dy).clamp( + 100, + MediaQuery.of(context).size.height / 1.7, + ); }); }, child: Container( @@ -1206,6 +1199,7 @@ class MessageInputState extends State { if (_openFilePickerSection) { setState(() { + _animateContainer = true; _openFilePickerSection = false; _filePickerSize = 250.0; }); @@ -1536,7 +1530,6 @@ class MessageInputState extends State { setState(() { _messageIsPresent = false; - _typingStarted = false; _commandEnabled = false; }); @@ -1632,7 +1625,6 @@ class MessageInputState extends State { void _parseExistingMessage(Message message) { textEditingController.text = message.text; - _typingStarted = true; _messageIsPresent = true; message.attachments?.forEach((attachment) { From a649e2d5e7b083039735df6b707ab44a13896eb3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 12:26:47 +0100 Subject: [PATCH 21/25] fix thumbnails --- lib/src/message_input.dart | 49 ++++++++++++++++++++++++++++++++------ lib/src/user_avatar.dart | 34 +++++++++++++------------- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e4693244..72c754e0 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -730,12 +730,18 @@ class MessageInputState extends State { void _addAttachment(Media medium) async { final mediaFile = await medium.getFile(); + final thumbBytes = await medium.getThumbnail(); final file = PlatformFile( path: mediaFile.path, bytes: mediaFile.readAsBytesSync(), ); + final thumbFile = PlatformFile( + bytes: thumbBytes, + name: '${file.name ?? file.path?.split('/')?.last}_thumbnail.jpeg', + ); + setState(() { _inputEnabled = true; }); @@ -747,6 +753,7 @@ class MessageInputState extends State { final channel = StreamChannel.of(context).channel; final attachment = _SendingAttachment( file: file, + thumbFile: thumbFile, attachment: Attachment( localUri: file.path != null ? Uri.parse(file.path) : null, type: medium.mediaType == MediaType.image ? 'image' : 'video', @@ -758,6 +765,11 @@ class MessageInputState extends State { _attachments.add(attachment); }); + final thumbUrl = await _uploadImage( + thumbFile, + channel, + ); + final url = await _uploadAttachment( file, medium.mediaType == MediaType.image @@ -772,10 +784,12 @@ class MessageInputState extends State { if (fileType == DefaultAttachmentTypes.image) { attachment.attachment = attachment.attachment.copyWith( imageUrl: url, + thumbUrl: thumbUrl, ); } else { attachment.attachment = attachment.attachment.copyWith( assetUrl: url, + thumbUrl: thumbUrl, ); } @@ -1041,7 +1055,7 @@ class MessageInputState extends State { return _attachments.isEmpty ? Container() : LimitedBox( - maxHeight: 76.0, + maxHeight: 104.0, child: ListView( scrollDirection: Axis.horizontal, children: _attachments @@ -1055,8 +1069,8 @@ class MessageInputState extends State { AspectRatio( aspectRatio: 1.0, child: Container( - height: 50, - width: 50, + height: 104, + width: 104, child: _buildAttachment(attachment), ), ), @@ -1138,9 +1152,26 @@ class MessageInputState extends State { ); break; case 'video': - return Container( - child: Icon(Icons.videocam), - color: Colors.black26, + return Stack( + children: [ + Container( + child: attachment.thumbFile != null + ? Image.memory( + attachment.thumbFile.bytes, + fit: BoxFit.cover, + ) + : Icon(Icons.videocam), + color: Colors.black26, + ), + Positioned( + left: 8, + bottom: 10, + child: SvgPicture.asset( + 'svgs/video_call_icon.svg', + package: 'stream_chat_flutter', + ), + ), + ], ); break; default: @@ -1426,7 +1457,9 @@ class MessageInputState extends State { MultipartFile.fromBytes( bytes, filename: filename, - contentType: httpParser.MediaType.parse(lookupMimeType(filename)), + contentType: filename != null + ? httpParser.MediaType.parse(lookupMimeType(filename)) + : null, ), ); return res.file; @@ -1657,12 +1690,14 @@ class MessageInputState extends State { class _SendingAttachment { PlatformFile file; + PlatformFile thumbFile; Attachment attachment; bool uploaded; String id; _SendingAttachment({ this.file, + this.thumbFile, this.attachment, this.uploaded = false, this.id, diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 86bf7abd..d1dd2e41 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -38,25 +38,23 @@ class UserAvatar extends StatelessWidget { : null, child: Stack( children: [ - ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, - child: Container( - constraints: constraints ?? - streamChatTheme.ownMessageTheme.avatarTheme.constraints, - decoration: BoxDecoration( - color: streamChatTheme.accentColor, - ), - child: hasImage - ? CachedNetworkImage( - imageUrl: user.extraData['image'], - errorWidget: (_, __, ___) { - return streamChatTheme.defaultUserImage(context, user); - }, - fit: BoxFit.cover, - ) - : streamChatTheme.defaultUserImage(context, user), + Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme.constraints, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, + color: streamChatTheme.accentColor, ), + child: hasImage + ? CachedNetworkImage( + imageUrl: user.extraData['image'], + errorWidget: (_, __, ___) { + return streamChatTheme.defaultUserImage(context, user); + }, + ) + : streamChatTheme.defaultUserImage(context, user), ), if (showOnlineStatus && user.online == true) Positioned( From 8dd718b148f4981cb259a5fbc20d140f349d2bff Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 16:26:24 +0100 Subject: [PATCH 22/25] ask permissions --- example/ios/Podfile.lock | 16 +- example/ios/Runner.xcodeproj/project.pbxproj | 4 +- example/lib/choose_user_page.dart | 268 ++++++++++--------- example/lib/main.dart | 130 ++++----- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 102 +++++-- svgs/icon_picture_empty_state.svg | 4 + 7 files changed, 303 insertions(+), 223 deletions(-) create mode 100644 svgs/icon_picture_empty_state.svg diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index af3a3d40..131e8aec 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -108,6 +108,8 @@ PODS: - GoogleUtilities/Logger - image_picker (0.0.1): - Flutter + - media_gallery (0.0.1): + - Flutter - nanopb (1.30906.0): - nanopb/decode (= 1.30906.0) - nanopb/encode (= 1.30906.0) @@ -115,7 +117,7 @@ PODS: - nanopb/encode (1.30906.0) - path_provider (0.0.1): - Flutter - - photo_gallery (0.0.1): + - "permission_handler (5.0.1+1)": - Flutter - PromisesObjC (1.2.11) - Protobuf (3.13.0) @@ -167,8 +169,9 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker (from `.symlinks/plugins/image_picker/ios`) + - media_gallery (from `.symlinks/plugins/media_gallery/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - - photo_gallery (from `.symlinks/plugins/photo_gallery/ios`) + - permission_handler (from `.symlinks/plugins/permission_handler/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) @@ -220,10 +223,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" image_picker: :path: ".symlinks/plugins/image_picker/ios" + media_gallery: + :path: ".symlinks/plugins/media_gallery/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" - photo_gallery: - :path: ".symlinks/plugins/photo_gallery/ios" + permission_handler: + :path: ".symlinks/plugins/permission_handler/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" sqflite: @@ -259,9 +264,10 @@ SPEC CHECKSUMS: GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09 + media_gallery: 834a04455a476b975c280aa1f24ed2853dfe36de nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - photo_gallery: 9f95e57747cd22c10676ece3660d1ffe6c603ee5 + permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748 SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ea52b6eb..709db931 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -327,9 +327,9 @@ "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework", + "${BUILT_PRODUCTS_DIR}/media_gallery/media_gallery.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", - "${BUILT_PRODUCTS_DIR}/photo_gallery/photo_gallery.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework", @@ -358,9 +358,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_gallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_gallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework", diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index fd7d2b6c..30205088 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -59,165 +59,167 @@ class ChooseUserPage extends StatelessWidget { (_, value) => value..extraData['image'] = getRandomPicUrl(value)); return Scaffold( - body: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 34, - bottom: 20, - ), - child: Center( - child: SvgPicture.asset( - 'assets/logo.svg', - height: 40, + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 34, + bottom: 20, + ), + child: Center( + child: SvgPicture.asset( + 'assets/logo.svg', + height: 40, + ), ), ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 13.0), - child: Text( - 'Welcome to Stream Chat', + Padding( + padding: const EdgeInsets.only(bottom: 13.0), + child: Text( + 'Welcome to Stream Chat', + style: TextStyle( + fontSize: 22, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + Text( + 'Select a user to try the Flutter SDK:', style: TextStyle( - fontSize: 22, + fontSize: 14.5, color: Colors.black, - fontWeight: FontWeight.bold, ), ), - ), - Text( - 'Select a user to try the Flutter SDK:', - style: TextStyle( - fontSize: 14.5, - color: Colors.black, - ), - ), - Expanded( - child: ListView.separated( - separatorBuilder: (context, i) { - return Container( - width: double.infinity, - color: Colors.black12, - height: 1, - ); - }, - itemCount: users.length + 1, - itemBuilder: (context, i) { - return [ - ...users.entries.map((entry) { - final token = entry.key; - final user = entry.value; - return ListTile( - onTap: () async { - showDialog( - barrierDismissible: false, - context: context, - builder: (context) => Center( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Colors.white, - ), - height: 100, - width: 100, - child: Center( - child: CircularProgressIndicator(), + Expanded( + child: ListView.separated( + separatorBuilder: (context, i) { + return Container( + width: double.infinity, + color: Colors.black12, + height: 1, + ); + }, + itemCount: users.length + 1, + itemBuilder: (context, i) { + return [ + ...users.entries.map((entry) { + final token = entry.key; + final user = entry.value; + return ListTile( + onTap: () async { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) => Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.white, + ), + height: 100, + width: 100, + child: Center( + child: CircularProgressIndicator(), + ), ), ), + ); + + final secureStorage = FlutterSecureStorage(); + final client = StreamChat.of(context).client; + + await client.setUser( + user, + token, + ); + + secureStorage.write( + key: kStreamApiKey, + value: kDefaultStreamApiKey, + ); + secureStorage.write( + key: kStreamUserId, + value: user.id, + ); + secureStorage.write( + key: kStreamToken, + value: token, + ); + + if (!kIsWeb) { + initNotifications(client); + } + + Navigator.pop(context); + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChat( + client: client, + child: ChannelListPage(), + ); + }, + ), + ); + }, + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tight( + Size.fromRadius(20), ), - ); - - final secureStorage = FlutterSecureStorage(); - final client = StreamChat.of(context).client; - - await client.setUser( - user, - token, - ); - - secureStorage.write( - key: kStreamApiKey, - value: kDefaultStreamApiKey, - ); - secureStorage.write( - key: kStreamUserId, - value: user.id, - ); - secureStorage.write( - key: kStreamToken, - value: token, - ); - - if (!kIsWeb) { - initNotifications(client); - } - - Navigator.pop(context); - await Navigator.pushReplacement( + ), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text('Stream test account'), + trailing: SvgPicture.asset( + 'assets/icon_arrow_right.svg', + height: 24, + width: 24, + ), + ); + }), + ListTile( + onTap: () { + Navigator.push( context, MaterialPageRoute( - builder: (context) { - return StreamChat( - client: client, - child: ChannelListPage(), - ); - }, + builder: (context) => AdvancedOptionsPage(), ), ); }, - leading: UserAvatar( - user: user, - constraints: BoxConstraints.tight( - Size.fromRadius(20), + leading: CircleAvatar( + child: Icon( + StreamIcons.settings, + color: Colors.black, ), + backgroundColor: + StreamChatTheme.of(context).secondaryColor, ), title: Text( - user.name, + 'Advanced Options', style: TextStyle(fontWeight: FontWeight.bold), ), - subtitle: Text('Stream test account'), + subtitle: Text('Custom settings'), trailing: SvgPicture.asset( 'assets/icon_arrow_right.svg', height: 24, width: 24, + clipBehavior: Clip.none, ), - ); - }), - ListTile( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AdvancedOptionsPage(), - ), - ); - }, - leading: CircleAvatar( - child: Icon( - StreamIcons.settings, - color: Colors.black, - ), - backgroundColor: - StreamChatTheme.of(context).secondaryColor, ), - title: Text( - 'Advanced Options', - style: TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text('Custom settings'), - trailing: SvgPicture.asset( - 'assets/icon_arrow_right.svg', - height: 24, - width: 24, - clipBehavior: Clip.none, - ), - ), - ][i]; - }, + ][i]; + }, + ), ), - ), - StreamVersion(), - ], + StreamVersion(), + ], + ), ), ); } diff --git a/example/lib/main.dart b/example/lib/main.dart index 1da02072..2e3f43e2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -64,83 +64,85 @@ class ChannelListPage extends StatelessWidget { final user = StreamChat.of(context).user; return Scaffold( drawer: Drawer( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).viewPadding.top + 8, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 20.0, - left: 8, - ), - child: Row( - children: [ - UserAvatar( - user: user, - showOnlineStatus: false, - constraints: BoxConstraints.tight(Size.fromRadius(20)), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - user.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + 8, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 20.0, + left: 8, + ), + child: Row( + children: [ + UserAvatar( + user: user, + showOnlineStatus: false, + constraints: BoxConstraints.tight(Size.fromRadius(20)), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + user.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), + ], + ), + ), + ListTile( + leading: Icon(StreamIcons.edit), + title: Text( + 'New direct message', + style: TextStyle( + fontSize: 14.5, ), - ], - ), - ), - ListTile( - leading: Icon(StreamIcons.edit), - title: Text( - 'New direct message', - style: TextStyle( - fontSize: 14.5, ), ), - ), - ListTile( - leading: Icon(StreamIcons.group), - title: Text( - 'New group', - style: TextStyle( - fontSize: 14.5, + ListTile( + leading: Icon(StreamIcons.group), + title: Text( + 'New group', + style: TextStyle( + fontSize: 14.5, + ), ), ), - ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - child: ListTile( - onTap: () async { - await StreamChat.of(context).client.disconnect(); + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: ListTile( + onTap: () async { + await StreamChat.of(context).client.disconnect(); - final secureStorage = FlutterSecureStorage(); - await secureStorage.deleteAll(); - Navigator.pop(context); - await Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ChooseUserPage(), + final secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); + Navigator.pop(context); + await Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => ChooseUserPage(), + ), + ); + }, + leading: Icon(StreamIcons.user), + title: Text( + 'Sign out', + style: TextStyle( + fontSize: 14.5, ), - ); - }, - leading: Icon(StreamIcons.user), - title: Text( - 'Sign out', - style: TextStyle( - fontSize: 14.5, ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e1442635..8fc2dadb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.58+60 +version: 1.0.59+61 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 72c754e0..1f157ed3 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'package:emojis/emoji.dart'; @@ -659,7 +660,7 @@ class MessageInputState extends State { setState(() { _animateContainer = false; _filePickerSize = (_filePickerSize - update.delta.dy).clamp( - 100, + 240.0, MediaQuery.of(context).size.height / 1.7, ); }); @@ -709,17 +710,80 @@ class MessageInputState extends State { Widget _buildPickerSection() { switch (_filePickerIndex) { case 0: - return MediaListView( - selectedIds: _attachments.map((e) => e.id).toList(), - onSelect: (media) { - if (!_attachments.any((element) => element.id == media.id)) { - _addAttachment(media); - } else { - _attachments.removeWhere((element) => element.id == media.id); - setState(() {}); - } - }, - ); + return FutureBuilder( + future: Platform.isAndroid + ? Permission.storage.status + : Permission.photos.status, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (snapshot.data.isGranted) { + return MediaListView( + selectedIds: _attachments.map((e) => e.id).toList(), + onSelect: (media) { + if (!_attachments + .any((element) => element.id == media.id)) { + _addAttachment(media); + } else { + _attachments + .removeWhere((element) => element.id == media.id); + setState(() {}); + } + }, + ); + } + + return InkWell( + onTap: () async { + var status = await (Platform.isAndroid + ? Permission.storage.status + : Permission.photos.status); + print('status: ${status}'); + if (status.isPermanentlyDenied || status.isDenied) { + if (await openAppSettings()) { + setState(() {}); + } + } else { + status = await (Platform.isAndroid + ? Permission.storage + : Permission.photos) + .request(); + if (status.isGranted) { + setState(() {}); + } + } + }, + child: Container( + color: Color(0xFFF2F2F2), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset( + 'svgs/icon_picture_empty_state.svg', + package: 'stream_chat_flutter', + height: 140, + color: StreamChatTheme.of(context).accentColor, + ), + Center( + child: Text( + 'Allow access to your gallery', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: StreamChatTheme.of(context).accentColor, + ), + ), + ), + ], + ), + ), + ); + }); break; case 1: break; @@ -1235,12 +1299,14 @@ class MessageInputState extends State { _filePickerSize = 250.0; }); } else { - final status = await Permission.storage.request(); - if (status.isDenied || status.isPermanentlyDenied) { - final res = await openAppSettings(); - if (!res) { - return; - } + final status = await (Platform.isAndroid + ? Permission.storage.status + : Permission.photos.status); + if (status.isUndetermined) { + await (Platform.isAndroid + ? Permission.storage + : Permission.photos) + .request(); } showAttachmentModal(); } diff --git a/svgs/icon_picture_empty_state.svg b/svgs/icon_picture_empty_state.svg new file mode 100644 index 00000000..135772d0 --- /dev/null +++ b/svgs/icon_picture_empty_state.svg @@ -0,0 +1,4 @@ + + + + From 49dc157ea751150fc36518f6a537ca7f26c948f5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 17:12:36 +0100 Subject: [PATCH 23/25] fix attachment shape --- lib/src/message_input.dart | 57 +++++++++++++++++++++---------------- lib/src/message_widget.dart | 25 ++++++++++------ 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 1f157ed3..bac36ef7 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -218,7 +218,7 @@ class MessageInputState extends State { Flex _buildTextField(BuildContext context) { return Flex( direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ if (!_commandEnabled) _buildExpandActionsButton(), if (widget.actionsLocation == ActionsLocation.left) @@ -388,10 +388,7 @@ class MessageInputState extends State { autofocus: false, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( - hintText: - (_commandEnabled && _chosenCommand.name == 'giphy') - ? 'Search GIFs' - : 'Write a message', + hintText: _getHint(), prefixText: _commandEnabled ? null : ' ', border: OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent)), @@ -443,6 +440,16 @@ class MessageInputState extends State { ); } + String _getHint() { + if (_commandEnabled && _chosenCommand.name == 'giphy') { + return 'Search GIFs'; + } + if (_attachments.isNotEmpty) { + return 'Add a command or send'; + } + return 'Write a message'; + } + void _checkEmoji(String s, BuildContext context) { if (textEditingController.selection.isCollapsed && (s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' || @@ -551,8 +558,10 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Icon(StreamIcons.lightning, - color: StreamChatTheme.of(context).accentColor), + child: Icon( + StreamIcons.lightning, + color: StreamChatTheme.of(context).accentColor, + ), ), Text( 'Instant Commands', @@ -1247,26 +1256,24 @@ class MessageInputState extends State { } 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, - color: Color(0xFF000000).withAlpha(128), - ), + return InkWell( + child: Padding( + padding: + const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0), + child: Icon( + StreamIcons.lightning, + color: Color(0xFF000000).withAlpha(128), ), - onTap: () { - if (_commandsOverlay == null) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } else { - _commandsOverlay?.remove(); - _commandsOverlay = null; - } - }, ), + onTap: () { + if (_commandsOverlay == null) { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } else { + _commandsOverlay?.remove(); + _commandsOverlay = null; + } + }, ); } diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index fd6286a2..ffe52634 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -585,14 +585,22 @@ class _MessageWidgetState extends State { widget.message, attachment, ); - return wrapAttachmentWidget(context, attachmentWidget, - attachment: attachment); + return wrapAttachmentWidget( + context, + attachmentWidget, + attachment: attachment, + ); })?.toList() ?? []; } - Padding wrapAttachmentWidget(BuildContext context, Widget attachmentWidget, - {Attachment attachment}) { + Padding wrapAttachmentWidget( + BuildContext context, + Widget attachmentWidget, { + Attachment attachment, + }) { + final attachmentShape = + widget.attachmentShape ?? widget.shape ?? _getDefaultShape(context); return Padding( padding: EdgeInsets.only( bottom: 4, @@ -605,13 +613,12 @@ class _MessageWidgetState extends State { ? Colors.white : _getBackgroundColor(), clipBehavior: Clip.hardEdge, - shape: widget.attachmentShape ?? - widget.shape ?? - _getDefaultShape(context), + shape: attachmentShape, child: Padding( padding: widget.attachmentPadding, - child: ClipRRect( - borderRadius: BorderRadius.circular(6), + child: Material( + clipBehavior: Clip.hardEdge, + shape: attachmentShape, child: Transform( transform: Matrix4.rotationY(widget.reverse ? pi : 0), alignment: Alignment.center, From 2a7fdc750dc01e29c36930410514748d2cbfb67a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 17:20:12 +0100 Subject: [PATCH 24/25] fix modal with attachment and text --- lib/src/message_actions_modal.dart | 13 +++++++------ lib/src/message_reactions_modal.dart | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 00354a49..16c48e25 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -45,14 +45,15 @@ class MessageActionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - var size = MediaQuery.of(context).size; - var user = StreamChat.of(context).user; + final size = MediaQuery.of(context).size; + final user = StreamChat.of(context).user; - var roughMaxSize = 2 * size.width / 3; - var roughSentenceSize = + final roughMaxSize = 2 * size.width / 3; + final roughSentenceSize = message.text.length * messageTheme.messageText.fontSize * 1.2; - var divFactor = - roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize); + final divFactor = message.attachments?.isNotEmpty == true + ? 1 + : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); return GestureDetector( behavior: HitTestBehavior.translucent, diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index fc3f68a4..d68bd769 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -36,14 +36,15 @@ class MessageReactionsModal extends StatelessWidget { @override Widget build(BuildContext context) { - var size = MediaQuery.of(context).size; - var user = StreamChat.of(context).user; + final size = MediaQuery.of(context).size; + final user = StreamChat.of(context).user; - var roughMaxSize = 2 * size.width / 3; - var roughSentenceSize = + final roughMaxSize = 2 * size.width / 3; + final roughSentenceSize = message.text.length * messageTheme.messageText.fontSize * 1.2; - var divFactor = - roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize); + final divFactor = message.attachments?.isNotEmpty == true + ? 1 + : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); return GestureDetector( behavior: HitTestBehavior.translucent, From 0efce1bf3a2b27c4a48dbad73672c0e19f6512b5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:33:44 +0100 Subject: [PATCH 25/25] fix tests --- lib/src/message_widget.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index ffe52634..4f551655 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -816,7 +816,8 @@ class _MessageWidgetState extends State { ), ), if (widget.message.attachments - .any((element) => element.ogScrapeUrl != null)) + ?.any((element) => element.ogScrapeUrl != null) == + true) _buildUrlAttachment(), ], ), @@ -843,7 +844,8 @@ class _MessageWidgetState extends State { } if (widget.message.attachments - .any((element) => element.ogScrapeUrl != null)) { + ?.any((element) => element.ogScrapeUrl != null) == + true) { return Color(0xFFE9F2FF); }