From f10faee751fb6a3e98631d6d6c6f17fa54512aa3 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Nov 2020 16:02:04 +0530 Subject: [PATCH 001/101] 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 002/101] 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 003/101] 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 004/101] 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 005/101] 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 006/101] 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 007/101] 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 008/101] 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 009/101] 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 010/101] 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 011/101] 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 012/101] 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 013/101] 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 014/101] 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 015/101] 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 016/101] 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 017/101] 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 018/101] 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 019/101] 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 020/101] 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 10e502f84a64072bafe8545f6f69672f27f24e68 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 12 Nov 2020 15:05:31 +0100 Subject: [PATCH 021/101] fix qa --- example/assets/icon_arrow_right.svg | 5 +++++ example/lib/choose_user_page.dart | 15 +++++++++------ example/lib/stream_version.dart | 1 - example/pubspec.yaml | 2 +- lib/src/stream_chat_theme.dart | 1 + lib/src/user_avatar.dart | 1 + 6 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 example/assets/icon_arrow_right.svg diff --git a/example/assets/icon_arrow_right.svg b/example/assets/icon_arrow_right.svg new file mode 100644 index 00000000..7cfa0c56 --- /dev/null +++ b/example/assets/icon_arrow_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index b5296f8a..3a035f34 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -178,9 +178,10 @@ class ChooseUserPage extends StatelessWidget { style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text('Stream test account'), - trailing: Icon( - StreamIcons.arrow_right, - color: StreamChatTheme.of(context).accentColor, + trailing: SvgPicture.asset( + 'assets/icon_arrow_right.svg', + height: 24, + width: 24, ), ); }), @@ -206,9 +207,11 @@ class ChooseUserPage extends StatelessWidget { style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text('Custom settings'), - trailing: Icon( - StreamIcons.arrow_right, - color: StreamChatTheme.of(context).accentColor, + trailing: SvgPicture.asset( + 'assets/icon_arrow_right.svg', + height: 24, + width: 24, + clipBehavior: Clip.none, ), ), ][i]; diff --git a/example/lib/stream_version.dart b/example/lib/stream_version.dart index ee340f89..113e36c0 100644 --- a/example/lib/stream_version.dart +++ b/example/lib/stream_version.dart @@ -24,7 +24,6 @@ class StreamVersion extends StatelessWidget { final streamChatDep = yaml['packages']['stream_chat_flutter']['version']; - print('streamChatDep: ${streamChatDep}'); return Text( 'Stream SDK v ${streamChatDep}', style: TextStyle( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c6743cd4..ce1ab622 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.53+55 +version: 1.0.54+56 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 3b3725ed..967cbbbf 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -229,6 +229,7 @@ class StreamChatThemeData { backgroundColor: isDark ? Colors.black : Colors.white, defaultUserImage: (context, user) => Center( child: CachedNetworkImage( + filterQuality: FilterQuality.high, imageUrl: getRandomPicUrl(user), fit: BoxFit.cover, ), diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 86bf7abd..66edba39 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -49,6 +49,7 @@ class UserAvatar extends StatelessWidget { ), child: hasImage ? CachedNetworkImage( + filterQuality: FilterQuality.high, imageUrl: user.extraData['image'], errorWidget: (_, __, ___) { return streamChatTheme.defaultUserImage(context, user); From 39a6861e4ceb5942a302948f4a44ff41f62c0e75 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 12 Nov 2020 18:26:13 +0100 Subject: [PATCH 022/101] remove waiting for keystorage --- example/lib/choose_user_page.dart | 26 ++++++++++++-------------- example/pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 3a035f34..fd7d2b6c 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -135,20 +135,18 @@ class ChooseUserPage extends StatelessWidget { token, ); - await Future.wait([ - secureStorage.write( - key: kStreamApiKey, - value: kDefaultStreamApiKey, - ), - secureStorage.write( - key: kStreamUserId, - value: user.id, - ), - secureStorage.write( - key: kStreamToken, - value: token, - ), - ]); + secureStorage.write( + key: kStreamApiKey, + value: kDefaultStreamApiKey, + ); + secureStorage.write( + key: kStreamUserId, + value: user.id, + ); + secureStorage.write( + key: kStreamToken, + value: token, + ); if (!kIsWeb) { initNotifications(client); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ce1ab622..f85127f0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.54+56 +version: 1.0.55+57 environment: sdk: ">=2.2.2 <3.0.0" From 7eb9a9dfd9be8d10f9098a4bfa0f4d646d438493 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 12 Nov 2020 18:35:55 +0100 Subject: [PATCH 023/101] remoe editing from giphy messages --- lib/src/message_widget.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index f9871d69..fd6286a2 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -493,7 +493,10 @@ class _MessageWidgetState extends State { message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onThreadTap: widget.onThreadTap, - showEditMessage: widget.showEditMessage, + showEditMessage: widget.showEditMessage && + widget.message.attachments + ?.any((element) => element.type == 'giphy') != + true, showReactions: widget.showReactions, showReply: widget.showReplyIndicator && widget.onThreadTap != null, From 0eb3aee4212984d5e249951d6b476eefeb40070f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 12 Nov 2020 18:52:10 +0100 Subject: [PATCH 024/101] fix group images --- lib/src/group_image.dart | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/src/group_image.dart b/lib/src/group_image.dart index 2385007d..96e25f74 100644 --- a/lib/src/group_image.dart +++ b/lib/src/group_image.dart @@ -46,9 +46,16 @@ class GroupImage extends StatelessWidget { .take(2) .map((url) => Flexible( fit: FlexFit.tight, - child: CachedNetworkImage( - imageUrl: url, + child: FittedBox( fit: BoxFit.cover, + clipBehavior: Clip.antiAlias, + child: Transform.scale( + scale: 1.2, + child: CachedNetworkImage( + imageUrl: url, + fit: BoxFit.cover, + ), + ), ), )) .toList(), @@ -64,9 +71,16 @@ class GroupImage extends StatelessWidget { .skip(2) .map((url) => Flexible( fit: FlexFit.tight, - child: CachedNetworkImage( - imageUrl: url, + child: FittedBox( fit: BoxFit.cover, + clipBehavior: Clip.antiAlias, + child: Transform.scale( + scale: 1.2, + child: CachedNetworkImage( + imageUrl: url, + fit: BoxFit.cover, + ), + ), ), )) .toList(), From 44dc5c24ae166a968c97ef91c48e30677c229c5d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 11:25:55 +0100 Subject: [PATCH 025/101] put error text inside textfield --- example/ios/Podfile.lock | 2 +- example/lib/advanced_options_page.dart | 85 +++++++++++++++++++++----- example/pubspec.yaml | 2 +- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f23764fa..5b00ea2d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -268,7 +268,7 @@ SPEC CHECKSUMS: SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e - wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4 + wakelock: bfc7955c418d0db797614075aabbc58a39ab5107 PODFILE CHECKSUM: eb001256612a59f8f9e4d083ad8b9671e69dd184 diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 8a4c79c0..59c59c26 100644 --- a/example/lib/advanced_options_page.dart +++ b/example/lib/advanced_options_page.dart @@ -17,10 +17,13 @@ class _AdvancedOptionsPageState extends State { final _formKey = GlobalKey(); final TextEditingController _apiKeyController = TextEditingController(); + String _apiKeyError; final TextEditingController _userIdController = TextEditingController(); + String _userIdError; final TextEditingController _userTokenController = TextEditingController(); + String _userTokenError; final TextEditingController _usernameController = TextEditingController(); @@ -65,17 +68,34 @@ class _AdvancedOptionsPageState extends State { children: [ TextFormField( controller: _apiKeyController, + onChanged: (_) { + if (_apiKeyError != null) { + setState(() { + _apiKeyError = null; + }); + } + }, validator: (value) { if (value.isEmpty) { - return 'Please enter the Chat API Key'; + setState(() { + _apiKeyError = 'Please enter the Chat API Key'; + }); + return _apiKeyError; } return null; }, + style: TextStyle( + fontSize: 14, + color: Colors.black, + ), decoration: InputDecoration( + errorStyle: TextStyle(fontSize: 0), labelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(.5), + color: _apiKeyError != null + ? Color(0xffff3742) + : Colors.black.withOpacity(.5), ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -83,7 +103,8 @@ class _AdvancedOptionsPageState extends State { ), fillColor: Color(0xffF5F5F5), filled: true, - labelText: 'Chat API Key', + labelText: + 'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}', ), textInputAction: TextInputAction.next, ), @@ -91,18 +112,35 @@ class _AdvancedOptionsPageState extends State { padding: const EdgeInsets.only(top: 8.0), child: TextFormField( controller: _userIdController, + onChanged: (_) { + if (_userIdError != null) { + setState(() { + _userIdError = null; + }); + } + }, validator: (value) { if (value.isEmpty) { - return 'Please enter the User ID'; + setState(() { + _userIdError = 'Please enter the User ID'; + }); + return _userIdError; } return null; }, + style: TextStyle( + fontSize: 14, + color: Colors.black, + ), textInputAction: TextInputAction.next, decoration: InputDecoration( + errorStyle: TextStyle(fontSize: 0), labelStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, - color: Colors.black.withOpacity(.5), + color: _userIdError != null + ? Color(0xffff3742) + : Colors.black.withOpacity(.5), ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -110,26 +148,44 @@ class _AdvancedOptionsPageState extends State { ), fillColor: Color(0xffF5F5F5), filled: true, - labelText: 'User ID', + labelText: + 'User ID ${_userIdError != null ? ': $_userIdError' : ''}', ), ), ), Padding( padding: const EdgeInsets.only(top: 8.0), child: TextFormField( + onChanged: (_) { + if (_userTokenError != null) { + setState(() { + _userTokenError = null; + }); + } + }, controller: _userTokenController, validator: (value) { if (value.isEmpty) { - return 'Please enter the user token'; + setState(() { + _userTokenError = 'Please enter the user token'; + }); + return _userTokenError; } return null; }, + style: TextStyle( + fontSize: 14, + color: Colors.black, + ), textInputAction: TextInputAction.next, decoration: InputDecoration( + errorStyle: TextStyle(fontSize: 0), labelStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, - color: Colors.black.withOpacity(.5), + color: _userTokenError != null + ? Color(0xffff3742) + : Colors.black.withOpacity(.5), ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -137,7 +193,8 @@ class _AdvancedOptionsPageState extends State { ), fillColor: Color(0xffF5F5F5), filled: true, - labelText: 'User Token', + labelText: + 'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}', ), ), ), @@ -183,13 +240,13 @@ class _AdvancedOptionsPageState extends State { if (loading) { return; } - loading = true; if (_formKey.currentState.validate()) { final apiKey = _apiKeyController.text; final userId = _userIdController.text; final userToken = _userTokenController.text; final username = _usernameController.text; + loading = true; showDialog( barrierDismissible: false, context: context, @@ -232,11 +289,9 @@ class _AdvancedOptionsPageState extends State { errorText = e['message'] ?? errorText; } Navigator.pop(context); - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text(errorText), - ), - ); + setState(() { + _apiKeyError = errorText; + }); loading = false; await client.disconnect(); return; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f85127f0..d4da197e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.55+57 +version: 1.0.56+58 environment: sdk: ">=2.2.2 <3.0.0" From c113862005cf1f942edfae091d756d2ca504c938 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 11:35:40 +0100 Subject: [PATCH 026/101] fix error label --- example/lib/advanced_options_page.dart | 7 +++---- example/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 59c59c26..56c9dbf0 100644 --- a/example/lib/advanced_options_page.dart +++ b/example/lib/advanced_options_page.dart @@ -104,7 +104,7 @@ class _AdvancedOptionsPageState extends State { fillColor: Color(0xffF5F5F5), filled: true, labelText: - 'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}', + 'Chat API Key ${_apiKeyError != null ? ':$_apiKeyError' : ''}', ), textInputAction: TextInputAction.next, ), @@ -149,7 +149,7 @@ class _AdvancedOptionsPageState extends State { fillColor: Color(0xffF5F5F5), filled: true, labelText: - 'User ID ${_userIdError != null ? ': $_userIdError' : ''}', + 'User ID ${_userIdError != null ? ':$_userIdError' : ''}', ), ), ), @@ -194,7 +194,7 @@ class _AdvancedOptionsPageState extends State { fillColor: Color(0xffF5F5F5), filled: true, labelText: - 'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}', + 'User Token ${_userTokenError != null ? ':$_userTokenError' : ''}', ), ), ), @@ -265,7 +265,6 @@ class _AdvancedOptionsPageState extends State { ), ); - print('CREATE CLIENT'); final client = Client( apiKey, logLevel: Level.INFO, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d4da197e..85fd549e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.56+58 +version: 1.0.57+59 environment: sdk: ">=2.2.2 <3.0.0" From a649e2d5e7b083039735df6b707ab44a13896eb3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 13 Nov 2020 12:26:47 +0100 Subject: [PATCH 027/101] 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 028/101] 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 029/101] 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 030/101] 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 581a9a2def432113fce2e866703f2f3fddeebe6b Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Sat, 14 Nov 2020 14:47:48 -0400 Subject: [PATCH 031/101] expose builder for empty state --- lib/src/channel_list_view.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index f88eefbe..b716fff6 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -63,6 +63,7 @@ class ChannelListView extends StatefulWidget { this.channelPreviewBuilder, this.separatorBuilder, this.errorBuilder, + this.emptyBuilder, this.onImageTap, this.pullToRefresh = true, }) : super(key: key); @@ -70,6 +71,9 @@ class ChannelListView extends StatefulWidget { /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. @@ -231,7 +235,11 @@ class _ChannelListViewState extends State final channels = snapshot.data; - if (channels.isEmpty) { + if (channels.isEmpty && widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + + if (channels.isEmpty && widget.emptyBuilder == null) { return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( From 83787846c90673ca6d657b7021efdb5ca4a47a7c Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Sat, 14 Nov 2020 14:47:48 -0400 Subject: [PATCH 032/101] expose builder for empty state --- lib/src/channel_list_view.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index f88eefbe..b716fff6 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -63,6 +63,7 @@ class ChannelListView extends StatefulWidget { this.channelPreviewBuilder, this.separatorBuilder, this.errorBuilder, + this.emptyBuilder, this.onImageTap, this.pullToRefresh = true, }) : super(key: key); @@ -70,6 +71,9 @@ class ChannelListView extends StatefulWidget { /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. @@ -231,7 +235,11 @@ class _ChannelListViewState extends State final channels = snapshot.data; - if (channels.isEmpty) { + if (channels.isEmpty && widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + + if (channels.isEmpty && widget.emptyBuilder == null) { return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( From 13604cee386afcb31dd2b6fd061d2dfcba00b0e1 Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Sun, 15 Nov 2020 21:32:53 -0400 Subject: [PATCH 033/101] Change material type to transparent --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 13f8291c..78833c8c 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -605,10 +605,10 @@ class MessageInputState extends State { Material _buildAttachmentButton() { return Material( clipBehavior: Clip.hardEdge, + type: MaterialType.transparency, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(32), ), - color: Colors.transparent, child: IconButton( onPressed: () { showAttachmentModal(); From 78e23bc256f7ddb6f369559c59ae66e42d611ec5 Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Sun, 15 Nov 2020 21:33:51 -0400 Subject: [PATCH 034/101] expose input text style --- lib/src/message_input.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 78833c8c..729866ee 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -93,6 +93,7 @@ class MessageInput extends StatefulWidget { this.actions, this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, + this.inputTextStyle, }) : super(key: key); /// Message to edit @@ -138,6 +139,10 @@ class MessageInput extends StatefulWidget { /// Map that defines a thumbnail builder for an attachment type final Map attachmentThumbnailBuilders; + /// Text style used in message text field. If null, [MessageInput] uses + /// `Theme.of(context).textTheme.bodyText2`. + final TextStyle inputTextStyle; + @override MessageInputState createState() => MessageInputState(); @@ -276,10 +281,12 @@ class MessageInputState extends State { _typingStarted = true; }); }, - style: Theme.of(context).textTheme.bodyText2, + style: widget.inputTextStyle ?? Theme.of(context).textTheme.bodyText2, autofocus: false, decoration: InputDecoration( hintText: 'Write a message', + hintStyle: widget.inputTextStyle ?? + Theme.of(context).textTheme.bodyText2, prefixText: ' ', border: InputBorder.none, ), From 77d62eefddc14813461d9e2a2ce963bee6613667 Mon Sep 17 00:00:00 2001 From: Neevash Ramdial Date: Sun, 15 Nov 2020 21:34:14 -0400 Subject: [PATCH 035/101] expose style option for attachment icon --- lib/src/message_input.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 729866ee..20a2bdf6 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -94,6 +94,7 @@ class MessageInput extends StatefulWidget { this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, this.inputTextStyle, + this.attachmentIconColor }) : super(key: key); /// Message to edit @@ -143,6 +144,9 @@ class MessageInput extends StatefulWidget { /// `Theme.of(context).textTheme.bodyText2`. final TextStyle inputTextStyle; + /// Color used for attachment icon. + final Color attachmentIconColor; + @override MessageInputState createState() => MessageInputState(); @@ -622,6 +626,7 @@ class MessageInputState extends State { }, icon: Icon( Icons.add_circle_outline, + color: widget.attachmentIconColor, ), ), ); From ef8e9788e36a209af5a663f6c0b78025016bc09c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 12:37:50 +0100 Subject: [PATCH 036/101] upgrade llc --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 241025d0..04d07d55 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.11 + stream_chat: ^0.2.11+1 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 From f9415636895cf96ff9bb89e5dd806ac558ac86c2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 15:04:49 +0100 Subject: [PATCH 037/101] update llc dependency --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 04d07d55..a3a1f55a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.11+1 + stream_chat: ^0.2.11+2 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 From 0efce1bf3a2b27c4a48dbad73672c0e19f6512b5 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:33:44 +0100 Subject: [PATCH 038/101] 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); } From 2c6c2f5bc168583ebe2ac4cf1d336ade9ce270e9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:41:58 +0100 Subject: [PATCH 039/101] update message input radius --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index bac36ef7..139d6ab7 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -341,7 +341,7 @@ class MessageInputState extends State { child: Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32.0), + borderRadius: BorderRadius.circular(20.0), border: Border.all( color: Colors.grey, ), From c04d72d46f3af897cdcdcd1779a05e84402766de Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:44:00 +0100 Subject: [PATCH 040/101] update message input radius --- lib/src/media_list_view.dart | 3 ++- lib/src/message_input.dart | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 627a480c..225d327b 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -63,10 +63,11 @@ class _MediaListViewState extends State { right: 8, ), child: CircleAvatar( - maxRadius: 12.0, + radius: 12, backgroundColor: Colors.white, child: Icon( StreamIcons.check, + size: 24, color: Colors.black, ), ), diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 139d6ab7..e393cd3c 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1170,8 +1170,8 @@ class MessageInputState extends State { Positioned _buildRemoveButton(_SendingAttachment attachment) { return Positioned( - height: 16, - width: 16, + height: 24, + width: 24, top: 4, right: 4, child: RawMaterialButton( @@ -1192,7 +1192,7 @@ class MessageInputState extends State { child: Center( child: Icon( StreamIcons.close, - size: 15, + size: 24, color: Colors.white, ), ), From 4ff3a225b2e68c468ce43c6dbb90c1e9801c3f17 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:49:00 +0100 Subject: [PATCH 041/101] add record icon --- lib/src/message_input.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e393cd3c..872864bd 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -630,6 +630,7 @@ class MessageInputState extends State { IconButton( icon: Icon( StreamIcons.picture, + size: 24, color: _filePickerIndex == 0 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -643,6 +644,7 @@ class MessageInputState extends State { IconButton( icon: Icon( StreamIcons.folder, + size: 24, color: _filePickerIndex == 1 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -654,6 +656,7 @@ class MessageInputState extends State { IconButton( icon: Icon( StreamIcons.camera, + size: 24, color: _filePickerIndex == 2 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -662,6 +665,18 @@ class MessageInputState extends State { pickFile(DefaultAttachmentTypes.image, true); }, ), + IconButton( + icon: Icon( + StreamIcons.record, + size: 24, + color: _filePickerIndex == 2 + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.5), + ), + onPressed: () { + pickFile(DefaultAttachmentTypes.video, true); + }, + ), ], ), GestureDetector( @@ -794,10 +809,6 @@ class MessageInputState extends State { ); }); break; - case 1: - break; - case 2: - break; } } From b0cbcbc6c846a83ac8fa2f02e47c7e7a8ca464a4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 16:59:58 +0100 Subject: [PATCH 042/101] update giphy label --- lib/src/giphy_attachment.dart | 134 ++++++++++++++++------------------ 1 file changed, 64 insertions(+), 70 deletions(-) diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index bfa21239..8cec9ce5 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -283,80 +283,74 @@ class GiphyAttachment extends StatelessWidget { Widget _buildSentAttachment(context) { return Container( - child: Column( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16.0), - bottomRight: Radius.circular(0.0), - topLeft: Radius.circular(16.0), - bottomLeft: Radius.circular(16.0), - ), - ), - clipBehavior: Clip.antiAlias, - child: GestureDetector( - onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (_) { - return FullScreenImage( - url: attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl, - ); - })); + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) { + return FullScreenImage( + url: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl, + ); + })); + }, + child: Stack( + children: [ + CachedNetworkImage( + height: size?.height, + width: size?.width, + placeholder: (_, __) { + return Container( + width: size?.width, + height: size?.height, + child: Center( + child: CircularProgressIndicator(), + ), + ); }, - child: CachedNetworkImage( - height: size?.height, - width: size?.width, - placeholder: (_, __) { - return Container( - width: size?.width, - height: size?.height, - child: Center( - child: CircularProgressIndicator(), - ), - ); - }, - imageUrl: attachment.thumbUrl ?? - attachment.imageUrl ?? - attachment.assetUrl, - errorWidget: (context, url, error) => AttachmentError( - attachment: attachment, - size: size, + imageUrl: attachment.thumbUrl ?? + attachment.imageUrl ?? + attachment.assetUrl, + errorWidget: (context, url, error) => AttachmentError( + attachment: attachment, + size: size, + ), + fit: BoxFit.cover, + ), + Positioned( + bottom: 8, + left: 8, + child: Material( + color: Colors.black.withOpacity(.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Row( + children: [ + Icon( + StreamIcons.lightning, + color: Colors.white, + size: 16, + ), + Text( + 'GIPHY', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 11, + ), + ), + ], + ), ), - fit: BoxFit.cover, ), ), - ), - Padding( - padding: const EdgeInsets.only( - top: 8.0, - bottom: 8, - ), - child: Row( - children: [ - Row( - children: [ - Icon( - StreamIcons.lightning, - color: StreamChatTheme.of(context).accentColor, - size: 15.0, - ), - Text( - 'GIPHY', - style: TextStyle( - color: StreamChatTheme.of(context).accentColor, - fontWeight: FontWeight.bold, - fontSize: 11.0, - ), - ), - ], - ), - ], - mainAxisAlignment: MainAxisAlignment.start, - ), - ) - ], + ], + ), ), ); } From bb243d7c7acf055c082c040f59e475ff792ada72 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 16 Nov 2020 17:18:57 +0100 Subject: [PATCH 043/101] bump version (#137) --- CHANGELOG.md | 7 ++++++ lib/src/channel_list_view.dart | 6 ++--- lib/src/message_input.dart | 46 ++++++++++++++++++---------------- pubspec.yaml | 4 +-- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03db8a6d..945199b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.13 + +- Update llc dependency +- Send parent_id in typing events +- Expose addition input styling options +- Expose builder for empty channel state + ## 0.2.12 - Upgrade dependencies diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index b716fff6..121a7d10 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -72,7 +72,7 @@ class ChannelListView extends StatefulWidget { final Widget Function(Error error) errorBuilder; /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; + final WidgetBuilder emptyBuilder; /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. @@ -236,8 +236,8 @@ class _ChannelListViewState extends State final channels = snapshot.data; if (channels.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } + return widget.emptyBuilder(context); + } if (channels.isEmpty && widget.emptyBuilder == null) { return LayoutBuilder( diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 20a2bdf6..02fbbd8f 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -77,25 +77,25 @@ enum DefaultAttachmentTypes { /// Modify it to change the widget appearance. class MessageInput extends StatefulWidget { /// Instantiate a new MessageInput - MessageInput({ - Key key, - this.onMessageSent, - this.preMessageSending, - this.parentMessage, - this.editMessage, - this.maxHeight = 150, - this.keyboardType = TextInputType.multiline, - this.disableAttachments = false, - this.doImageUploadRequest, - this.doFileUploadRequest, - this.initialMessage, - this.textEditingController, - this.actions, - this.actionsLocation = ActionsLocation.left, - this.attachmentThumbnailBuilders, - this.inputTextStyle, - this.attachmentIconColor - }) : super(key: key); + MessageInput( + {Key key, + this.onMessageSent, + this.preMessageSending, + this.parentMessage, + this.editMessage, + this.maxHeight = 150, + this.keyboardType = TextInputType.multiline, + this.disableAttachments = false, + this.doImageUploadRequest, + this.doFileUploadRequest, + this.initialMessage, + this.textEditingController, + this.actions, + this.actionsLocation = ActionsLocation.left, + this.attachmentThumbnailBuilders, + this.inputTextStyle, + this.attachmentIconColor}) + : super(key: key); /// Message to edit final Message editMessage; @@ -253,7 +253,9 @@ class MessageInputState extends State { controller: textEditingController, focusNode: _focusNode, onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); + StreamChannel.of(context).channel.keyStroke( + widget.parentMessage?.id, + ); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -289,8 +291,8 @@ class MessageInputState extends State { autofocus: false, decoration: InputDecoration( hintText: 'Write a message', - hintStyle: widget.inputTextStyle ?? - Theme.of(context).textTheme.bodyText2, + hintStyle: + widget.inputTextStyle ?? Theme.of(context).textTheme.bodyText2, prefixText: ' ', border: InputBorder.none, ), diff --git a/pubspec.yaml b/pubspec.yaml index 7733d25b..00be2548 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.12 +version: 0.2.13 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -23,7 +23,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.10 + stream_chat: ^0.2.13 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 From 413dec0336d66b5bbddefa2b385149e58770fb62 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 16:00:52 +0530 Subject: [PATCH 044/101] feat: Navigate to new chat screen on drawer item click Signed-off-by: xsahil03x --- example/lib/main.dart | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e3f43e2..7c288eae 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -63,6 +63,13 @@ class ChannelListPage extends StatelessWidget { Widget build(BuildContext context) { final user = StreamChat.of(context).user; return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Stream Chat', + style: TextStyle(color: Colors.black), + ), + ), drawer: Drawer( child: SafeArea( child: Padding( @@ -97,6 +104,12 @@ class ChannelListPage extends StatelessWidget { ), ), ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewChatScreen()), + ); + }, leading: Icon(StreamIcons.edit), title: Text( 'New direct message', @@ -434,3 +447,24 @@ class _CreateChannelPageState extends State { }).whenComplete(() => loading = false); } } + +class NewChatScreen extends StatefulWidget { + @override + _NewChatScreenState createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'New Chat', + style: TextStyle(color: Colors.black), + ), + ), + body: Container(), + ); + } +} From 0d7bf98a5b2797c4771543dfb2f2a4357dda292d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Nov 2020 11:38:34 +0100 Subject: [PATCH 045/101] add shimmer effect --- example/lib/main.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/channel_list_view.dart | 317 +++++++++++++++++++++------------ pubspec.yaml | 1 + 4 files changed, 203 insertions(+), 119 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e3f43e2..c09030f3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -21,7 +21,7 @@ void main() async { logLevel: Level.INFO, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, - persistenceEnabled: true, + persistenceEnabled: false, ); if (userId != null) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8fc2dadb..fca549d2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.59+61 +version: 1.0.60+62 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index a10ad4bc..8b66db34 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/channels_bloc.dart'; import 'package:stream_chat_flutter/src/utils.dart'; @@ -163,127 +164,209 @@ class _ChannelListViewState extends State return StreamBuilder>( stream: channelsBlocState.channelsStream, builder: (context, snapshot) { + var child; if (snapshot.hasError) { - if (snapshot.error is Error) { - print((snapshot.error as Error).stackTrace); + child = _buildErrorWidget( + snapshot, + context, + channelsBlocState, + ); + } else if (!snapshot.hasData) { + child = _buildLoadingWidget(); + } else { + final channels = snapshot.data; + + if (channels.isEmpty && widget.emptyBuilder != null) { + child = widget.emptyBuilder(context); } - if (widget.errorBuilder != null) { - return widget.errorBuilder(snapshot.error); + if (channels.isEmpty && widget.emptyBuilder == null) { + child = LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('You have no channels currently'), + ), + ), + ); + }, + ); } - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError; - if (dioError.type == DioErrorType.RESPONSE) { - message = dioError.message; - } else { - message = 'Check your connection and retry'; - } + if (channels.isNotEmpty) { + child = ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channels); + }, + childCount: (channels.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channels.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); } - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text.rich( - TextSpan( - children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only( - right: 2.0, - ), - child: Icon(Icons.error_outline), - ), - ), - TextSpan(text: 'Error loading channels'), - ], - ), - style: Theme.of(context).textTheme.headline6, - ), - Padding( - padding: const EdgeInsets.only( - top: 16.0, - ), - child: Text(message), - ), - FlatButton( - onPressed: () { - channelsBlocState.queryChannels( - filter: widget.filter, - sortOptions: widget.sort, - paginationParams: widget.pagination, - options: widget.options, - ); - }, - child: Text('Retry'), - ), - ], - ), - ); } - if (!snapshot.hasData) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: CircularProgressIndicator(), - ), - ), - ); - }, - ); - } - - final channels = snapshot.data; - - if (channels.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (channels.isEmpty && widget.emptyBuilder == null) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('You have no channels currently'), - ), - ), - ); - }, - ); - } - - return ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); - }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), + return AnimatedSwitcher( + child: child, + duration: Duration(milliseconds: 500), ); }); } + Widget _buildLoadingWidget() { + return ListView( + physics: AlwaysScrollableScrollPhysics(), + children: List.generate( + 25, + (i) { + if (i % 2 != 0) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, i); + } + return _separatorBuilder(context, i); + } + return _buildLoadingItem(); + }, + ), + ); + } + + Shimmer _buildLoadingItem() { + return Shimmer.fromColors( + baseColor: Color(0xffE5E5E5), + highlightColor: Color(0xffffffff), + child: ListTile( + leading: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: Align( + alignment: Alignment.centerLeft, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + ), + constraints: BoxConstraints.tightFor( + height: 16, + width: 82, + ), + ), + ), + subtitle: Row( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + ), + constraints: BoxConstraints.tightFor( + height: 16, + width: 238, + ), + ), + ), + Container( + margin: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + ), + constraints: BoxConstraints.tightFor( + height: 16, + width: 42, + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorWidget( + AsyncSnapshot> snapshot, + BuildContext context, + ChannelsBlocState channelsBlocState, + ) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + if (widget.errorBuilder != null) { + return widget.errorBuilder(snapshot.error); + } + + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 2.0, + ), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: 'Error loading channels'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Text(message), + ), + FlatButton( + onPressed: () { + channelsBlocState.queryChannels( + filter: widget.filter, + sortOptions: widget.sort, + paginationParams: widget.pagination, + options: widget.options, + ); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + Widget _itemBuilder(context, int i, List channels) { if (i % 2 != 0) { if (widget.separatorBuilder != null) { @@ -441,7 +524,9 @@ class _ChannelListViewState extends State } Widget _buildQueryProgressIndicator( - context, ChannelsBlocState channelsProvider) { + context, + ChannelsBlocState channelsProvider, + ) { return StreamBuilder( stream: channelsProvider.queryChannelsLoading, initialData: false, @@ -457,13 +542,11 @@ class _ChannelListViewState extends State ), ); } - return Container( - height: 100, - padding: EdgeInsets.all(32), - child: Center( - child: snapshot.data ? CircularProgressIndicator() : Container(), - ), - ); + return snapshot.data + ? _buildLoadingItem() + : Container( + height: 70, + ); }); } diff --git a/pubspec.yaml b/pubspec.yaml index 9c1b0a29..804c9728 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: flutter_svg: ^0.19.1 flutter_portal: ^0.3.0 cached_network_image: ^2.2.0+1 + shimmer: ^1.1.2 flutter_markdown: ^0.5.0 url_launcher: ^5.4.11 emojis: ^0.9.3 From a8a5e5dfdf67d2ad3c3d422ed138f72ece0047b9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 18:19:33 +0530 Subject: [PATCH 046/101] [Channels Bloc] Fix typo Signed-off-by: Sahil Kumar --- lib/src/channels_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 3af2300d..474d9599 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -31,7 +31,7 @@ class ChannelsBloc extends StatefulWidget { streamChatState = context.findAncestorStateOfType(); if (streamChatState == null) { - throw Exception('You must have a ChannelsBloc widget as anchestor'); + throw Exception('You must have a ChannelsBloc widget as ancestor'); } return streamChatState; From cd8ca90052836203a8a47bbc4049793d93fe5b38 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Nov 2020 21:30:05 +0530 Subject: [PATCH 047/101] feat: user list Signed-off-by: Sahil Kumar --- lib/src/user_list_view.dart | 468 +++++++++++++++++++++++++++++++++++ lib/src/users_bloc.dart | 114 +++++++++ lib/stream_chat_flutter.dart | 1 + 3 files changed, 583 insertions(+) create mode 100644 lib/src/user_list_view.dart create mode 100644 lib/src/users_bloc.dart diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart new file mode 100644 index 00000000..5b661ee8 --- /dev/null +++ b/lib/src/user_list_view.dart @@ -0,0 +1,468 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/users_bloc.dart'; + +import 'stream_chat.dart'; + +typedef UserTapCallback = void Function(User, Widget); + +/// +/// It shows the list of current users. +/// +/// ```dart +/// class UsersListPage extends StatelessWidget { +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: UsersListView( +/// filter: { +/// 'members': { +/// '\$in': [StreamChat.of(context).user.id], +/// } +/// }, +/// sort: [SortOption('last_message_at')], +/// pagination: PaginationParams( +/// limit: 20, +/// ), +/// channelWidget: ChannelPage(), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// +/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels. +/// The widget uses a [ListView.custom] to render the list of channels. +/// +/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class UserListView extends StatefulWidget { + /// Instantiate a new UserListView + const UserListView({ + Key key, + this.errorBuilder, + this.emptyBuilder, + this.filter, + this.options, + this.sort, + this.pagination, + this.onUserTap, + this.onUserLongPress, + this.userWidget, + this.separatorBuilder, + this.onImageTap, + this.swipeToAction = false, + this.pullToRefresh = true, + }) : super(key: key); + + /// The builder that will be used in case of error + final Widget Function(Error error) errorBuilder; + + /// If true a default swipe to action behaviour will be added to this widget + final bool swipeToAction; + + /// The builder used when the channel list is empty. + final WidgetBuilder emptyBuilder; + + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. + final Map filter; + + /// Query channels options. + /// + /// state: if true returns the Channel state + /// watch: if true listen to changes to this Channel in real time. + final Map options; + + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. + /// Direction can be ascending or descending. + final List sort; + + /// Pagination parameters + /// limit: the number of users to return (max is 30) + /// offset: the offset (max is 1000) + /// message_limit: how many messages should be included to each channel + final PaginationParams pagination; + + /// Function called when tapping on a channel + /// By default it calls [Navigator.push] building a [MaterialPageRoute] + /// with the widget [userWidget] as child. + final UserTapCallback onUserTap; + + /// Function called when long pressing on a channel + final Function(User) onUserLongPress; + + /// Widget used when opening a channel + final Widget userWidget; + + // /// Builder used to create a custom channel preview + // final ChannelPreviewBuilder channelPreviewBuilder; + + /// Builder used to create a custom item separator + final Function(BuildContext, int) separatorBuilder; + + /// The function called when the image is tapped + final Function(User) onImageTap; + + /// Set it to false to disable the pull-to-refresh widget + final bool pullToRefresh; + + @override + _UserListViewState createState() => _UserListViewState(); +} + +class _UserListViewState extends State + with WidgetsBindingObserver { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final usersBloc = UsersBloc.of(context); + + if (!widget.pullToRefresh) { + return _buildListView(usersBloc); + } + + return RefreshIndicator( + onRefresh: () async { + return usersBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + options: widget.options, + pagination: widget.pagination, + ); + }, + child: _buildListView(usersBloc), + ); + } + + StreamBuilder> _buildListView( + UsersBlocState usersBlocState, + ) { + return StreamBuilder( + stream: usersBlocState.usersStream, + builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + + if (widget.errorBuilder != null) { + return widget.errorBuilder(snapshot.error); + } + + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only( + right: 2.0, + ), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: 'Error loading channels'), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + Padding( + padding: const EdgeInsets.only( + top: 16.0, + ), + child: Text(message), + ), + FlatButton( + onPressed: () { + usersBlocState.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + }, + child: Text('Retry'), + ), + ], + ), + ); + } + + if (!snapshot.hasData) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + }, + ); + } + + final users = snapshot.data; + + if (users.isEmpty && widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + + if (users.isEmpty && widget.emptyBuilder == null) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text('There are no users currently'), + ), + ), + ); + }, + ); + } + + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, users); + }, + childCount: (users.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = users.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + }, + ); + } + + Widget _itemBuilder(context, int i, List users) { + if (i % 2 != 0) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, i); + } + return _separatorBuilder(context, i); + } + + i = i ~/ 2; + + final usersProvider = UsersBloc.of(context); + if (i < users.length) { + final user = users[i]; + + UserTapCallback onTap; + if (widget.onUserTap != null) { + onTap = widget.onUserTap; + } else { + onTap = (client, _) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) { + // return StreamChannel( + // child: widget.userWidget, + // channel: client, + // ); + // }, + // ), + // ); + }; + } + + return Container( + key: ValueKey('USER-${user.id}'), + child: Builder( + builder: (context) { + Widget child; + child = ListTile( + title: Text(user.name), + ); + // if (widget.channelPreviewBuilder != null) { + // child = Stack( + // children: [ + // widget.channelPreviewBuilder( + // context, + // channel, + // ), + // Positioned.fill( + // child: Material( + // color: Colors.transparent, + // child: InkWell( + // onTap: () { + // onTap(channel, widget.channelWidget); + // }, + // ), + // ), + // ), + // ], + // ); + // } else { + // final backgroundColor = + // Theme.of(context).brightness == Brightness.light + // ? Color(0xffEBEBEB) + // : Color(0xff141414); + // child = Slidable( + // enabled: widget.swipeToAction, + // actionPane: SlidableBehindActionPane(), + // actionExtentRatio: 0.12, + // closeOnScroll: true, + // secondaryActions: [ + // IconSlideAction( + // color: backgroundColor, + // icon: Icons.more_horiz, + // onTap: () { + // showModalBottomSheet( + // clipBehavior: Clip.hardEdge, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.only( + // topLeft: Radius.circular(32), + // topRight: Radius.circular(32), + // ), + // ), + // context: context, + // builder: (context) { + // return ChannelBottomSheet(channel: channel); + // }, + // ); + // }, + // ), + // IconSlideAction( + // color: backgroundColor, + // icon: StreamIcons.mute, + // onTap: () async { + // if (!channel.isMuted) { + // await channel.mute(); + // } else { + // await channel.unmute(); + // } + // }, + // ), + // if (channel.isGroup && !channel.isDistinct) + // IconSlideAction( + // color: backgroundColor, + // icon: StreamIcons.user_minus, + // onTap: () async { + // final confirm = await showConfirmationDialog( + // context, + // 'Do you want to leave the group?', + // ); + // if (confirm == true) { + // await channel + // .removeMembers([StreamChat.of(context).user.id]); + // } + // }, + // ), + // if (!channel.isGroup && !channel.isDistinct) + // IconSlideAction( + // color: backgroundColor, + // icon: Icons.delete_outline, + // onTap: () async { + // final confirm = await showConfirmationDialog( + // context, + // 'Do you want to delete the chat?', + // ); + // if (confirm == true) { + // await channel + // .removeMembers([StreamChat.of(context).user.id]); + // } + // }, + // ), + // ], + // child: Container( + // color: StreamChatTheme.of(context).backgroundColor, + // child: ChannelPreview( + // onLongPress: widget.onChannelLongPress, + // channel: channel, + // onImageTap: widget.onImageTap != null + // ? () { + // widget.onImageTap(channel); + // } + // : null, + // onTap: (channel) { + // onTap(channel, widget.userWidget); + // }, + // ), + // ), + // ); + // } + return child; + }, + ), + ); + } else { + return _buildQueryProgressIndicator(context, usersProvider); + } + } + + Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { + return StreamBuilder( + stream: usersProvider.queryUsersLoading, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: Color(0xffd0021B).withAlpha(26), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center( + child: Text('Error loading users'), + ), + ), + ); + } + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); + }); + } + + Widget _separatorBuilder(context, i) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } +} diff --git a/lib/src/users_bloc.dart b/lib/src/users_bloc.dart new file mode 100644 index 00000000..da2221e0 --- /dev/null +++ b/lib/src/users_bloc.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_chat.dart'; + +/// Widget dedicated to the management of a users list with pagination +class UsersBloc extends StatefulWidget { + /// The widget child + final Widget child; + + /// Instantiate a new UsersBloc + const UsersBloc({ + Key key, + @required this.child, + }) : super(key: key); + + @override + UsersBlocState createState() => UsersBlocState(); + + /// Use this method to get the current [UsersBlocState] instance + static UsersBlocState of(BuildContext context) { + UsersBlocState state; + + state = context.findAncestorStateOfType(); + + if (state == null) { + throw Exception('You must have a UsersBloc widget as ancestor'); + } + + return state; + } +} + +/// The current state of the [UsersBloc] +class UsersBlocState extends State + with AutomaticKeepAliveClientMixin { + /// The current users list + List get users => _usersController.value; + + /// The current users list as a stream + Stream> get usersStream => _usersController.stream; + + final BehaviorSubject> _usersController = BehaviorSubject(); + + final BehaviorSubject _queryUsersLoadingController = + BehaviorSubject.seeded(false); + + /// The stream notifying the state of queryUsers call + Stream get queryUsersLoading => _queryUsersLoadingController.stream; + + /// Calls [Client.queryUsers] updating [queryUsersLoading] stream + Future queryUsers({ + Map filter, + List sort, + Map options, + PaginationParams pagination, + }) async { + final client = StreamChat.of(context).client; + + if (client.state?.user == null || + _queryUsersLoadingController.value == true) { + return; + } + _queryUsersLoadingController.add(true); + try { + final clear = pagination == null || + pagination.offset == null || + pagination.offset == 0; + + final oldUsers = List.from(users ?? []); + + final usersResponse = await client.queryUsers( + filter: filter, + sort: sort, + options: options, + pagination: pagination, + ); + + if (clear) { + _usersController.add(usersResponse.users); + } else { + final temp = oldUsers + usersResponse.users; + _usersController.add(temp); + } + + _queryUsersLoadingController.add(false); + } catch (err, stackTrace) { + _queryUsersLoadingController.addError(err, stackTrace); + } + } + + @override + void initState() { + super.initState(); + final client = StreamChat.of(context).client; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + void dispose() { + _usersController.close(); + _queryUsersLoadingController.close(); + super.dispose(); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index e433e58e..3c9554fc 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -30,3 +30,4 @@ export 'src/typing_indicator.dart'; export 'src/user_avatar.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; +export 'src/user_list_view.dart'; From e5b25d8d06d57896dfbbe9fc08ceb470b727f78e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Tue, 17 Nov 2020 23:32:16 +0530 Subject: [PATCH 048/101] feat: user list --- example/lib/main.dart | 14 ++- lib/src/user_item.dart | 69 +++++++++++++ lib/src/user_list_view.dart | 187 ++++++++++++----------------------- lib/src/users_bloc.dart | 6 -- lib/stream_chat_flutter.dart | 1 + 5 files changed, 148 insertions(+), 129 deletions(-) create mode 100644 lib/src/user_item.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 7c288eae..5fba1c06 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -464,7 +464,19 @@ class _NewChatScreenState extends State { style: TextStyle(color: Colors.black), ), ), - body: Container(), + body: UsersBloc( + child: UserListView( + pagination: PaginationParams( + limit: 25, + ), + sort: [ + SortOption( + 'name', + direction: SortOption.ASC, + ), + ], + ), + ), ); } } diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart new file mode 100644 index 00000000..14b488c7 --- /dev/null +++ b/lib/src/user_item.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/user_list_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'stream_chat_theme.dart'; + +/// +/// It shows the current [User] preview. +/// +/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates. +/// +/// Usually you don't use this widget as it's the default user preview used by [UserListView]. +/// +/// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. +/// Modify it to change the widget appearance. +class UserItem extends StatelessWidget { + /// Instantiate a new UserItem + const UserItem( + {Key key, + @required this.user, + this.onTap, + this.onLongPress, + this.onImageTap}) + : super(key: key); + + /// Function called when tapping this widget + final void Function(User) onTap; + + /// Function called when long pressing this widget + final void Function(User) onLongPress; + + /// User displayed + final User user; + + /// The function called when the image is tapped + final void Function(User) onImageTap; + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: () { + if (onTap != null) { + onTap(user); + } + }, + onLongPress: () { + if (onLongPress != null) { + onLongPress(user); + } + }, + leading: UserAvatar( + user: user, + showOnlineStatus: true, + onTap: (user) { + if (onImageTap != null) { + onImageTap(user); + } + }), + title: Text(user.name), + subtitle: _buildLastActive(context), + ); + } + + Widget _buildLastActive(context) { + return Text('Last seen ${Jiffy(user.lastActive).fromNow()}'); + } +} diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 5b661ee8..93c21849 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,11 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'stream_chat.dart'; +import 'user_item.dart'; +/// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget); +/// Builder used to create a custom [UserItem] from a [User] +typedef UserItemBuilder = Widget Function(BuildContext, User); + /// /// It shows the list of current users. /// @@ -50,6 +57,7 @@ class UserListView extends StatefulWidget { this.onUserTap, this.onUserLongPress, this.userWidget, + this.userItemBuilder, this.separatorBuilder, this.onImageTap, this.swipeToAction = false, @@ -99,8 +107,8 @@ class UserListView extends StatefulWidget { /// Widget used when opening a channel final Widget userWidget; - // /// Builder used to create a custom channel preview - // final ChannelPreviewBuilder channelPreviewBuilder; + /// Builder used to create a custom user preview + final UserItemBuilder userItemBuilder; /// Builder used to create a custom item separator final Function(BuildContext, int) separatorBuilder; @@ -122,6 +130,21 @@ class _UserListViewState extends State @override void initState() { super.initState(); + final channelsBloc = UsersBloc.of(context); + channelsBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + + _scrollController.addListener(() { + channelsBloc.queryUsersLoading.first.then((loading) { + if (!loading) { + _listenUserPagination(channelsBloc); + } + }); + }); } @override @@ -263,8 +286,8 @@ class _UserListViewState extends State childCount: (users.length * 2) + 1, findChildIndexCallback: (key) { final ValueKey valueKey = key; - final index = users.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + final index = users + .indexWhere((user) => 'USER-${user.id}' == valueKey.value); return index != -1 ? (index * 2) : null; }, ), @@ -306,125 +329,12 @@ class _UserListViewState extends State }; } - return Container( + return UserItem( key: ValueKey('USER-${user.id}'), - child: Builder( - builder: (context) { - Widget child; - child = ListTile( - title: Text(user.name), - ); - // if (widget.channelPreviewBuilder != null) { - // child = Stack( - // children: [ - // widget.channelPreviewBuilder( - // context, - // channel, - // ), - // Positioned.fill( - // child: Material( - // color: Colors.transparent, - // child: InkWell( - // onTap: () { - // onTap(channel, widget.channelWidget); - // }, - // ), - // ), - // ), - // ], - // ); - // } else { - // final backgroundColor = - // Theme.of(context).brightness == Brightness.light - // ? Color(0xffEBEBEB) - // : Color(0xff141414); - // child = Slidable( - // enabled: widget.swipeToAction, - // actionPane: SlidableBehindActionPane(), - // actionExtentRatio: 0.12, - // closeOnScroll: true, - // secondaryActions: [ - // IconSlideAction( - // color: backgroundColor, - // icon: Icons.more_horiz, - // onTap: () { - // showModalBottomSheet( - // clipBehavior: Clip.hardEdge, - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.only( - // topLeft: Radius.circular(32), - // topRight: Radius.circular(32), - // ), - // ), - // context: context, - // builder: (context) { - // return ChannelBottomSheet(channel: channel); - // }, - // ); - // }, - // ), - // IconSlideAction( - // color: backgroundColor, - // icon: StreamIcons.mute, - // onTap: () async { - // if (!channel.isMuted) { - // await channel.mute(); - // } else { - // await channel.unmute(); - // } - // }, - // ), - // if (channel.isGroup && !channel.isDistinct) - // IconSlideAction( - // color: backgroundColor, - // icon: StreamIcons.user_minus, - // onTap: () async { - // final confirm = await showConfirmationDialog( - // context, - // 'Do you want to leave the group?', - // ); - // if (confirm == true) { - // await channel - // .removeMembers([StreamChat.of(context).user.id]); - // } - // }, - // ), - // if (!channel.isGroup && !channel.isDistinct) - // IconSlideAction( - // color: backgroundColor, - // icon: Icons.delete_outline, - // onTap: () async { - // final confirm = await showConfirmationDialog( - // context, - // 'Do you want to delete the chat?', - // ); - // if (confirm == true) { - // await channel - // .removeMembers([StreamChat.of(context).user.id]); - // } - // }, - // ), - // ], - // child: Container( - // color: StreamChatTheme.of(context).backgroundColor, - // child: ChannelPreview( - // onLongPress: widget.onChannelLongPress, - // channel: channel, - // onImageTap: widget.onImageTap != null - // ? () { - // widget.onImageTap(channel); - // } - // : null, - // onTap: (channel) { - // onTap(channel, widget.userWidget); - // }, - // ), - // ), - // ); - // } - return child; - }, - ), + user: user, + onTap: (user) => onTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + onImageTap: widget.onImageTap, ); } else { return _buildQueryProgressIndicator(context, usersProvider); @@ -465,4 +375,37 @@ class _UserListViewState extends State : Colors.black.withOpacity(0.1), ); } + + void _listenUserPagination(UsersBlocState usersProvider) { + if (_scrollController.position.maxScrollExtent == + _scrollController.offset && + _scrollController.offset != 0) { + usersProvider.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination.copyWith( + offset: usersProvider.users?.length ?? 0, + ), + options: widget.options, + ); + } + } + + @override + void didUpdateWidget(UserListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.filter?.toString() != oldWidget.filter?.toString() || + jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || + widget.pagination?.toJson()?.toString() != + oldWidget.pagination?.toJson()?.toString() || + widget.options?.toString() != oldWidget.options?.toString()) { + final channelsBloc = UsersBloc.of(context); + channelsBloc.queryUsers( + filter: widget.filter, + sort: widget.sort, + pagination: widget.pagination, + options: widget.options, + ); + } + } } diff --git a/lib/src/users_bloc.dart b/lib/src/users_bloc.dart index da2221e0..91cbaf80 100644 --- a/lib/src/users_bloc.dart +++ b/lib/src/users_bloc.dart @@ -90,12 +90,6 @@ class UsersBlocState extends State } } - @override - void initState() { - super.initState(); - final client = StreamChat.of(context).client; - } - @override Widget build(BuildContext context) { super.build(context); diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 3c9554fc..663e72a9 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -30,4 +30,5 @@ export 'src/typing_indicator.dart'; export 'src/user_avatar.dart'; export 'src/utils.dart'; export 'src/video_attachment.dart'; +export 'src/users_bloc.dart'; export 'src/user_list_view.dart'; From 9b36c2348ebbd65374c4a49d561609a7045ef70b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 00:09:07 +0530 Subject: [PATCH 049/101] feat: add selection property to user list item --- lib/src/user_item.dart | 27 ++++++++++++++++++++------- lib/src/user_list_view.dart | 6 ++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index 14b488c7..ef442440 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -17,13 +17,14 @@ import 'stream_chat_theme.dart'; /// Modify it to change the widget appearance. class UserItem extends StatelessWidget { /// Instantiate a new UserItem - const UserItem( - {Key key, - @required this.user, - this.onTap, - this.onLongPress, - this.onImageTap}) - : super(key: key); + const UserItem({ + Key key, + @required this.user, + this.onTap, + this.onLongPress, + this.onImageTap, + this.selected = false, + }) : super(key: key); /// Function called when tapping this widget final void Function(User) onTap; @@ -37,6 +38,9 @@ class UserItem extends StatelessWidget { /// The function called when the image is tapped final void Function(User) onImageTap; + /// If true the [UserItem] will show a trailing checkmark + final bool selected; + @override Widget build(BuildContext context) { return ListTile( @@ -58,6 +62,15 @@ class UserItem extends StatelessWidget { onImageTap(user); } }), + trailing: selected + ? CircleAvatar( + child: Icon( + StreamIcons.check, + size: 20, + ), + radius: 10, + ) + : null, title: Text(user.name), subtitle: _buildLastActive(context), ); diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 93c21849..f4487dc0 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -60,6 +60,7 @@ class UserListView extends StatefulWidget { this.userItemBuilder, this.separatorBuilder, this.onImageTap, + this.selectedUsers, this.swipeToAction = false, this.pullToRefresh = true, }) : super(key: key); @@ -119,6 +120,9 @@ class UserListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; + /// Sets a blue trailing checkMark in [UserItem] for all the [selectedUsers] + final List selectedUsers; + @override _UserListViewState createState() => _UserListViewState(); } @@ -309,6 +313,7 @@ class _UserListViewState extends State final usersProvider = UsersBloc.of(context); if (i < users.length) { final user = users[i]; + final selected = widget.selectedUsers?.contains(user) ?? false; UserTapCallback onTap; if (widget.onUserTap != null) { @@ -335,6 +340,7 @@ class _UserListViewState extends State onTap: (user) => onTap(user, widget.userWidget), onLongPress: widget.onUserLongPress, onImageTap: widget.onImageTap, + selected: selected, ); } else { return _buildQueryProgressIndicator(context, usersProvider); From c8001de7ad0c13ac6883ac34628a115fb63e457d Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 00:09:57 +0530 Subject: [PATCH 050/101] [CreateChannelPage] Refactor to use newly build UserListView --- example/lib/main.dart | 93 +++++++++++-------------------------------- 1 file changed, 23 insertions(+), 70 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 5fba1c06..0f84c3a8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -268,12 +268,7 @@ class CreateChannelPage extends StatefulWidget { } class _CreateChannelPageState extends State { - final ScrollController _scrollController = ScrollController(); - Client client; - List users = []; List selectedUsers = []; - int offset = 0; - bool loading = false; @override Widget build(BuildContext context) { @@ -292,31 +287,29 @@ class _CreateChannelPageState extends State { ); } - ListView _buildListView() { - return ListView.builder( - controller: _scrollController, - itemBuilder: _itemBuilder, - itemCount: users.length, - ); - } - - Widget _itemBuilder(context, i) { - final user = users[i]; - return ListTile( - onLongPress: () { - _selectUser(user); - }, - selected: selectedUsers.contains(user), - onTap: () { - if (selectedUsers.isNotEmpty) { - return _selectUser(user); - } - _createChannel(context, [user]); - }, - leading: UserAvatar( - user: user, + Widget _buildListView() { + return UsersBloc( + child: UserListView( + pagination: PaginationParams( + limit: 25, + ), + sort: [ + SortOption( + 'name', + direction: SortOption.ASC, + ), + ], + onUserLongPress: (user) { + _selectUser(user); + }, + selectedUsers: selectedUsers, + onUserTap: (user, _) { + if (selectedUsers.isNotEmpty) { + return _selectUser(user); + } + _createChannel(context, [user]); + }, ), - title: Text(user.name), ); } @@ -373,6 +366,7 @@ class _CreateChannelPageState extends State { List users, [ String name, ]) async { + final client = StreamChat.of(context).client; final channel = client.channel('messaging', extraData: { 'members': [ client.state.user.id, @@ -405,47 +399,6 @@ class _CreateChannelPageState extends State { }); } } - - @override - void initState() { - super.initState(); - - client = StreamChat.of(context).client; - - _scrollController.addListener(() async { - if (!loading && - _scrollController.offset >= - _scrollController.position.maxScrollExtent - 100) { - offset += 25; - await _queryUsers(); - } - }); - - _queryUsers(); - } - - Future _queryUsers() { - loading = true; - return client.queryUsers( - pagination: PaginationParams( - limit: 25, - offset: offset, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - ).then((value) { - setState(() { - users = [ - ...users, - ...value.users, - ]; - }); - }).whenComplete(() => loading = false); - } } class NewChatScreen extends StatefulWidget { From b115cd254c377981aff56737374b5b8991838493 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 12:56:46 +0530 Subject: [PATCH 051/101] feat : Add alphabetically grouping flag to User list --- lib/src/user_list_view.dart | 166 +++++++++++++++++++++++++++--------- 1 file changed, 125 insertions(+), 41 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index f4487dc0..56cb762d 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'stream_chat.dart'; import 'user_item.dart'; @@ -10,8 +11,8 @@ import 'user_item.dart'; /// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget); -/// Builder used to create a custom [UserItem] from a [User] -typedef UserItemBuilder = Widget Function(BuildContext, User); +/// Builder used to create a custom [ListUserItem] from a [User] +typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// /// It shows the list of current users. @@ -63,6 +64,7 @@ class UserListView extends StatefulWidget { this.selectedUsers, this.swipeToAction = false, this.pullToRefresh = true, + this.groupAlphabetically = false, }) : super(key: key); /// The builder that will be used in case of error @@ -120,9 +122,14 @@ class UserListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; - /// Sets a blue trailing checkMark in [UserItem] for all the [selectedUsers] + /// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers] final List selectedUsers; + /// Set it to true to group users by their first character + /// + /// defaults to false + final bool groupAlphabetically; + @override _UserListViewState createState() => _UserListViewState(); } @@ -172,11 +179,30 @@ class _UserListViewState extends State ); } - StreamBuilder> _buildListView( + StreamBuilder> _buildListView( UsersBlocState usersBlocState, ) { return StreamBuilder( - stream: usersBlocState.usersStream, + stream: usersBlocState.usersStream.map( + (users) { + if (widget.groupAlphabetically) { + final temp = users + ..sort((curr, next) => curr.name.compareTo(next.name)); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; + } + final items = []; + for (var key in groupedUsers.keys) { + items.add(ListHeaderItem(key)); + items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); + } + return items; + } + return users.map((e) => ListUserItem(e)).toList(); + }, + ), builder: (context, snapshot) { if (snapshot.hasError) { if (snapshot.error is Error) { @@ -256,13 +282,13 @@ class _UserListViewState extends State ); } - final users = snapshot.data; + final items = snapshot.data; - if (users.isEmpty && widget.emptyBuilder != null) { + if (items.isEmpty && widget.emptyBuilder != null) { return widget.emptyBuilder(context); } - if (users.isEmpty && widget.emptyBuilder == null) { + if (items.isEmpty && widget.emptyBuilder == null) { return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( @@ -285,13 +311,13 @@ class _UserListViewState extends State controller: _scrollController, childrenDelegate: SliverChildBuilderDelegate( (context, i) { - return _itemBuilder(context, i, users); + return _itemBuilder(context, i, items); }, - childCount: (users.length * 2) + 1, + childCount: (items.length * 2) + 1, findChildIndexCallback: (key) { final ValueKey valueKey = key; - final index = users - .indexWhere((user) => 'USER-${user.id}' == valueKey.value); + final index = + items.indexWhere((item) => item.key == valueKey.value); return index != -1 ? (index * 2) : null; }, ), @@ -300,7 +326,7 @@ class _UserListViewState extends State ); } - Widget _itemBuilder(context, int i, List users) { + Widget _itemBuilder(BuildContext context, int i, List items) { if (i % 2 != 0) { if (widget.separatorBuilder != null) { return widget.separatorBuilder(context, i); @@ -311,36 +337,57 @@ class _UserListViewState extends State i = i ~/ 2; final usersProvider = UsersBloc.of(context); - if (i < users.length) { - final user = users[i]; - final selected = widget.selectedUsers?.contains(user) ?? false; + if (i < items.length) { + final item = items[i]; + return item.when( + headerItem: (header) { + return Container( + key: ValueKey('HEADER-$header'), + color: Colors.grey.shade100, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + header, + style: TextStyle(fontWeight: FontWeight.w500), + ), + ), + ); + }, + userItem: (user) { + final selected = widget.selectedUsers?.contains(user) ?? false; - UserTapCallback onTap; - if (widget.onUserTap != null) { - onTap = widget.onUserTap; - } else { - onTap = (client, _) { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) { - // return StreamChannel( - // child: widget.userWidget, - // channel: client, - // ); - // }, - // ), - // ); - }; - } + UserTapCallback onTap; + if (widget.onUserTap != null) { + onTap = widget.onUserTap; + } else { + onTap = (client, _) { + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) { + // return StreamChannel( + // child: widget.userWidget, + // channel: client, + // ); + // }, + // ), + // ); + }; + } - return UserItem( - key: ValueKey('USER-${user.id}'), - user: user, - onTap: (user) => onTap(user, widget.userWidget), - onLongPress: widget.onUserLongPress, - onImageTap: widget.onImageTap, - selected: selected, + return Container( + key: ValueKey('USER-${user.id}'), + child: widget.userItemBuilder != null + ? widget.userItemBuilder(context, user, selected) + : UserItem( + user: user, + onTap: (user) => onTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + onImageTap: widget.onImageTap, + selected: selected, + ), + ); + }, ); } else { return _buildQueryProgressIndicator(context, usersProvider); @@ -415,3 +462,40 @@ class _UserListViewState extends State } } } + +abstract class ListItem { + String get key { + if (this is ListHeaderItem) { + final header = (this as ListHeaderItem).heading; + return 'HEADER-$header'; + } + if (this is ListUserItem) { + final user = (this as ListUserItem).user; + return 'USER-${user.id}'; + } + } + + Widget when({ + @required Widget Function(String heading) headerItem, + @required Widget Function(User user) userItem, + }) { + if (this is ListHeaderItem) { + return headerItem((this as ListHeaderItem).heading); + } + if (this is ListUserItem) { + return userItem((this as ListUserItem).user); + } + } +} + +class ListHeaderItem extends ListItem { + final String heading; + + ListHeaderItem(this.heading); +} + +class ListUserItem extends ListItem { + final User user; + + ListUserItem(this.user); +} From 4561057452f76fb8ced3af7c68d78ab6d6302b9a Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 13:30:03 +0530 Subject: [PATCH 052/101] feat : Add support for userName filtering in user list --- lib/src/user_list_view.dart | 74 ++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 56cb762d..57abcc99 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -65,7 +66,13 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - }) : super(key: key); + this.filterByUserName = '', + this.filterByUserNameStream, + }) : assert( + filterByUserName == null || filterByUserNameStream == null, + 'Cannot provide both filterByUserName and filterByUserNameStream.', + ), + super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -130,6 +137,12 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + /// + final String filterByUserName; + + /// + final Stream filterByUserNameStream; + @override _UserListViewState createState() => _UserListViewState(); } @@ -179,30 +192,49 @@ class _UserListViewState extends State ); } + List _getFilteredItems(List users, String query) { + if (widget.groupAlphabetically) { + var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); + temp = temp + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; + } + final items = []; + for (var key in groupedUsers.keys) { + items.add(ListHeaderItem(key)); + items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); + } + return items; + } + return users + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) + .map((e) => ListUserItem(e)) + .toList(); + } + + Stream> _buildUserStream( + UsersBlocState usersBlocState, + ) { + if (widget.filterByUserNameStream == null) { + return usersBlocState.usersStream.map( + (users) => _getFilteredItems(users, widget.filterByUserName), + ); + } + return Rx.combineLatest2( + usersBlocState.usersStream, + widget.filterByUserNameStream, + _getFilteredItems, + ); + } + StreamBuilder> _buildListView( UsersBlocState usersBlocState, ) { return StreamBuilder( - stream: usersBlocState.usersStream.map( - (users) { - if (widget.groupAlphabetically) { - final temp = users - ..sort((curr, next) => curr.name.compareTo(next.name)); - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]; - groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; - } - final items = []; - for (var key in groupedUsers.keys) { - items.add(ListHeaderItem(key)); - items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); - } - return items; - } - return users.map((e) => ListUserItem(e)).toList(); - }, - ), + stream: _buildUserStream(usersBlocState), builder: (context, snapshot) { if (snapshot.hasError) { if (snapshot.error is Error) { From eda42df7da5c786dbe9c1f162922d96cb2c2cb8c Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 13:38:08 +0530 Subject: [PATCH 053/101] fix : whereIterable to List --- lib/src/user_list_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 57abcc99..1c3652ff 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -196,7 +196,8 @@ class _UserListViewState extends State if (widget.groupAlphabetically) { var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); temp = temp - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())); + .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) + .toList(); final groupedUsers = >{}; for (var e in temp) { final alphabet = e.name[0]; From 150a8b00a6b2ce759a72c44b404ee0fcee943a0e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 09:35:23 +0100 Subject: [PATCH 054/101] Update README.md update readme adding file picker troubleshooting link --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 7ca34145..ba5de4c1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,11 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin. Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements. +### Troubleshooting + +It may happen that you have some problems building the app. +If it seems related to the [flutter file picker plugin](https://github.com/miguelpruivo/flutter_file_picker) make sure to check [this page](https://github.com/miguelpruivo/flutter_file_picker/wiki/Troubleshooting) + ## Docs ### Business logic components From d379de9f9672cba83f34e0a365da2994951d1c61 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 10:49:35 +0100 Subject: [PATCH 055/101] minor fixes --- example/ios/Flutter/.last_build_id | 2 +- example/lib/choose_user_page.dart | 207 ++++++++-------- example/lib/main.dart | 185 +++++++------- example/pubspec.yaml | 2 +- lib/src/channel_list_view.dart | 86 ++++++- lib/src/giphy_attachment.dart | 381 ++++++++++++++--------------- lib/src/message_widget.dart | 1 + 7 files changed, 471 insertions(+), 393 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 8aba7787..20c7c514 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -bb5f9103d9045cd6244bcae1f5f343e5 \ No newline at end of file +c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 30205088..4583a2a6 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -94,127 +94,130 @@ class ChooseUserPage extends StatelessWidget { ), ), 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(), + child: Padding( + padding: const EdgeInsets.only(top: 32), + 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(), diff --git a/example/lib/main.dart b/example/lib/main.dart index c09030f3..d0b39d98 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -21,7 +21,7 @@ void main() async { logLevel: Level.INFO, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, - persistenceEnabled: false, + persistenceEnabled: true, ); if (userId != null) { @@ -43,6 +43,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), //TODO change to system once dark theme is implemented @@ -63,89 +64,9 @@ class ChannelListPage extends StatelessWidget { Widget build(BuildContext context) { final user = StreamChat.of(context).user; return Scaffold( - drawer: Drawer( - 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.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(); - - 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, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), + drawerEnableOpenDragGesture: true, + drawerEdgeDragWidth: 50, + drawer: _buildDrawer(context, user), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { @@ -156,6 +77,11 @@ class ChannelListPage extends StatelessWidget { ), body: ChannelsBloc( child: ChannelListView( + onStartChatPressed: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) { + return CreateChannelPage(); + })); + }, swipeToAction: true, filter: { 'members': { @@ -174,6 +100,92 @@ class ChannelListPage extends StatelessWidget { ), ); } + + Drawer _buildDrawer(BuildContext context, User user) { + return Drawer( + 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.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(); + + 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, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } } class ChannelPage extends StatelessWidget { @@ -418,6 +430,11 @@ class _CreateChannelPageState extends State { limit: 25, offset: offset, ), + filter: { + 'id': { + r'$ne': client.state.user.id, + } + }, sort: [ SortOption( 'name', diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fca549d2..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.60+62 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 8b66db34..9dae4213 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -70,6 +70,7 @@ class ChannelListView extends StatefulWidget { this.errorBuilder, this.emptyBuilder, this.onImageTap, + this.onStartChatPressed, this.swipeToAction = false, this.pullToRefresh = true, }) : super(key: key); @@ -129,6 +130,9 @@ class ChannelListView extends StatefulWidget { /// Set it to false to disable the pull-to-refresh widget final bool pullToRefresh; + /// Callback used in the default empty list widget + final VoidCallback onStartChatPressed; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -185,13 +189,69 @@ class _ChannelListViewState extends State builder: (context, viewportConstraints) { return SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Text('You have no channels currently'), - ), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + StreamIcons.message, + size: 136, + color: Color(0xffDBDBDB), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Let’s start chatting!', + style: TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 52, + ), + child: Text( + 'How about sending your first message to a friend?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Color(0xff7A7A7A), + ), + ), + ), + ], + ), + ), + if (widget.onStartChatPressed != null) + Positioned( + right: 0, + left: 0, + bottom: 32, + child: Center( + child: FlatButton( + onPressed: widget.onStartChatPressed, + child: Text( + 'Start a chat', + style: TextStyle( + color: + StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], ), ); }, @@ -542,11 +602,13 @@ class _ChannelListViewState extends State ), ); } - return snapshot.data - ? _buildLoadingItem() - : Container( - height: 70, - ); + return Container( + height: 100, + padding: EdgeInsets.all(32), + child: Center( + child: snapshot.data ? CircularProgressIndicator() : Container(), + ), + ); }); } diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index 8cec9ce5..df4708c5 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -41,211 +41,206 @@ class GiphyAttachment extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - DecoratedBox( - decoration: BoxDecoration(), - child: Card( - elevation: 2, - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16.0), - bottomRight: Radius.circular(0.0), - topLeft: Radius.circular(16.0), - bottomLeft: Radius.circular(16.0), - ), + Card( + elevation: 2, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(16.0), + bottomRight: Radius.circular(0.0), + topLeft: Radius.circular(16.0), + bottomLeft: Radius.circular(16.0), ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Stack( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: GestureDetector( - onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (_) { - return FullScreenImage( - url: attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl, - ); - })); - }, - child: CachedNetworkImage( - height: size?.height, - width: size?.width, - placeholder: (_, __) { - return Container( - width: size?.width, - height: size?.height, - child: Center( - child: CircularProgressIndicator(), - ), - ); - }, - imageUrl: attachment.thumbUrl ?? - attachment.imageUrl ?? - attachment.assetUrl, - errorWidget: (context, url, error) => AttachmentError( - attachment: attachment, - size: size, - ), - fit: BoxFit.cover, - ), - ), - ), - Positioned( - left: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(16.0), - )), - child: Padding( - padding: const EdgeInsets.only( - left: 8.0, - right: 8.0, - top: 8.0, - bottom: 4.0, - ), - child: Row( - children: [ - Icon( - StreamIcons.lightning, - color: StreamChatTheme.of(context).accentColor, - size: 16.0, - ), - Text( - 'GIPHY', - style: TextStyle( - color: - StreamChatTheme.of(context).accentColor, - fontWeight: FontWeight.bold, - fontSize: 11.0, - ), - ), - ], - ), - ), - ), - ), - ], - ), - if (attachment.title != null) - Container( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Card( - elevation: 2, - child: IconButton( - padding: const EdgeInsets.all(0), - constraints: BoxConstraints.tight(Size(32, 32)), - icon: Icon( - StreamIcons.left, - size: 24.0, - ), - splashRadius: 16, - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'shuffle', - }); - }, - ), - shape: CircleBorder(), - ), - Expanded( + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Stack( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) { + return FullScreenImage( + url: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl, + ); + })); + }, + child: CachedNetworkImage( + height: size?.height, + width: size?.width, + placeholder: (_, __) { + return Container( + width: size?.width, + height: size?.height, child: Center( - child: Text( - '"${attachment.title}"', - style: TextStyle( - fontStyle: FontStyle.italic, - ), - ), + child: CircularProgressIndicator(), ), - ), - Card( - elevation: 2, - child: IconButton( - padding: const EdgeInsets.all(0), - constraints: BoxConstraints.tight(Size(32, 32)), - icon: Icon( - StreamIcons.right, - size: 24.0, - ), - splashRadius: 16, - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'shuffle', - }); - }, - ), - shape: CircleBorder(), - ), - ], + ); + }, + imageUrl: attachment.thumbUrl ?? + attachment.imageUrl ?? + attachment.assetUrl, + errorWidget: (context, url, error) => AttachmentError( + attachment: attachment, + size: size, + ), + fit: BoxFit.cover, ), ), ), - SizedBox( - height: 4.0, - ), - Container( - color: Colors.black.withOpacity(0.2), - width: double.infinity, - height: 0.5, - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: FlatButton( - height: 50, - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'cancel', - }); - }, - child: Text( - 'Cancel', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.black.withOpacity(0.5)), + Positioned( + left: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomRight: Radius.circular(16.0), + )), + child: Padding( + padding: const EdgeInsets.only( + left: 8.0, + right: 8.0, + top: 8.0, + bottom: 4.0, ), - ), - ), - Container( - width: 0.5, - color: Colors.black.withOpacity(0.2), - height: 50.0, - ), - Expanded( - child: FlatButton( - height: 50, - onPressed: () { - streamChannel.channel.sendAction(message, { - 'image_action': 'send', - }); - }, - child: Text( - 'Send', - style: TextStyle( + child: Row( + children: [ + Icon( + StreamIcons.lightning, color: StreamChatTheme.of(context).accentColor, - fontWeight: FontWeight.bold), + size: 16.0, + ), + Text( + 'GIPHY', + style: TextStyle( + color: StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold, + fontSize: 11.0, + ), + ), + ], ), ), ), - ], + ), + ], + ), + if (attachment.title != null) + Container( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Card( + elevation: 2, + child: IconButton( + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tight(Size(32, 32)), + icon: Icon( + StreamIcons.left, + size: 24.0, + ), + splashRadius: 16, + onPressed: () { + streamChannel.channel.sendAction(message, { + 'image_action': 'shuffle', + }); + }, + ), + shape: CircleBorder(), + ), + Expanded( + child: Center( + child: Text( + '"${attachment.title}"', + style: TextStyle( + fontStyle: FontStyle.italic, + ), + ), + ), + ), + Card( + elevation: 2, + child: IconButton( + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tight(Size(32, 32)), + icon: Icon( + StreamIcons.right, + size: 24.0, + ), + splashRadius: 16, + onPressed: () { + streamChannel.channel.sendAction(message, { + 'image_action': 'shuffle', + }); + }, + ), + shape: CircleBorder(), + ), + ], + ), + ), ), - ], - ), + SizedBox( + height: 4.0, + ), + Container( + color: Colors.black.withOpacity(0.2), + width: double.infinity, + height: 0.5, + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: FlatButton( + height: 50, + onPressed: () { + streamChannel.channel.sendAction(message, { + 'image_action': 'cancel', + }); + }, + child: Text( + 'Cancel', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.5)), + ), + ), + ), + Container( + width: 0.5, + color: Colors.black.withOpacity(0.2), + height: 50.0, + ), + Expanded( + child: FlatButton( + height: 50, + onPressed: () { + streamChannel.channel.sendAction(message, { + 'image_action': 'send', + }); + }, + child: Text( + 'Send', + style: TextStyle( + color: StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ], ), ), SizedBox( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 4f551655..86479ebe 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -619,6 +619,7 @@ class _MessageWidgetState extends State { child: Material( clipBehavior: Clip.hardEdge, shape: attachmentShape, + type: MaterialType.transparency, child: Transform( transform: Matrix4.rotationY(widget.reverse ? pi : 0), alignment: Alignment.center, From e021ea5cd675dc263072eabb675d178457973d9f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 12:06:26 +0100 Subject: [PATCH 056/101] update flutter action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8e87c58..4b3e086a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Flutter action - uses: subosito/flutter-action@v1.3.2 + uses: subosito/flutter-action@v1.4.0 with: channel: 'stable' - name: Get dependencies From 1742fe6f5fa6b39af7caf24226f3e6a4cbe1f3ea Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 17:16:18 +0530 Subject: [PATCH 057/101] feat: Replace List with Set for easier computation. --- lib/src/user_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 1c3652ff..c4e3f2ce 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -130,7 +130,7 @@ class UserListView extends StatefulWidget { final bool pullToRefresh; /// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers] - final List selectedUsers; + final Set selectedUsers; /// Set it to true to group users by their first character /// From 7cf30b501f123914a531ab3d55a8f5d52de8d332 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 13:32:18 +0100 Subject: [PATCH 058/101] fix qa --- example/ios/Flutter/.last_build_id | 2 +- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 99 ++++++++++++++---------------- lib/src/video_thumbnail.dart | 37 +++++++++++ svgs/icon_camera.svg | 3 + 5 files changed, 87 insertions(+), 56 deletions(-) create mode 100644 lib/src/video_thumbnail.dart create mode 100644 svgs/icon_camera.svg diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 8aba7787..20c7c514 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -bb5f9103d9045cd6244bcae1f5f343e5 \ No newline at end of file +c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8fc2dadb..fca549d2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.59+61 +version: 1.0.60+62 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6024169b..dc5bf2f6 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -19,6 +19,7 @@ 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:stream_chat_flutter/src/video_thumbnail.dart'; import 'package:substring_highlight/substring_highlight.dart'; import '../stream_chat_flutter.dart'; @@ -41,6 +42,8 @@ enum DefaultAttachmentTypes { file, } +const _kMinMediaPickerSize = 360.0; + /// Inactive state /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_input_paint.png) @@ -182,7 +185,7 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; - double _filePickerSize = 250.0; + double _filePickerSize = _kMinMediaPickerSize; /// The editing controller passed to the input TextField TextEditingController textEditingController; @@ -654,9 +657,11 @@ class MessageInputState extends State { }, ), IconButton( - icon: Icon( - StreamIcons.camera, - size: 24, + icon: SvgPicture.asset( + 'svgs/icon_camera.svg', + package: 'stream_chat_flutter', + height: 24, + width: 24, color: _filePickerIndex == 2 ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.5), @@ -684,7 +689,7 @@ class MessageInputState extends State { setState(() { _animateContainer = false; _filePickerSize = (_filePickerSize - update.delta.dy).clamp( - 240.0, + _kMinMediaPickerSize, MediaQuery.of(context).size.height / 1.7, ); }); @@ -753,9 +758,10 @@ class MessageInputState extends State { .any((element) => element.id == media.id)) { _addAttachment(media); } else { - _attachments - .removeWhere((element) => element.id == media.id); - setState(() {}); + setState(() { + _attachments + .removeWhere((element) => element.id == media.id); + }); } }, ); @@ -813,35 +819,7 @@ 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; - }); - - if (file == null) { - return; - } - - 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', - ), id: medium.id, ); @@ -849,11 +827,23 @@ class MessageInputState extends State { _attachments.add(attachment); }); - final thumbUrl = await _uploadImage( - thumbFile, - channel, + final mediaFile = await medium.getFile(); + + final file = PlatformFile( + path: mediaFile.path, + bytes: mediaFile.readAsBytesSync(), ); + final channel = StreamChannel.of(context).channel; + setState(() { + attachment + ..file = file + ..attachment = Attachment( + localUri: file.path != null ? Uri.parse(file.path) : null, + type: medium.mediaType == MediaType.image ? 'image' : 'video', + ); + }); + final url = await _uploadAttachment( file, medium.mediaType == MediaType.image @@ -868,12 +858,10 @@ 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, ); } @@ -1221,6 +1209,10 @@ class MessageInputState extends State { ); } + if (attachment.attachment == null) { + return SizedBox(); + } + switch (attachment.attachment.type) { case 'image': case 'giphy': @@ -1230,22 +1222,20 @@ class MessageInputState extends State { fit: BoxFit.cover, ) : Image.network( - attachment.attachment.imageUrl ?? - attachment.attachment.thumbUrl, + attachment.attachment.imageUrl, fit: BoxFit.cover, ); break; case 'video': return Stack( children: [ - Container( - child: attachment.thumbFile != null - ? Image.memory( - attachment.thumbFile.bytes, - fit: BoxFit.cover, - ) - : Icon(Icons.videocam), - color: Colors.black26, + Positioned.fill( + child: Container( + child: VideoThumbnail( + file: File( + attachment.file.path, + )), + ), ), Positioned( left: 8, @@ -1314,7 +1304,7 @@ class MessageInputState extends State { setState(() { _animateContainer = true; _openFilePickerSection = false; - _filePickerSize = 250.0; + _filePickerSize = _kMinMediaPickerSize; }); } else { final status = await (Platform.isAndroid @@ -1450,6 +1440,9 @@ class MessageInputState extends State { } else if (fileType == DefaultAttachmentTypes.video) { pickedFile = await _imagePicker.getVideo(source: ImageSource.camera); } + if (pickedFile == null) { + return; + } final bytes = await pickedFile.readAsBytes(); file = PlatformFile( path: pickedFile.path, @@ -1774,14 +1767,12 @@ 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/video_thumbnail.dart b/lib/src/video_thumbnail.dart new file mode 100644 index 00000000..c36ab327 --- /dev/null +++ b/lib/src/video_thumbnail.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoThumbnail extends StatefulWidget { + final File file; + + const VideoThumbnail({ + Key key, + @required this.file, + }) : super(key: key); + + @override + _VideoThumbnailState createState() => _VideoThumbnailState(); +} + +class _VideoThumbnailState extends State { + VideoPlayerController _videoPlayerController; + @override + Widget build(BuildContext context) { + return VideoPlayer(_videoPlayerController); + } + + @override + void initState() { + _videoPlayerController = VideoPlayerController.file(widget.file) + ..initialize(); + super.initState(); + } + + @override + void dispose() { + _videoPlayerController.dispose(); + super.dispose(); + } +} diff --git a/svgs/icon_camera.svg b/svgs/icon_camera.svg new file mode 100644 index 00000000..0bf3122d --- /dev/null +++ b/svgs/icon_camera.svg @@ -0,0 +1,3 @@ + + + From 6411afdee00728dd01042cded5fa2ab418de89ad Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Wed, 18 Nov 2020 20:15:34 +0530 Subject: [PATCH 059/101] temp push --- example/lib/chips_input_text_field.dart | 142 ++++++++++++++++++++++++ example/lib/main.dart | 114 +++++++++++++++++-- 2 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 example/lib/chips_input_text_field.dart diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart new file mode 100644 index 00000000..e62773f7 --- /dev/null +++ b/example/lib/chips_input_text_field.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_icons.dart'; + +typedef ChipBuilder = Widget Function(BuildContext context, T chip); +typedef OnChipAdded = void Function(T chip); +typedef OnChipRemoved = void Function(T chip); + +class ChipsInputTextField extends StatefulWidget { + final TextEditingController controller; + final FocusNode focusNode; + final ValueChanged onInputChanged; + final ChipBuilder chipBuilder; + final OnChipAdded onChipAdded; + final OnChipRemoved onChipRemoved; + final String hint; + + const ChipsInputTextField({ + Key key, + @required this.chipBuilder, + @required this.controller, + this.onInputChanged, + this.focusNode, + this.onChipAdded, + this.onChipRemoved, + this.hint = 'Type a name or group', + }) : super(key: key); + + @override + ChipInputTextFieldState createState() => ChipInputTextFieldState(); +} + +class ChipInputTextFieldState extends State> { + final _chips = {}; + bool _pauseItemAddition = false; + + void addItem(T item) { + if (!_pauseItemAddition) { + setState(() => _chips.add(item)); + if (widget.onChipAdded != null) widget.onChipAdded(item); + } + } + + void removeItem(T item) { + setState(() { + _chips.remove(item); + if (_chips.isEmpty) resumeItemAddition(); + }); + if (widget.focusNode != null) widget.focusNode.requestFocus(); + if (widget.onChipRemoved != null) widget.onChipRemoved(item); + } + + void pauseItemAddition() { + if (!_pauseItemAddition) { + setState(() => _pauseItemAddition = true); + } + } + + void resumeItemAddition() { + if (_pauseItemAddition) { + setState(() => _pauseItemAddition = false); + } + } + + @override + Widget build(BuildContext context) { + return Material( + elevation: 4, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16.0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 4.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) ...[ + if (_chips.isNotEmpty) SizedBox(height: 4), + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + ), + hintText: widget.hint, + hintStyle: TextStyle(fontSize: 18)), + ), + ] + ], + ), + ), + SizedBox(width: 12), + IconButton( + icon: Icon( + _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + ), + onPressed: () { + resumeItemAddition(); + }, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 0f84c3a8..fa1c14a6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'chips_input_text_field.dart'; import 'notifications_service.dart'; @@ -268,7 +269,7 @@ class CreateChannelPage extends StatefulWidget { } class _CreateChannelPageState extends State { - List selectedUsers = []; + final selectedUsers = {}; @override Widget build(BuildContext context) { @@ -282,7 +283,7 @@ class _CreateChannelPageState extends State { ), ), floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(), + selectedUsers.isNotEmpty ? _buildFAB(context) : null, body: _buildListView(), ); } @@ -325,7 +326,7 @@ class _CreateChannelPageState extends State { } } - _createChannel(context, selectedUsers, name); + _createChannel(context, selectedUsers.toList(), name); }, ); } @@ -407,10 +408,44 @@ class NewChatScreen extends StatefulWidget { } class _NewChatScreenState extends State { + final _chipInputTextFieldStateKey = + GlobalKey>(); + + TextEditingController _controller; + + ChipInputTextFieldState get _chipInputTextFieldState => + _chipInputTextFieldStateKey.currentState; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController() + ..addListener(() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 0, backgroundColor: Colors.white, title: Text( 'New Chat', @@ -418,14 +453,71 @@ class _NewChatScreenState extends State { ), ), body: UsersBloc( - child: UserListView( - pagination: PaginationParams( - limit: 25, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: FocusNode(), + chipBuilder: (context, user) { + return InputChip( + key: ObjectKey(user), + label: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + avatar: UserAvatar( + user: user, + ), + onDeleted: () => _chipInputTextFieldState.removeItem(user), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + Container( + width: double.maxFinite, + color: Colors.white54, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + _controller.clear(); + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + MessageInput( ), ], ), From 6c4bcd2073a5f52473d26a3304aa29ac6b3e1fa4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Nov 2020 18:06:27 +0100 Subject: [PATCH 060/101] fix messageinput --- example/lib/main.dart | 149 ++++++++++++++++++++---------------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 20 ++--- lib/src/stream_channel.dart | 4 +- pubspec.yaml | 2 +- 5 files changed, 98 insertions(+), 79 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index fa1c14a6..c4917e51 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,8 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'chips_input_text_field.dart'; +import 'chips_input_text_field.dart'; import 'notifications_service.dart'; void main() async { @@ -422,9 +422,12 @@ class _NewChatScreenState extends State { bool _isSearchActive = false; + Channel channel; + @override void initState() { super.initState(); + channel = StreamChat.of(context).client.channel('messaging'); _controller = TextEditingController() ..addListener(() { setState(() { @@ -452,74 +455,88 @@ class _NewChatScreenState extends State { style: TextStyle(color: Colors.black), ), ), - body: UsersBloc( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: FocusNode(), - chipBuilder: (context, user) { - return InputChip( - key: ObjectKey(user), - label: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - avatar: UserAvatar( - user: user, - ), - onDeleted: () => _chipInputTextFieldState.removeItem(user), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - Container( - width: double.maxFinite, - color: Colors.white54, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - child: UserListView( - filterByUserName: _userNameQuery, - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - if (!_selectedUsers.contains(user)) { - _controller.clear(); - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } + body: StreamChannel( + showLoading: false, + channel: channel, + child: UsersBloc( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: FocusNode(), + chipBuilder: (context, user) { + return InputChip( + key: ObjectKey(user), + label: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + avatar: UserAvatar( + user: user, + ), + onDeleted: () => _chipInputTextFieldState.removeItem(user), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); }, - pagination: PaginationParams( - limit: 25, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + Container( + width: double.maxFinite, + color: Colors.white54, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), ), ), - ), - MessageInput( - ), - ], + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + _controller.clear(); + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + MessageInput( + preMessageSending: (message) async { + channel.extraData = { + 'members': [ + ..._selectedUsers.map((e) => e.id), + channel.client.state.user.id, + ], + }; + await channel.watch(); + return message; + }, + ), + ], + ), ), ), ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8fc2dadb..fca549d2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.59+61 +version: 1.0.60+62 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 6024169b..6f89db84 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -364,7 +364,10 @@ class MessageInputState extends State { controller: textEditingController, focusNode: _focusNode, onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); + StreamChannel.of(context) + .channel + .keyStroke() + .catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -479,7 +482,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (textEditingController.selection.isCollapsed && - (s.isNotEmpty && s[textEditingController.selection.start - 1] == '@' || + (s.isNotEmpty && + textEditingController.selection.start > 0 && + s[textEditingController.selection.start - 1] == '@' || textEditingController.text .substring(0, textEditingController.selection.start) .split(' ') @@ -1569,14 +1574,9 @@ class MessageInputState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Center( - child: InkWell( - onTap: () { - sendMessage(); - }, - child: Icon( - _getIdleSendIcon(), - color: Colors.grey, - ), + child: Icon( + _getIdleSendIcon(), + color: Colors.grey, )), ), ); diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index ce19b261..6fafa070 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -12,12 +12,14 @@ class StreamChannel extends StatefulWidget { Key key, @required this.child, @required this.channel, + this.showLoading = true, }) : super( key: key, ); final Widget child; final Channel channel; + final bool showLoading; /// Use this method to get the current [StreamChannelState] instance static StreamChannelState of(BuildContext context) { @@ -155,7 +157,7 @@ class StreamChannelState extends State { future: widget.channel.initialized, initialData: widget.channel.state != null, builder: (context, snapshot) { - if (!snapshot.hasData || !snapshot.data) { + if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) { return Container( height: 30, child: Center( diff --git a/pubspec.yaml b/pubspec.yaml index 9c1b0a29..7e7fe36b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.13 + stream_chat: ^0.2.13+1 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 From 7733cc74591904968e92968338ada29e580c0d0d Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 12:11:57 +0530 Subject: [PATCH 061/101] [NewChatScreen] Navigate to ChannelPage on successful messageSent --- example/lib/main.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index c4917e51..0be01886 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -534,6 +534,19 @@ class _NewChatScreenState extends State { await channel.watch(); return message; }, + onMessageSent: (_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + }, ), ], ), From 83d68c4baf8c60ebf5c52ce85de9cb0ee1424257 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 13:14:08 +0530 Subject: [PATCH 062/101] [NewChatScreen] Add create group button, Finishing touches --- example/lib/chips_input_text_field.dart | 2 +- example/lib/main.dart | 28 ++++++++++++- example/lib/neumorphic_button.dart | 52 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 example/lib/neumorphic_button.dart diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e62773f7..ef8229c8 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,7 +64,7 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 4, + elevation: 2, color: Colors.white, child: Container( child: Padding( diff --git a/example/lib/main.dart b/example/lib/main.dart index 0be01886..f681129d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; import 'notifications_service.dart'; +import 'neumorphic_button.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -487,9 +488,34 @@ class _NewChatScreenState extends State { setState(() => _selectedUsers.remove(user)); }, ), + if (!_isSearchActive) + Container( + color: Colors.white54, + child: InkWell( + onTap: () {}, + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Colors.blue.shade700, + ), + ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ], + ), + ), + ), Container( width: double.maxFinite, - color: Colors.white54, + color: Colors.grey.shade50, child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, diff --git a/example/lib/neumorphic_button.dart b/example/lib/neumorphic_button.dart new file mode 100644 index 00000000..c738cecd --- /dev/null +++ b/example/lib/neumorphic_button.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +extension ColorUtils on Color { + Color mix(Color another, double amount) { + return Color.lerp(this, another, amount); + } +} + +class NeumorphicButton extends StatelessWidget { + final Widget child; + final Color backgroundColor; + final EdgeInsets margin; + final EdgeInsets padding; + + const NeumorphicButton({ + Key key, + @required this.child, + this.backgroundColor = Colors.white, + this.margin = const EdgeInsets.all(8), + this.padding = const EdgeInsets.all(14), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + child: child, + margin: margin, + padding: padding, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundColor, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + backgroundColor.mix(Colors.white, 0.2), + backgroundColor.mix(Colors.black, 0.1), + ]), + boxShadow: [ + BoxShadow( + blurRadius: 1, + color: backgroundColor.mix(Colors.white, 0.6), + ), + BoxShadow( + blurRadius: 1, + color: backgroundColor.mix(Colors.black, 0.3), + ) + ], + ), + ); + } +} From 1a21dc27a2f080404857aef69699dbf6233fc32f Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 13:20:55 +0530 Subject: [PATCH 063/101] [Drawer,NewChatScreen] Navigate to NewGroupChatScreen on "New Group" button press. --- example/lib/chips_input_text_field.dart | 2 +- example/lib/main.dart | 33 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index ef8229c8..e62773f7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,7 +64,7 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 2, + elevation: 4, color: Colors.white, child: Container( child: Padding( diff --git a/example/lib/main.dart b/example/lib/main.dart index f681129d..82fee6b2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -121,6 +121,12 @@ class ChannelListPage extends StatelessWidget { ), ), ListTile( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, leading: Icon(StreamIcons.group), title: Text( 'New group', @@ -492,7 +498,12 @@ class _NewChatScreenState extends State { Container( color: Colors.white54, child: InkWell( - onTap: () {}, + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, child: Row( children: [ NeumorphicButton( @@ -581,3 +592,23 @@ class _NewChatScreenState extends State { ); } } + +class NewGroupChatScreen extends StatefulWidget { + @override + _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + title: Text( + 'Add Group Members', + style: TextStyle(color: Colors.black), + ), + ), + ); + } +} From 33e91ff18801dbe4205fc523b2f9c8eb0bd05531 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 15:11:40 +0530 Subject: [PATCH 064/101] [New Group Chat] Complete add group members screen --- example/lib/main.dart | 168 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 82fee6b2..907d4ec3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -599,15 +599,183 @@ class NewGroupChatScreen extends StatefulWidget { } class _NewGroupChatScreenState extends State { + TextEditingController _controller; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController() + ..addListener(() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 2, backgroundColor: Colors.white, title: Text( 'Add Group Members', style: TextStyle(color: Colors.black), ), + actions: [ + if (_selectedUsers.isNotEmpty) + IconButton( + icon: Icon( + StreamIcons.arrow_right, + color: Colors.blue.shade700, + ), + onPressed: () {}, + ) + ], + ), + body: UsersBloc( + child: Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(24), + ), + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: TextField( + onTap: () {}, + controller: _controller, + decoration: InputDecoration( + prefixIcon: Icon(StreamIcons.search), + hintText: 'Search', + contentPadding: const EdgeInsets.all(8), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ), + if (_selectedUsers.isNotEmpty) + Container( + height: 120, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _selectedUsers.length, + padding: const EdgeInsets.all(8), + separatorBuilder: (_, __) => SizedBox(width: 16), + itemBuilder: (_, index) { + final user = _selectedUsers.elementAt(index); + return Column( + children: [ + Stack( + children: [ + UserAvatar( + user: user, + showOnlineStatus: true, + borderRadius: BorderRadius.circular(40), + constraints: BoxConstraints.tightFor( + height: 80, + width: 80, + ), + ), + Positioned( + top: 0, + right: 0, + child: InkWell( + onTap: () { + if (_selectedUsers.contains(user)) { + setState(() => _selectedUsers.remove(user)); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: + Border.all(color: Colors.grey.shade100), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Icon( + Icons.clear_rounded, + size: 16, + ), + ), + ), + ), + ) + ], + ), + SizedBox(height: 4), + Text( + user.name.split(' ')[0], + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + ], + ); + }, + ), + ), + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? 'Matches for \"$_userNameQuery\"' + : 'On the platform', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: UserListView( + filterByUserName: _userNameQuery, + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + setState(() { + _selectedUsers.add(user); + }); + } + }, + pagination: PaginationParams( + limit: 25, + ), + ), + ), + ], + ), ), ); } From a793c623b299880641756b28d9f74936c28f61ee Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 10:58:10 +0100 Subject: [PATCH 065/101] use texteditingcontroller listener instead of onchanged --- lib/src/message_input.dart | 126 +++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 02fbbd8f..7ba3e4b9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -77,25 +77,25 @@ enum DefaultAttachmentTypes { /// Modify it to change the widget appearance. class MessageInput extends StatefulWidget { /// Instantiate a new MessageInput - MessageInput( - {Key key, - this.onMessageSent, - this.preMessageSending, - this.parentMessage, - this.editMessage, - this.maxHeight = 150, - this.keyboardType = TextInputType.multiline, - this.disableAttachments = false, - this.doImageUploadRequest, - this.doFileUploadRequest, - this.initialMessage, - this.textEditingController, - this.actions, - this.actionsLocation = ActionsLocation.left, - this.attachmentThumbnailBuilders, - this.inputTextStyle, - this.attachmentIconColor}) - : super(key: key); + MessageInput({ + Key key, + this.onMessageSent, + this.preMessageSending, + this.parentMessage, + this.editMessage, + this.maxHeight = 150, + this.keyboardType = TextInputType.multiline, + this.disableAttachments = false, + this.doImageUploadRequest, + this.doFileUploadRequest, + this.initialMessage, + this.textEditingController, + this.actions, + this.actionsLocation = ActionsLocation.left, + this.attachmentThumbnailBuilders, + this.inputTextStyle, + this.attachmentIconColor, + }) : super(key: key); /// Message to edit final Message editMessage; @@ -252,36 +252,6 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - onChanged: (s) { - StreamChannel.of(context).channel.keyStroke( - widget.parentMessage?.id, - ); - - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - }); - - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - - if (s.startsWith('/')) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } - - if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == '@' || - textEditingController.text - .substring(0, textEditingController.selection.start) - .split(' ') - .last - .contains('@'))) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - } - }, onTap: () { setState(() { _typingStarted = true; @@ -403,7 +373,7 @@ class MessageInputState extends State { OverlayEntry _buildMentionsOverlayEntry() { final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + .substring(0, textEditingController.value.selection.baseOffset) .split('@'); final query = splits.last.toLowerCase(); @@ -467,7 +437,7 @@ class MessageInputState extends State { text: rejoin + textEditingController.text.substring( textEditingController - .selection.start), + .selection.baseOffset), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -962,23 +932,7 @@ class MessageInputState extends State { if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { if (visible) { - if (_commandsOverlay != null) { - if (textEditingController.text.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); - }); - } - } + _onChange(); } else { if (_commandsOverlay != null) { _commandsOverlay.remove(); @@ -992,11 +946,47 @@ class MessageInputState extends State { textEditingController = widget.textEditingController ?? TextEditingController(); + + textEditingController.addListener(_onChange); + if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage); } } + void _onChange() { + final s = textEditingController.text; + StreamChannel.of(context).channel.keyStroke( + widget.parentMessage?.id, + ); + + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + }); + + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + + if (s.trim().startsWith('/')) { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } + + if (_messageIsPresent && + textEditingController.selection.isCollapsed && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) + .split(' ') + .last + .contains('@')) { + _mentionsOverlay = _buildMentionsOverlayEntry(); + Overlay.of(context).insert(_mentionsOverlay); + } + } + void _parseExistingMessage(Message message) { textEditingController.text = message.text; From 1c1cf66d9394cc32e1ee4b4243dddec30bcca1c7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 10:59:59 +0100 Subject: [PATCH 066/101] bump version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 945199b9..675d40d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.13+1 + +- Use TextEditingController.addListener instea of TextField.onChanged + ## 0.2.13 - Update llc dependency diff --git a/pubspec.yaml b/pubspec.yaml index 00be2548..f667acfb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.13 +version: 0.2.13+1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues From 6a8e76dc189d992a183065e3c1b9bb91f51f73d9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 11:00:25 +0100 Subject: [PATCH 067/101] fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 675d40d0..7d5b93f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.2.13+1 -- Use TextEditingController.addListener instea of TextField.onChanged +- Use TextEditingController.addListener instead of TextField.onChanged ## 0.2.13 From 199831a7ec7fa8fe383b47156e80f2da1e3eed33 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 11:50:41 +0100 Subject: [PATCH 068/101] fix show overlay --- lib/src/message_input.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 7ba3e4b9..a4bfcd18 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -934,12 +934,10 @@ class MessageInputState extends State { if (visible) { _onChange(); } else { - if (_commandsOverlay != null) { - _commandsOverlay.remove(); - } - if (_mentionsOverlay != null) { - _mentionsOverlay.remove(); - } + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; } }); } From 04da9a06b34aee886ebfb4decfe498732df9ed55 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 11:58:30 +0100 Subject: [PATCH 069/101] update github action --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 373aa879..bb72c2f2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Flutter action - uses: subosito/flutter-action@v1.3.2 + uses: subosito/flutter-action@v1.4.0 with: channel: 'stable' - name: Get dependencies From 7bb769e0a6f0ad49026f0d4a09fcd557402f6fec Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 12:32:28 +0100 Subject: [PATCH 070/101] update github action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8e87c58..4b3e086a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Flutter action - uses: subosito/flutter-action@v1.3.2 + uses: subosito/flutter-action@v1.4.0 with: channel: 'stable' - name: Get dependencies From d3ae3a5e4d6d58d6c669c4661938d7165d7fd82e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:06:44 +0530 Subject: [PATCH 071/101] [UserItem] Add "showLastSeen" property --- lib/src/user_item.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index ef442440..d4331cf8 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -24,6 +24,7 @@ class UserItem extends StatelessWidget { this.onLongPress, this.onImageTap, this.selected = false, + this.showLastSeen = true, }) : super(key: key); /// Function called when tapping this widget @@ -41,6 +42,9 @@ class UserItem extends StatelessWidget { /// If true the [UserItem] will show a trailing checkmark final bool selected; + /// If true the [UserItem] will show the last seen + final bool showLastSeen; + @override Widget build(BuildContext context) { return ListTile( @@ -72,7 +76,7 @@ class UserItem extends StatelessWidget { ) : null, title: Text(user.name), - subtitle: _buildLastActive(context), + subtitle: showLastSeen ? _buildLastActive(context) : null, ); } From 382e77e4a419ef893b4115e74ef1e21e6568d8f2 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:07:54 +0530 Subject: [PATCH 072/101] Expose UserItem from the package. --- lib/stream_chat_flutter.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 663e72a9..91ac68c8 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -32,3 +32,4 @@ export 'src/utils.dart'; export 'src/video_attachment.dart'; export 'src/users_bloc.dart'; export 'src/user_list_view.dart'; +export 'src/user_item.dart'; From 08bc56799dc5c7c4d50829ba2c03c718a9d99829 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:38:36 +0530 Subject: [PATCH 073/101] [New Group Chat] Complete name of group chat screen. --- example/lib/main.dart | 174 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 173 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 907d4ec3..9158b6b0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -643,7 +643,16 @@ class _NewGroupChatScreenState extends State { StreamIcons.arrow_right, color: Colors.blue.shade700, ), - onPressed: () {}, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupChatDetailsScreen( + selectedUsers: _selectedUsers.toList(growable: false), + ), + ), + ); + }, ) ], ), @@ -780,3 +789,166 @@ class _NewGroupChatScreenState extends State { ); } } + +class GroupChatDetailsScreen extends StatefulWidget { + final List selectedUsers; + + const GroupChatDetailsScreen({ + Key key, + @required this.selectedUsers, + }) : super(key: key); + + @override + _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); +} + +class _GroupChatDetailsScreenState extends State { + final _selectedUsers = []; + + TextEditingController _groupNameController; + + Channel _channel; + + bool _isGroupNameEmpty = true; + + @override + void initState() { + super.initState(); + _channel = StreamChat.of(context).client.channel('messaging'); + _selectedUsers.addAll(widget.selectedUsers); + _groupNameController = TextEditingController() + ..addListener(() { + final name = _groupNameController.text; + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 2, + backgroundColor: Colors.white, + title: Text( + 'Name of Group Chat', + style: TextStyle(color: Colors.black), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + Text( + 'NAME', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + ), + ), + SizedBox(width: 16), + Expanded( + child: TextField( + controller: _groupNameController, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: 8, + ), + hintText: 'Choose a group chat name', + hintStyle: TextStyle(fontSize: 18)), + ), + ), + ], + ), + ), + ), + actions: [ + NeumorphicButton( + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.symmetric(vertical: 8), + child: IconButton( + padding: const EdgeInsets.all(0), + icon: Icon(StreamIcons.check), + color: Colors.blue.shade700, + onPressed: _isGroupNameEmpty + ? null + : () async { + final groupName = _groupNameController.text; + final client = _channel.client; + _channel.extraData = { + 'members': [ + client.state.user.id, + ..._selectedUsers.map((e) => e.id), + ], + 'name': groupName, + }; + await _channel.watch(); + Navigator.of(context) + ..pop() + ..pushReplacement( + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: _channel, + ); + }, + ), + ); + }, + ), + ), + ], + ), + body: Column( + children: [ + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '5 Members', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: ListView.separated( + itemCount: _selectedUsers.length, + separatorBuilder: (_, __) => Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ), + itemBuilder: (_, index) { + final user = _selectedUsers[index]; + return UserItem( + key: ObjectKey(user), + user: user, + selected: true, + showLastSeen: false, + ); + }, + ), + ), + ], + ), + ); + } +} From 97b03910f1eeeee3670e1b3894e1b1933f9d75ab Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 17:47:50 +0530 Subject: [PATCH 074/101] Minor ui fixes --- example/lib/main.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9158b6b0..382c938d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -671,7 +671,6 @@ class _NewGroupChatScreenState extends State { horizontal: 8, ), child: TextField( - onTap: () {}, controller: _controller, decoration: InputDecoration( prefixIcon: Icon(StreamIcons.search), @@ -811,6 +810,8 @@ class _GroupChatDetailsScreenState extends State { bool _isGroupNameEmpty = true; + int get _totalUsers => _selectedUsers.length; + @override void initState() { super.initState(); @@ -920,7 +921,7 @@ class _GroupChatDetailsScreenState extends State { horizontal: 8, ), child: Text( - '5 Members', + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', style: TextStyle( fontWeight: FontWeight.w500, ), From 26e8ffeae3ebd1f779661f72641148e36a984440 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 13:24:48 +0100 Subject: [PATCH 075/101] remove onchanged --- lib/src/message_input.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 5fef41f7..cd96119a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -366,9 +366,6 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - onChanged: (s) { - _onChanged(context, s); - }, style: Theme.of(context).textTheme.bodyText2, autofocus: false, textAlignVertical: TextAlignVertical.center, From 0138f87c75f9dce2eddd6cca6664a1a483d7f422 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:18:01 +0530 Subject: [PATCH 076/101] [NewChat, NewGroup] Add emptyState widget --- example/lib/main.dart | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 382c938d..43a92d1b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -558,6 +558,35 @@ class _NewChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, ), ), MessageInput( @@ -780,6 +809,35 @@ class _NewGroupChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, ), ), ], From f3c32148029f0ef5c04e976c6c598a471ca0f342 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:18:55 +0530 Subject: [PATCH 077/101] [UserListView] Cleanup --- lib/src/user_list_view.dart | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index c4e3f2ce..4a3ae96c 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -388,33 +388,13 @@ class _UserListViewState extends State }, userItem: (user) { final selected = widget.selectedUsers?.contains(user) ?? false; - - UserTapCallback onTap; - if (widget.onUserTap != null) { - onTap = widget.onUserTap; - } else { - onTap = (client, _) { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) { - // return StreamChannel( - // child: widget.userWidget, - // channel: client, - // ); - // }, - // ), - // ); - }; - } - return Container( key: ValueKey('USER-${user.id}'), child: widget.userItemBuilder != null ? widget.userItemBuilder(context, user, selected) : UserItem( user: user, - onTap: (user) => onTap(user, widget.userWidget), + onTap: (user) => widget.onUserTap(user, widget.userWidget), onLongPress: widget.onUserLongPress, onImageTap: widget.onImageTap, selected: selected, From 1cbb10942f20f35a8ef62a235317ffd4d0f0db2b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:32:49 +0530 Subject: [PATCH 078/101] Remove old create channel screen --- example/lib/main.dart | 147 ------------------------------------------ 1 file changed, 147 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 43a92d1b..f9250efe 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -167,14 +167,6 @@ class ChannelListPage extends StatelessWidget { ), ), ), - floatingActionButton: FloatingActionButton( - child: Icon(Icons.add), - onPressed: () { - Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return CreateChannelPage(); - })); - }, - ), body: ChannelsBloc( child: ChannelListView( swipeToAction: true, @@ -270,145 +262,6 @@ class ThreadPage extends StatelessWidget { } } -class CreateChannelPage extends StatefulWidget { - @override - _CreateChannelPageState createState() => _CreateChannelPageState(); -} - -class _CreateChannelPageState extends State { - final selectedUsers = {}; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.transparent, - title: Text( - 'Create a channel', - style: Theme.of(context).textTheme.headline6, - ), - ), - floatingActionButton: - selectedUsers.isNotEmpty ? _buildFAB(context) : null, - body: _buildListView(), - ); - } - - Widget _buildListView() { - return UsersBloc( - child: UserListView( - pagination: PaginationParams( - limit: 25, - ), - sort: [ - SortOption( - 'name', - direction: SortOption.ASC, - ), - ], - onUserLongPress: (user) { - _selectUser(user); - }, - selectedUsers: selectedUsers, - onUserTap: (user, _) { - if (selectedUsers.isNotEmpty) { - return _selectUser(user); - } - _createChannel(context, [user]); - }, - ), - ); - } - - Widget _buildFAB(BuildContext context) { - return FloatingActionButton( - child: Icon(Icons.done), - onPressed: () async { - String name; - if (selectedUsers.length > 1) { - name = await _showEnterNameDialog(context); - if (name?.isNotEmpty != true) { - return; - } - } - - _createChannel(context, selectedUsers.toList(), name); - }, - ); - } - - Future _showEnterNameDialog(BuildContext context) { - final controller = TextEditingController(); - return showDialog( - context: context, - builder: (context) => SimpleDialog( - contentPadding: const EdgeInsets.all(16), - title: Text('Enter a name for the channel'), - children: [ - TextField( - controller: controller, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - ), - ButtonBar( - children: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'), - ), - FlatButton( - onPressed: () => Navigator.pop(context, controller.text), - child: Text('Ok'), - ), - ], - ), - ], - ), - ); - } - - Future _createChannel( - BuildContext context, - List users, [ - String name, - ]) async { - final client = StreamChat.of(context).client; - final channel = client.channel('messaging', extraData: { - 'members': [ - client.state.user.id, - ...users.map((e) => e.id), - ], - if (name != null) 'name': name, - }); - await channel.watch(); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - } - - void _selectUser(User user) { - if (!selectedUsers.contains(user)) { - setState(() { - selectedUsers.add(user); - }); - } else { - setState(() { - selectedUsers.remove(user); - }); - } - } -} - class NewChatScreen extends StatefulWidget { @override _NewChatScreenState createState() => _NewChatScreenState(); From 822547b6442d749b4402384b8f31323db756cb79 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Thu, 19 Nov 2020 18:40:45 +0530 Subject: [PATCH 079/101] Release textEditingController listeners in dispose. --- example/lib/main.dart | 54 +++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f9250efe..baddd1ee 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -284,22 +284,24 @@ class _NewChatScreenState extends State { Channel channel; + void _userNameListener() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + } + @override void initState() { super.initState(); channel = StreamChat.of(context).client.channel('messaging'); - _controller = TextEditingController() - ..addListener(() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); + _controller = TextEditingController()..addListener(_userNameListener); } @override void dispose() { _controller?.clear(); + _controller?.removeListener(_userNameListener); _controller?.dispose(); super.dispose(); } @@ -489,21 +491,23 @@ class _NewGroupChatScreenState extends State { bool _isSearchActive = false; + void _userNameListener() { + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + } + @override void initState() { super.initState(); - _controller = TextEditingController() - ..addListener(() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); + _controller = TextEditingController()..addListener(_userNameListener); } @override void dispose() { _controller?.clear(); + _controller?.removeListener(_userNameListener); _controller?.dispose(); super.dispose(); } @@ -723,18 +727,28 @@ class _GroupChatDetailsScreenState extends State { int get _totalUsers => _selectedUsers.length; + void _groupNameListener() { + final name = _groupNameController.text; + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } + @override void initState() { super.initState(); _channel = StreamChat.of(context).client.channel('messaging'); _selectedUsers.addAll(widget.selectedUsers); _groupNameController = TextEditingController() - ..addListener(() { - final name = _groupNameController.text; - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); - }); + ..addListener(_groupNameListener); + } + + @override + void dispose() { + _groupNameController?.clear(); + _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.dispose(); + super.dispose(); } @override From 5421a447ba7870fbc5bcc3dbbdf14efebef30311 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 19 Nov 2020 16:22:16 +0100 Subject: [PATCH 080/101] init notifications --- example/lib/advanced_options_page.dart | 4 ++++ example/lib/main.dart | 3 +++ example/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 56c9dbf0..c84ef58e 100644 --- a/example/lib/advanced_options_page.dart +++ b/example/lib/advanced_options_page.dart @@ -282,6 +282,10 @@ class _AdvancedOptionsPageState extends State { }), userToken, ); + + if (!kIsWeb) { + initNotifications(client); + } } catch (e) { var errorText = 'Error connecting, retry'; if (e is Map) { diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e3f43e2..d42bb7af 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -30,6 +30,9 @@ void main() async { User(id: userId), token, ); + if (!kIsWeb) { + initNotifications(client); + } } runApp(MyApp(client)); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fca549d2..e5c11897 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.60+62 +version: 1.0.61+63 environment: sdk: ">=2.2.2 <3.0.0" From 389656a4777d34923bc82ac4172781a4ee4c63c8 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 12:06:00 +0530 Subject: [PATCH 081/101] UI nit picks --- example/lib/chips_input_text_field.dart | 44 ++++----- example/lib/main.dart | 125 ++++++++++++++++-------- lib/src/user_item.dart | 21 ++-- lib/src/user_list_view.dart | 10 +- 4 files changed, 124 insertions(+), 76 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e62773f7..e3b0eae7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -64,24 +64,20 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { return Material( - elevation: 4, + elevation: 1, color: Colors.white, child: Container( child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 16.0, - ), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( 'TO:', style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -92,46 +88,48 @@ class ChipInputTextFieldState extends State> { mainAxisSize: MainAxisSize.min, children: [ Wrap( - spacing: 4.0, + spacing: 8.0, runSpacing: 4.0, children: _chips.map((item) { return widget.chipBuilder(context, item); }).toList(), ), if (!_pauseItemAddition) ...[ - if (_chips.isNotEmpty) SizedBox(height: 4), TextField( controller: widget.controller, onChanged: widget.onInputChanged, focusNode: widget.focusNode, - style: TextStyle(fontSize: 18), decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 8, - ), - hintText: widget.hint, - hintStyle: TextStyle(fontSize: 18)), + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + ), ), ] ], ), ), - SizedBox(width: 12), IconButton( icon: Icon( _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), ), onPressed: () { resumeItemAddition(); }, + alignment: Alignment.topRight, visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), ), ], ), diff --git a/example/lib/main.dart b/example/lib/main.dart index baddd1ee..f187ac05 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -312,10 +312,12 @@ class _NewChatScreenState extends State { appBar: AppBar( elevation: 0, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'New Chat', style: TextStyle(color: Colors.black), ), + centerTitle: true, ), body: StreamChannel( showLoading: false, @@ -329,17 +331,31 @@ class _NewChatScreenState extends State { controller: _controller, focusNode: FocusNode(), chipBuilder: (context, user) { - return InputChip( - key: ObjectKey(user), - label: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - avatar: UserAvatar( - user: user, - ), - onDeleted: () => _chipInputTextFieldState.removeItem(user), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + ), + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], ); }, onChipAdded: (user) { @@ -381,7 +397,17 @@ class _NewChatScreenState extends State { ), Container( width: double.maxFinite, - color: Colors.grey.shade50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, @@ -392,7 +418,7 @@ class _NewChatScreenState extends State { ? "Matches for \"$_userNameQuery\"" : 'On the platform', style: TextStyle( - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -516,12 +542,14 @@ class _NewGroupChatScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - elevation: 2, + elevation: 1, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'Add Group Members', style: TextStyle(color: Colors.black), ), + centerTitle: true, actions: [ if (_selectedUsers.isNotEmpty) IconButton( @@ -546,6 +574,7 @@ class _NewGroupChatScreenState extends State { child: Column( children: [ Container( + height: 36, decoration: BoxDecoration( border: Border.all( color: Colors.grey.shade300, @@ -559,9 +588,13 @@ class _NewGroupChatScreenState extends State { child: TextField( controller: _controller, decoration: InputDecoration( - prefixIcon: Icon(StreamIcons.search), + prefixIcon: Icon( + StreamIcons.search, + color: Colors.black, + ), hintText: 'Search', - contentPadding: const EdgeInsets.all(8), + contentPadding: const EdgeInsets.all(0), + // isDense: true, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24), @@ -571,7 +604,7 @@ class _NewGroupChatScreenState extends State { ), if (_selectedUsers.isNotEmpty) Container( - height: 120, + height: 104, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _selectedUsers.length, @@ -586,10 +619,10 @@ class _NewGroupChatScreenState extends State { UserAvatar( user: user, showOnlineStatus: true, - borderRadius: BorderRadius.circular(40), + borderRadius: BorderRadius.circular(32), constraints: BoxConstraints.tightFor( - height: 80, - width: 80, + height: 64, + width: 64, ), ), Positioned( @@ -609,10 +642,10 @@ class _NewGroupChatScreenState extends State { Border.all(color: Colors.grey.shade100), ), child: Padding( - padding: const EdgeInsets.all(4.0), + padding: const EdgeInsets.all(2.0), child: Icon( Icons.clear_rounded, - size: 16, + size: 14, ), ), ), @@ -624,8 +657,8 @@ class _NewGroupChatScreenState extends State { Text( user.name.split(' ')[0], style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, + fontWeight: FontWeight.bold, + fontSize: 12, ), ), ], @@ -635,7 +668,17 @@ class _NewGroupChatScreenState extends State { ), Container( width: double.maxFinite, - color: Colors.grey.shade50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, @@ -646,7 +689,7 @@ class _NewGroupChatScreenState extends State { ? 'Matches for \"$_userNameQuery\"' : 'On the platform', style: TextStyle( - fontWeight: FontWeight.w500, + color: Colors.black.withOpacity(0.5), ), ), ), @@ -755,42 +798,40 @@ class _GroupChatDetailsScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - elevation: 2, + elevation: 1, backgroundColor: Colors.white, + leading: const StreamBackButton(), title: Text( 'Name of Group Chat', style: TextStyle(color: Colors.black), ), + centerTitle: true, bottom: PreferredSize( preferredSize: Size.fromHeight(kToolbarHeight), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), child: Row( children: [ Text( 'NAME', style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, + color: Colors.black.withOpacity(0.5), ), ), SizedBox(width: 16), Expanded( child: TextField( controller: _groupNameController, - style: TextStyle(fontSize: 18), decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - vertical: 8, - ), - hintText: 'Choose a group chat name', - hintStyle: TextStyle(fontSize: 18)), + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.all(0), + hintText: 'Choose a group chat name', + ), ), ), ], diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index d4331cf8..0c320b7c 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -59,13 +59,18 @@ class UserItem extends StatelessWidget { } }, leading: UserAvatar( - user: user, - showOnlineStatus: true, - onTap: (user) { - if (onImageTap != null) { - onImageTap(user); - } - }), + user: user, + showOnlineStatus: true, + onTap: (user) { + if (onImageTap != null) { + onImageTap(user); + } + }, + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), trailing: selected ? CircleAvatar( child: Icon( @@ -75,7 +80,7 @@ class UserItem extends StatelessWidget { radius: 10, ) : null, - title: Text(user.name), + title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), subtitle: showLastSeen ? _buildLastActive(context) : null, ); } diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 4a3ae96c..b35c1c68 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -376,12 +376,16 @@ class _UserListViewState extends State headerItem: (header) { return Container( key: ValueKey('HEADER-$header'), - color: Colors.grey.shade100, + color: Colors.black.withOpacity(0.05), child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal:8.0,vertical: 6), child: Text( header, - style: TextStyle(fontWeight: FontWeight.w500), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color:Colors.black.withOpacity(0.3) + ), ), ), ); From c7e27577b2dc9d28534522c1ce27cf0b129becce Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 11:22:41 +0100 Subject: [PATCH 082/101] use fork of media gallery waiting for the pr to be merged --- lib/src/media_list_view.dart | 1 + pubspec.yaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 225d327b..da21111b 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -44,6 +44,7 @@ class _MediaListViewState extends State { placeholder: MemoryImage(kTransparentImage), image: MediaThumbnailProvider( media: media, + highQuality: true, ), fit: BoxFit.cover, ), diff --git a/pubspec.yaml b/pubspec.yaml index 9c1b0a29..97804c53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,8 @@ dependencies: flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 - media_gallery: ^0.1.5 + media_gallery: + git: https://github.com/imtoori/media_gallery.git permission_handler: ^5.0.1+1 transparent_image: ^1.0.0 From 09fd54a605898ac309ea12ceb828bc88298b8158 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 11:58:11 +0100 Subject: [PATCH 083/101] update fadeinduration --- lib/src/media_list_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index da21111b..cd414fa9 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -41,6 +41,7 @@ class _MediaListViewState extends State { AspectRatio( aspectRatio: 1.0, child: FadeInImage( + fadeInDuration: Duration(milliseconds: 300), placeholder: MemoryImage(kTransparentImage), image: MediaThumbnailProvider( media: media, From c384588d5b1533e41412a27e0831d326a4c5e06e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 17:55:55 +0530 Subject: [PATCH 084/101] UI nit picks --- example/lib/chips_input_text_field.dart | 9 +-- example/lib/main.dart | 78 +++++++++++++++------- example/lib/neumorphic_button.dart | 38 ++++------- lib/src/user_list_view.dart | 88 +++++++++---------------- 4 files changed, 102 insertions(+), 111 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index e3b0eae7..695783d9 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -34,10 +34,8 @@ class ChipInputTextFieldState extends State> { bool _pauseItemAddition = false; void addItem(T item) { - if (!_pauseItemAddition) { - setState(() => _chips.add(item)); - if (widget.onChipAdded != null) widget.onChipAdded(item); - } + setState(() => _chips.add(item)); + if (widget.onChipAdded != null) widget.onChipAdded(item); } void removeItem(T item) { @@ -108,6 +106,9 @@ class ChipInputTextFieldState extends State> { disabledBorder: InputBorder.none, contentPadding: const EdgeInsets.only(top: 4.0), hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), ] diff --git a/example/lib/main.dart b/example/lib/main.dart index f187ac05..9cce3cf3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -309,6 +309,7 @@ class _NewChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 0, backgroundColor: Colors.white, @@ -367,7 +368,6 @@ class _NewChatScreenState extends State { ), if (!_isSearchActive) Container( - color: Colors.white54, child: InkWell( onTap: () { Navigator.push( @@ -375,23 +375,26 @@ class _NewChatScreenState extends State { MaterialPageRoute(builder: (_) => NewGroupChatScreen()), ); }, - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Colors.blue.shade700, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), - ), - ], + ], + ), ), ), ), @@ -425,20 +428,33 @@ class _NewChatScreenState extends State { ), Expanded( child: UserListView( - filterByUserName: _userNameQuery, selectedUsers: _selectedUsers, groupAlphabetically: _isSearchActive ? false : true, onUserTap: (user, _) { + _controller.clear(); if (!_selectedUsers.contains(user)) { - _controller.clear(); _chipInputTextFieldState ..addItem(user) ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); } }, pagination: PaginationParams( limit: 25, ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { @@ -541,6 +557,7 @@ class _NewGroupChatScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 1, backgroundColor: Colors.white, @@ -555,7 +572,7 @@ class _NewGroupChatScreenState extends State { IconButton( icon: Icon( StreamIcons.arrow_right, - color: Colors.blue.shade700, + color: Color(0xFF006CFF), ), onPressed: () { Navigator.push( @@ -576,6 +593,7 @@ class _NewGroupChatScreenState extends State { Container( height: 36, decoration: BoxDecoration( + color: Colors.white, border: Border.all( color: Colors.grey.shade300, ), @@ -593,8 +611,10 @@ class _NewGroupChatScreenState extends State { color: Colors.black, ), hintText: 'Search', + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), contentPadding: const EdgeInsets.all(0), - // isDense: true, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24), @@ -696,7 +716,6 @@ class _NewGroupChatScreenState extends State { ), Expanded( child: UserListView( - filterByUserName: _userNameQuery, selectedUsers: _selectedUsers, groupAlphabetically: _isSearchActive ? false : true, onUserTap: (user, _) { @@ -709,6 +728,18 @@ class _NewGroupChatScreenState extends State { pagination: PaginationParams( limit: 25, ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { @@ -797,6 +828,7 @@ class _GroupChatDetailsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: AppBar( elevation: 1, backgroundColor: Colors.white, @@ -840,12 +872,10 @@ class _GroupChatDetailsScreenState extends State { ), actions: [ NeumorphicButton( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.symmetric(vertical: 8), child: IconButton( padding: const EdgeInsets.all(0), icon: Icon(StreamIcons.check), - color: Colors.blue.shade700, + color: Color(0xFF006CFF), onPressed: _isGroupNameEmpty ? null : () async { diff --git a/example/lib/neumorphic_button.dart b/example/lib/neumorphic_button.dart index c738cecd..69454fb2 100644 --- a/example/lib/neumorphic_button.dart +++ b/example/lib/neumorphic_button.dart @@ -1,50 +1,38 @@ import 'package:flutter/material.dart'; -extension ColorUtils on Color { - Color mix(Color another, double amount) { - return Color.lerp(this, another, amount); - } -} - class NeumorphicButton extends StatelessWidget { final Widget child; final Color backgroundColor; - final EdgeInsets margin; - final EdgeInsets padding; const NeumorphicButton({ Key key, @required this.child, this.backgroundColor = Colors.white, - this.margin = const EdgeInsets.all(8), - this.padding = const EdgeInsets.all(14), }) : super(key: key); @override Widget build(BuildContext context) { return Container( child: child, - margin: margin, - padding: padding, + margin: EdgeInsets.all(8.0), + height: 40, + width: 40, decoration: BoxDecoration( - shape: BoxShape.circle, color: backgroundColor, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - backgroundColor.mix(Colors.white, 0.2), - backgroundColor.mix(Colors.black, 0.1), - ]), + shape: BoxShape.circle, boxShadow: [ BoxShadow( - blurRadius: 1, - color: backgroundColor.mix(Colors.white, 0.6), + color: Colors.grey[700], + offset: Offset(0, 1.0), + blurRadius: 0.5, + spreadRadius: 0, ), BoxShadow( - blurRadius: 1, - color: backgroundColor.mix(Colors.black, 0.3), - ) + color: Colors.white, + offset: Offset.zero, + blurRadius: 0.5, + spreadRadius: 0, + ), ], ), ); diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b35c1c68..b21e8c7d 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/users_bloc.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -66,13 +65,7 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - this.filterByUserName = '', - this.filterByUserNameStream, - }) : assert( - filterByUserName == null || filterByUserNameStream == null, - 'Cannot provide both filterByUserName and filterByUserNameStream.', - ), - super(key: key); + }) : super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -137,12 +130,6 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; - /// - final String filterByUserName; - - /// - final Stream filterByUserNameStream; - @override _UserListViewState createState() => _UserListViewState(); } @@ -154,8 +141,8 @@ class _UserListViewState extends State @override void initState() { super.initState(); - final channelsBloc = UsersBloc.of(context); - channelsBloc.queryUsers( + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( filter: widget.filter, sort: widget.sort, pagination: widget.pagination, @@ -163,9 +150,9 @@ class _UserListViewState extends State ); _scrollController.addListener(() { - channelsBloc.queryUsersLoading.first.then((loading) { + usersBloc.queryUsersLoading.first.then((loading) { if (!loading) { - _listenUserPagination(channelsBloc); + _listenUserPagination(usersBloc); } }); }); @@ -192,42 +179,28 @@ class _UserListViewState extends State ); } - List _getFilteredItems(List users, String query) { - if (widget.groupAlphabetically) { - var temp = users..sort((curr, next) => curr.name.compareTo(next.name)); - temp = temp - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - final groupedUsers = >{}; - for (var e in temp) { - final alphabet = e.name[0]; - groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; - } - final items = []; - for (var key in groupedUsers.keys) { - items.add(ListHeaderItem(key)); - items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); - } - return items; - } - return users - .where((it) => it.name.toLowerCase().contains(query.toLowerCase())) - .map((e) => ListUserItem(e)) - .toList(); - } - Stream> _buildUserStream( UsersBlocState usersBlocState, ) { - if (widget.filterByUserNameStream == null) { - return usersBlocState.usersStream.map( - (users) => _getFilteredItems(users, widget.filterByUserName), - ); - } - return Rx.combineLatest2( - usersBlocState.usersStream, - widget.filterByUserNameStream, - _getFilteredItems, + return usersBlocState.usersStream.map( + (users) { + if (widget.groupAlphabetically) { + var temp = users + ..sort((curr, next) => curr.name.compareTo(next.name)); + final groupedUsers = >{}; + for (var e in temp) { + final alphabet = e.name[0]; + groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; + } + final items = []; + for (var key in groupedUsers.keys) { + items.add(ListHeaderItem(key)); + items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); + } + return items; + } + return users.map((e) => ListUserItem(e)).toList(); + }, ); } @@ -378,14 +351,13 @@ class _UserListViewState extends State key: ValueKey('HEADER-$header'), color: Colors.black.withOpacity(0.05), child: Padding( - padding: const EdgeInsets.symmetric(horizontal:8.0,vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6), child: Text( header, style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color:Colors.black.withOpacity(0.3) - ), + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.black.withOpacity(0.3)), ), ), ); @@ -469,8 +441,8 @@ class _UserListViewState extends State widget.pagination?.toJson()?.toString() != oldWidget.pagination?.toJson()?.toString() || widget.options?.toString() != oldWidget.options?.toString()) { - final channelsBloc = UsersBloc.of(context); - channelsBloc.queryUsers( + final usersBloc = UsersBloc.of(context); + usersBloc.queryUsers( filter: widget.filter, sort: widget.sort, pagination: widget.pagination, From 409d95d86a6d3310e9bfe0b5ba60e004894b1523 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:06:47 +0530 Subject: [PATCH 085/101] [UserListView] Add a "isListAlreadySorted" check before sorting list. --- lib/src/user_list_view.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b21e8c7d..0f7b969e 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -179,14 +179,19 @@ class _UserListViewState extends State ); } + bool get isListAlreadySorted => + widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; + Stream> _buildUserStream( UsersBlocState usersBlocState, ) { return usersBlocState.usersStream.map( (users) { if (widget.groupAlphabetically) { - var temp = users - ..sort((curr, next) => curr.name.compareTo(next.name)); + var temp = users; + if (!isListAlreadySorted) { + temp = users..sort((curr, next) => curr.name.compareTo(next.name)); + } final groupedUsers = >{}; for (var e in temp) { final alphabet = e.name[0]; From 2908cd66c5019cebcf49b54ffb4c4266dbe746b0 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:11:57 +0530 Subject: [PATCH 086/101] Change last seen text to last online --- example/lib/main.dart | 2 +- lib/src/user_item.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9cce3cf3..ea665cb6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -939,7 +939,7 @@ class _GroupChatDetailsScreenState extends State { key: ObjectKey(user), user: user, selected: true, - showLastSeen: false, + showLastOnline: false, ); }, ), diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index 0c320b7c..be8037b6 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -24,7 +24,7 @@ class UserItem extends StatelessWidget { this.onLongPress, this.onImageTap, this.selected = false, - this.showLastSeen = true, + this.showLastOnline = true, }) : super(key: key); /// Function called when tapping this widget @@ -43,7 +43,7 @@ class UserItem extends StatelessWidget { final bool selected; /// If true the [UserItem] will show the last seen - final bool showLastSeen; + final bool showLastOnline; @override Widget build(BuildContext context) { @@ -81,11 +81,11 @@ class UserItem extends StatelessWidget { ) : null, title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), - subtitle: showLastSeen ? _buildLastActive(context) : null, + subtitle: showLastOnline ? _buildLastActive(context) : null, ); } Widget _buildLastActive(context) { - return Text('Last seen ${Jiffy(user.lastActive).fromNow()}'); + return Text('Last online ${Jiffy(user.lastActive).fromNow()}'); } } From aa62d9a01de261261f85265aa1503eaaf364b486 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:15:59 +0530 Subject: [PATCH 087/101] add debounce in searchQuery --- example/lib/main.dart | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ea665cb6..6f86ce23 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; import 'notifications_service.dart'; import 'neumorphic_button.dart'; +import 'dart:async'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -284,10 +285,16 @@ class _NewChatScreenState extends State { Channel channel; + Timer _debounce; + void _userNameListener() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); }); } @@ -533,10 +540,16 @@ class _NewGroupChatScreenState extends State { bool _isSearchActive = false; + Timer _debounce; + void _userNameListener() { - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); }); } From 4ded4d577d56e5c9711add180c8d12977e254bdf Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:38:07 +0530 Subject: [PATCH 088/101] Hide Create group button once a user is selected --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6f86ce23..b0f5d73f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -373,7 +373,7 @@ class _NewChatScreenState extends State { setState(() => _selectedUsers.remove(user)); }, ), - if (!_isSearchActive) + if (!_isSearchActive && !_selectedUsers.isNotEmpty) Container( child: InkWell( onTap: () { From daf8758f61aebf701ce6924d586f25307464e7d5 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:58:53 +0530 Subject: [PATCH 089/101] Fix UserAdd IconButton alignment --- example/lib/chips_input_text_field.dart | 123 ++++++++++++------------ 1 file changed, 63 insertions(+), 60 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 695783d9..1eb3159d 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -67,72 +67,75 @@ class ChipInputTextFieldState extends State> { child: Container( child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'TO:', - style: TextStyle( - color: Colors.black.withOpacity(0.5), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'TO:', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: _chips.map((item) { - return widget.chipBuilder(context, item); - }).toList(), - ), - if (!_pauseItemAddition) ...[ - TextField( - controller: widget.controller, - onChanged: widget.onInputChanged, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(top: 4.0), - hintText: widget.hint, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - ] - ], + ], + ), ), - ), - IconButton( - icon: Icon( - _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, - color: Colors.black.withOpacity(0.5), + SizedBox(width: 12), + Align( + alignment: Alignment.bottomCenter, + child: IconButton( + icon: Icon( + _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), + ), + onPressed: !_pauseItemAddition ? null : resumeItemAddition, + alignment: Alignment.topRight, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), ), - onPressed: () { - resumeItemAddition(); - }, - alignment: Alignment.topRight, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), - ], + ], + ), ), ), ), From 8bc9e62917d85a66ef7a22703ed615c754603ae7 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 18:59:36 +0530 Subject: [PATCH 090/101] Reduce debounce duration to 350ms from 500ms --- example/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b0f5d73f..2a30af4f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -289,7 +289,7 @@ class _NewChatScreenState extends State { void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { + _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) setState(() { _userNameQuery = _controller.text; @@ -544,7 +544,7 @@ class _NewGroupChatScreenState extends State { void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { + _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted) setState(() { _userNameQuery = _controller.text; From cc22b1b4ae8c9b7ffd5600fa3054480d62452e6b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 14:55:38 +0100 Subject: [PATCH 091/101] fix ui issues --- example/lib/chips_input_text_field.dart | 5 ++- example/lib/main.dart | 43 +++++++++++++++++-------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 6 ++-- lib/src/user_avatar.dart | 15 +++++---- lib/src/user_item.dart | 5 ++- 6 files changed, 50 insertions(+), 26 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 1eb3159d..30183926 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -69,13 +69,14 @@ class ChipInputTextFieldState extends State> { padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), child: IntrinsicHeight( child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.baseline, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( 'TO:', style: TextStyle( + fontSize: 12, color: Colors.black.withOpacity(0.5), ), ), @@ -109,6 +110,7 @@ class ChipInputTextFieldState extends State> { hintText: widget.hint, hintStyle: TextStyle( color: Colors.black.withOpacity(0.5), + fontSize: 14, ), ), ), @@ -122,6 +124,7 @@ class ChipInputTextFieldState extends State> { icon: Icon( _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, color: Colors.black.withOpacity(0.5), + size: 24, ), onPressed: !_pauseItemAddition ? null : resumeItemAddition, alignment: Alignment.topRight, diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a30af4f..758e7784 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:example/choose_user_page.dart'; @@ -8,9 +9,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'chips_input_text_field.dart'; -import 'notifications_service.dart'; import 'neumorphic_button.dart'; -import 'dart:async'; +import 'notifications_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -179,7 +179,6 @@ class ChannelListPage extends StatelessWidget { options: { 'presence': true, }, - sort: [SortOption('last_message_at')], pagination: PaginationParams( limit: 20, ), @@ -323,7 +322,10 @@ class _NewChatScreenState extends State { leading: const StreamBackButton(), title: Text( 'New Chat', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, ), @@ -577,7 +579,10 @@ class _NewGroupChatScreenState extends State { leading: const StreamBackButton(), title: Text( 'Add Group Members', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, actions: [ @@ -622,10 +627,12 @@ class _NewGroupChatScreenState extends State { prefixIcon: Icon( StreamIcons.search, color: Colors.black, + size: 24, ), hintText: 'Search', hintStyle: TextStyle( color: Colors.black.withOpacity(0.5), + fontSize: 14, ), contentPadding: const EdgeInsets.all(0), border: OutlineInputBorder( @@ -650,6 +657,7 @@ class _NewGroupChatScreenState extends State { Stack( children: [ UserAvatar( + onlineIndicatorAlignment: Alignment(0.9, 0.9), user: user, showOnlineStatus: true, borderRadius: BorderRadius.circular(32), @@ -659,9 +667,9 @@ class _NewGroupChatScreenState extends State { ), ), Positioned( - top: 0, - right: 0, - child: InkWell( + top: -4, + right: -4, + child: GestureDetector( onTap: () { if (_selectedUsers.contains(user)) { setState(() => _selectedUsers.remove(user)); @@ -671,14 +679,15 @@ class _NewGroupChatScreenState extends State { decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, - border: - Border.all(color: Colors.grey.shade100), + border: Border.all( + color: Colors.grey.shade100, + ), ), child: Padding( - padding: const EdgeInsets.all(2.0), + padding: const EdgeInsets.all(0.0), child: Icon( - Icons.clear_rounded, - size: 14, + StreamIcons.close, + size: 24, ), ), ), @@ -848,7 +857,10 @@ class _GroupChatDetailsScreenState extends State { leading: const StreamBackButton(), title: Text( 'Name of Group Chat', - style: TextStyle(color: Colors.black), + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), ), centerTitle: true, bottom: PreferredSize( @@ -860,6 +872,7 @@ class _GroupChatDetailsScreenState extends State { Text( 'NAME', style: TextStyle( + fontSize: 12, color: Colors.black.withOpacity(0.5), ), ), @@ -876,6 +889,8 @@ class _GroupChatDetailsScreenState extends State { disabledBorder: InputBorder.none, contentPadding: const EdgeInsets.all(0), hintText: 'Choose a group chat name', + hintStyle: TextStyle( + fontSize: 14, color: Colors.black.withOpacity(.5)), ), ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fca549d2..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.60+62 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index cd96119a..423d4c8f 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -423,7 +423,7 @@ class MessageInputState extends State { } void _onChanged(BuildContext context, String s) { - StreamChannel.of(context).channel.keyStroke(); + StreamChannel.of(context).channel.keyStroke().catchError((e) {}); setState(() { _messageIsPresent = s.trim().isNotEmpty; @@ -1712,7 +1712,9 @@ class MessageInputState extends State { if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { - _onChanged(context, textEditingController.text); + if (_focusNode.hasFocus) { + _onChanged(context, textEditingController.text); + } }); } diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 66edba39..566ef603 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -13,9 +13,11 @@ class UserAvatar extends StatelessWidget { this.onTap, this.showOnlineStatus = true, this.borderRadius, + this.onlineIndicatorAlignment = Alignment.topRight, }) : super(key: key); final User user; + final Alignment onlineIndicatorAlignment; final BoxConstraints constraints; final BorderRadius borderRadius; final BoxConstraints onlineIndicatorConstraints; @@ -60,11 +62,11 @@ class UserAvatar extends StatelessWidget { ), ), if (showOnlineStatus && user.online == true) - Positioned( - top: 0, - right: 0, - child: Material( - child: Center( + Positioned.fill( + child: Align( + alignment: onlineIndicatorAlignment, + child: Material( + type: MaterialType.circle, child: Container( padding: const EdgeInsets.all(2.0), constraints: onlineIndicatorConstraints ?? @@ -77,9 +79,8 @@ class UserAvatar extends StatelessWidget { color: Color(0xff20E070), ), ), + color: Colors.white, ), - shape: CircleBorder(), - color: Colors.white, ), ), ], diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index be8037b6..06ab0848 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -80,7 +80,10 @@ class UserItem extends StatelessWidget { radius: 10, ) : null, - title: Text(user.name,style: TextStyle(fontWeight: FontWeight.bold),), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), subtitle: showLastOnline ? _buildLastActive(context) : null, ); } From 879f5d31bc3c0e7e5ee5c9bcc6a99da85729090e Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 19:31:13 +0530 Subject: [PATCH 092/101] UI fixes --- example/lib/main.dart | 50 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a30af4f..cc64cbd7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -736,6 +736,10 @@ class _NewGroupChatScreenState extends State { setState(() { _selectedUsers.add(user); }); + } else { + setState(() { + _selectedUsers.remove(user); + }); } }, pagination: PaginationParams( @@ -939,7 +943,7 @@ class _GroupChatDetailsScreenState extends State { ), Expanded( child: ListView.separated( - itemCount: _selectedUsers.length, + itemCount: _selectedUsers.length + 1, separatorBuilder: (_, __) => Container( height: 1, color: Theme.of(context).brightness == Brightness.dark @@ -947,12 +951,48 @@ class _GroupChatDetailsScreenState extends State { : Colors.black.withOpacity(0.1), ), itemBuilder: (_, index) { + if (index == _selectedUsers.length) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } final user = _selectedUsers[index]; - return UserItem( + return ListTile( key: ObjectKey(user), - user: user, - selected: true, - showLastOnline: false, + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + width: 40, + height: 40, + ), + ), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + trailing: IconButton( + icon: Icon( + Icons.clear_rounded, + color: Colors.black, + ), + padding: const EdgeInsets.all(0), + splashRadius: 24, + onPressed: () { + setState(() { + _selectedUsers.remove(user); + }); + if (_selectedUsers.isEmpty) { + Navigator.pop(context); + } + }, + ), ); }, ), From 1d5717ce9a03cc5e5080b83db338e845fbe7e56b Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 20:04:51 +0530 Subject: [PATCH 093/101] Add flutter format check in test.yml --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b3e086a..a76cc4c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,8 @@ jobs: channel: 'stable' - name: Get dependencies run: flutter pub get + - name: Run formatter + run: flutter format -n --set-exit-if-changed . - name: Coverage fix run: | file=test/coverage_helper_test.dart From 265e8622f3cc543de8070a076692fe6b3bdec832 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 20:05:14 +0530 Subject: [PATCH 094/101] Add formatting mistake for testing action. --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index d42bb7af..9c1977e3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -434,6 +434,6 @@ class _CreateChannelPageState extends State { ...value.users, ]; }); - }).whenComplete(() => loading = false); + }).whenComplete(() => loading = false ); } } From 593dcabc6e8510e00fcf53e6de7c6bf8fa355bd3 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Fri, 20 Nov 2020 20:08:50 +0530 Subject: [PATCH 095/101] Fix formatting mistake --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 9c1977e3..d42bb7af 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -434,6 +434,6 @@ class _CreateChannelPageState extends State { ...value.users, ]; }); - }).whenComplete(() => loading = false ); + }).whenComplete(() => loading = false); } } From 682c0b3e10736746b8d7f73cc38b93e03fb87946 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 20 Nov 2020 18:03:33 +0100 Subject: [PATCH 096/101] swap user list with message list and create channel with draft: true --- example/lib/chips_input_text_field.dart | 157 +++++----- example/lib/main.dart | 382 ++++++++++++++---------- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 8 +- lib/src/message_list_view.dart | 19 ++ lib/src/url_attachment.dart | 6 +- 6 files changed, 339 insertions(+), 235 deletions(-) diff --git a/example/lib/chips_input_text_field.dart b/example/lib/chips_input_text_field.dart index 30183926..317a68f7 100644 --- a/example/lib/chips_input_text_field.dart +++ b/example/lib/chips_input_text_field.dart @@ -22,7 +22,7 @@ class ChipsInputTextField extends StatefulWidget { this.focusNode, this.onChipAdded, this.onChipRemoved, - this.hint = 'Type a name or group', + this.hint = 'Type a name', }) : super(key: key); @override @@ -61,83 +61,96 @@ class ChipInputTextFieldState extends State> { @override Widget build(BuildContext context) { - return Material( - elevation: 1, - color: Colors.white, - child: Container( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - 'TO:', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(0.5), + return GestureDetector( + onTap: _pauseItemAddition + ? () { + setState(() { + _pauseItemAddition = false; + widget.focusNode?.requestFocus(); + }); + } + : null, + child: Material( + elevation: 1, + color: Colors.white, + child: Container( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Text( + 'TO:', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + ), ), ), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: _chips.map((item) { - return widget.chipBuilder(context, item); - }).toList(), - ), - if (!_pauseItemAddition) - TextField( - controller: widget.controller, - onChanged: widget.onInputChanged, - focusNode: widget.focusNode, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.only(top: 4.0), - hintText: widget.hint, - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 14, + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _chips.map((item) { + return widget.chipBuilder(context, item); + }).toList(), + ), + if (!_pauseItemAddition) + TextField( + controller: widget.controller, + onChanged: widget.onInputChanged, + focusNode: widget.focusNode, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.only(top: 4.0), + hintText: widget.hint, + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 14, + ), ), ), - ), - ], - ), - ), - SizedBox(width: 12), - Align( - alignment: Alignment.bottomCenter, - child: IconButton( - icon: Icon( - _chips.isEmpty ? StreamIcons.user : StreamIcons.user_add, - color: Colors.black.withOpacity(0.5), - size: 24, - ), - onPressed: !_pauseItemAddition ? null : resumeItemAddition, - alignment: Alignment.topRight, - visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + ], ), ), - ), - ], + SizedBox(width: 12), + Align( + alignment: Alignment.bottomCenter, + child: IconButton( + icon: Icon( + _chips.isEmpty + ? StreamIcons.user + : StreamIcons.user_add, + color: Colors.black.withOpacity(0.5), + size: 24, + ), + onPressed: + !_pauseItemAddition ? null : resumeItemAddition, + alignment: Alignment.topRight, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ), + ], + ), ), ), ), diff --git a/example/lib/main.dart b/example/lib/main.dart index 70c11940..e2a8a5f7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -111,6 +111,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewChatScreen()), @@ -126,6 +127,7 @@ class ChannelListPage extends StatelessWidget { ), ListTile( onTap: () { + Navigator.pop(context); Navigator.push( context, MaterialPageRoute(builder: (_) => NewGroupChatScreen()), @@ -177,7 +179,10 @@ class ChannelListPage extends StatelessWidget { filter: { 'members': { '\$in': [user.id], - } + }, + 'draft': { + r'$ne': true, + }, }, options: { 'presence': true, @@ -283,12 +288,17 @@ class _NewChatScreenState extends State { final _selectedUsers = {}; + final _searchFocusNode = FocusNode(); + final _messageInputFocusNode = FocusNode(); + bool _isSearchActive = false; Channel channel; Timer _debounce; + bool _showUserList = true; + void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { @@ -305,10 +315,50 @@ class _NewChatScreenState extends State { super.initState(); channel = StreamChat.of(context).client.channel('messaging'); _controller = TextEditingController()..addListener(_userNameListener); + + _searchFocusNode.addListener(() async { + if (_searchFocusNode.hasFocus && !_showUserList) { + if (channel.extraData['draft'] == true) { + await channel.stopWatching(); + channel.dispose(); + channel.client.state.channels.remove(channel.cid); + } + setState(() { + _showUserList = true; + }); + } + }); + + _messageInputFocusNode.addListener(() async { + if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { + final chatState = StreamChat.of(context); + + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + 'draft': true, + }, + ); + + if (!chatState.client.state.channels.containsKey(channel.cid)) { + await channel.watch(); + } + + setState(() { + _showUserList = false; + }); + } + }); } @override void dispose() { + _searchFocusNode.dispose(); + _messageInputFocusNode.dispose(); _controller?.clear(); _controller?.removeListener(_userNameListener); _controller?.dispose(); @@ -335,81 +385,81 @@ class _NewChatScreenState extends State { body: StreamChannel( showLoading: false, channel: channel, - child: UsersBloc( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: FocusNode(), - chipBuilder: (context, user) { - return Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: _searchFocusNode, + chipBuilder: (context, user) { + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), ), ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), ), - ), - ], - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => NewGroupChatScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Color(0xFF006CFF), - ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), + ), + ], ), ), ), + ), + if (_showUserList) Container( width: double.maxFinite, decoration: BoxDecoration( @@ -438,97 +488,108 @@ class _NewChatScreenState extends State { ), ), ), - Expanded( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - _controller.clear(); - if (!_selectedUsers.contains(user)) { - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } else { - _chipInputTextFieldState.removeItem(user); - } - }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - } - }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, + Expanded( + child: _showUserList + ? UsersBloc( + child: UserListView( + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + _controller.clear(); + if (!_selectedUsers.contains(user)) { + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + }, + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No user matches these keywords...'), + ], ), ), - Text('No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - MessageInput( - preMessageSending: (message) async { - channel.extraData = { - 'members': [ - ..._selectedUsers.map((e) => e.id), - channel.client.state.user.id, - ], - }; - await channel.watch(); - return message; - }, - onMessageSent: (_) { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - }, - ), - ], - ), + ), + ); + }, + ); + }, + ), + ) + : MessageListView(), + ), + MessageInput( + focusNode: _messageInputFocusNode, + onMessageSent: (m) { + if (!m.isEphemeral) { + _updateChannelAndNavigate(context); + } else { + channel.on('message.new').first.then((_) { + _updateChannelAndNavigate(context); + }); + } + }, + ), + ], ), ), ); } + + void _updateChannelAndNavigate(BuildContext context) { + channel.update({ + 'draft': false, + }); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + } } class NewGroupChatScreen extends StatefulWidget { @@ -761,7 +822,10 @@ class _NewGroupChatScreenState extends State { if (_userNameQuery.isNotEmpty) 'name': { r'$autocomplete': _userNameQuery, - } + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + } }, sort: [ SortOption( @@ -832,9 +896,11 @@ class _GroupChatDetailsScreenState extends State { void _groupNameListener() { final name = _groupNameController.text; - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); + if (mounted) { + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } } @override diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e5c11897..0fe6a0ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.61+63 +version: 1.0.62+64 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 423d4c8f..6845d1a4 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -104,6 +104,7 @@ class MessageInput extends StatefulWidget { this.actions, this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, + this.focusNode, }) : super(key: key); /// Message to edit @@ -149,6 +150,9 @@ class MessageInput extends StatefulWidget { /// Map that defines a thumbnail builder for an attachment type final Map attachmentThumbnailBuilders; + /// The focus node associated to the TextField + final FocusNode focusNode; + @override MessageInputState createState() => MessageInputState(); @@ -169,10 +173,10 @@ class MessageInput extends StatefulWidget { class MessageInputState extends State { final List<_SendingAttachment> _attachments = []; - final _focusNode = FocusNode(); final List _mentionedUsers = []; final _imagePicker = ImagePicker(); + FocusNode _focusNode; bool _inputEnabled = true; bool _messageIsPresent = false; bool _animateContainer = true; @@ -1708,6 +1712,8 @@ class MessageInputState extends State { void initState() { super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + _emojiNames = Emoji.all().map((e) => e.name); if (!kIsWeb) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 2a60b75b..4a5e445b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -180,7 +180,26 @@ class _MessageListViewState extends State { ? streamChannel.channel.state.threads[widget.parentMessage.id] : streamChannel.channel.state.messages, builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), + ); + } + final messages = snapshot.data?.reversed?.toList() ?? []; + + if (messages.isEmpty) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); + } + return Stack( alignment: Alignment.center, children: [ diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart index a873217f..d735ad67 100644 --- a/lib/src/url_attachment.dart +++ b/lib/src/url_attachment.dart @@ -4,9 +4,9 @@ import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class UrlAttachment extends StatelessWidget { - Attachment urlAttachment; - String hostDisplayName; - EdgeInsets textPadding; + final Attachment urlAttachment; + final String hostDisplayName; + final EdgeInsets textPadding; UrlAttachment({ @required this.urlAttachment, From d7a8d4f3f9111fbacf40e26bda15cefc072cb20a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Sat, 21 Nov 2020 11:24:38 +0100 Subject: [PATCH 097/101] extract screens --- example/lib/group_chat_details_screen.dart | 221 ++++++ example/lib/main.dart | 822 +-------------------- example/lib/new_chat_screen.dart | 331 +++++++++ example/lib/new_group_chat_screen.dart | 285 +++++++ example/pubspec.yaml | 2 +- 5 files changed, 840 insertions(+), 821 deletions(-) create mode 100644 example/lib/group_chat_details_screen.dart create mode 100644 example/lib/new_chat_screen.dart create mode 100644 example/lib/new_group_chat_screen.dart diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart new file mode 100644 index 00000000..8ba8c71f --- /dev/null +++ b/example/lib/group_chat_details_screen.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'main.dart'; +import 'neumorphic_button.dart'; + +class GroupChatDetailsScreen extends StatefulWidget { + final List selectedUsers; + + const GroupChatDetailsScreen({ + Key key, + @required this.selectedUsers, + }) : super(key: key); + + @override + _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); +} + +class _GroupChatDetailsScreenState extends State { + final _selectedUsers = []; + + TextEditingController _groupNameController; + + Channel _channel; + + bool _isGroupNameEmpty = true; + + int get _totalUsers => _selectedUsers.length; + + void _groupNameListener() { + final name = _groupNameController.text; + if (mounted) { + setState(() { + _isGroupNameEmpty = name.isEmpty; + }); + } + } + + @override + void initState() { + super.initState(); + _channel = StreamChat.of(context).client.channel('messaging'); + _selectedUsers.addAll(widget.selectedUsers); + _groupNameController = TextEditingController() + ..addListener(_groupNameListener); + } + + @override + void dispose() { + _groupNameController?.clear(); + _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 1, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'Name of Group Chat', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + centerTitle: true, + bottom: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), + child: Row( + children: [ + Text( + 'NAME', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(0.5), + ), + ), + SizedBox(width: 16), + Expanded( + child: TextField( + controller: _groupNameController, + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.all(0), + hintText: 'Choose a group chat name', + hintStyle: TextStyle( + fontSize: 14, color: Colors.black.withOpacity(.5)), + ), + ), + ), + ], + ), + ), + ), + actions: [ + NeumorphicButton( + child: IconButton( + padding: const EdgeInsets.all(0), + icon: Icon(StreamIcons.check), + color: Color(0xFF006CFF), + onPressed: _isGroupNameEmpty + ? null + : () async { + final groupName = _groupNameController.text; + final client = _channel.client; + _channel.extraData = { + 'members': [ + client.state.user.id, + ..._selectedUsers.map((e) => e.id), + ], + 'name': groupName, + }; + await _channel.watch(); + Navigator.of(context) + ..pop() + ..pushReplacement( + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: _channel, + ); + }, + ), + ); + }, + ), + ), + ], + ), + body: Column( + children: [ + Container( + width: double.maxFinite, + color: Colors.grey.shade50, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', + style: TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + Expanded( + child: ListView.separated( + itemCount: _selectedUsers.length + 1, + separatorBuilder: (_, __) => Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ), + itemBuilder: (_, index) { + if (index == _selectedUsers.length) { + return Container( + height: 1, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), + ); + } + final user = _selectedUsers[index]; + return ListTile( + key: ObjectKey(user), + leading: UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + width: 40, + height: 40, + ), + ), + title: Text( + user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + trailing: IconButton( + icon: Icon( + Icons.clear_rounded, + color: Colors.black, + ), + padding: const EdgeInsets.all(0), + splashRadius: 24, + onPressed: () { + setState(() { + _selectedUsers.remove(user); + }); + if (_selectedUsers.isEmpty) { + Navigator.pop(context); + } + }, + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index e2a8a5f7..7b547659 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:example/choose_user_page.dart'; @@ -8,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'chips_input_text_field.dart'; -import 'neumorphic_button.dart'; +import 'new_chat_screen.dart'; +import 'new_group_chat_screen.dart'; import 'notifications_service.dart'; void main() async { @@ -269,820 +268,3 @@ class ThreadPage extends StatelessWidget { ); } } - -class NewChatScreen extends StatefulWidget { - @override - _NewChatScreenState createState() => _NewChatScreenState(); -} - -class _NewChatScreenState extends State { - final _chipInputTextFieldStateKey = - GlobalKey>(); - - TextEditingController _controller; - - ChipInputTextFieldState get _chipInputTextFieldState => - _chipInputTextFieldStateKey.currentState; - - String _userNameQuery = ''; - - final _selectedUsers = {}; - - final _searchFocusNode = FocusNode(); - final _messageInputFocusNode = FocusNode(); - - bool _isSearchActive = false; - - Channel channel; - - Timer _debounce; - - bool _showUserList = true; - - void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); - } - - @override - void initState() { - super.initState(); - channel = StreamChat.of(context).client.channel('messaging'); - _controller = TextEditingController()..addListener(_userNameListener); - - _searchFocusNode.addListener(() async { - if (_searchFocusNode.hasFocus && !_showUserList) { - if (channel.extraData['draft'] == true) { - await channel.stopWatching(); - channel.dispose(); - channel.client.state.channels.remove(channel.cid); - } - setState(() { - _showUserList = true; - }); - } - }); - - _messageInputFocusNode.addListener(() async { - if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { - final chatState = StreamChat.of(context); - - channel = chatState.client.channel( - 'messaging', - extraData: { - 'members': [ - ..._selectedUsers.map((e) => e.id), - chatState.user.id, - ], - 'draft': true, - }, - ); - - if (!chatState.client.state.channels.containsKey(channel.cid)) { - await channel.watch(); - } - - setState(() { - _showUserList = false; - }); - } - }); - } - - @override - void dispose() { - _searchFocusNode.dispose(); - _messageInputFocusNode.dispose(); - _controller?.clear(); - _controller?.removeListener(_userNameListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'New Chat', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - centerTitle: true, - ), - body: StreamChannel( - showLoading: false, - channel: channel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChipsInputTextField( - key: _chipInputTextFieldStateKey, - controller: _controller, - focusNode: _searchFocusNode, - chipBuilder: (context, user) { - return Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), - ), - ), - ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - ), - ], - ); - }, - onChipAdded: (user) { - setState(() => _selectedUsers.add(user)); - }, - onChipRemoved: (user) { - setState(() => _selectedUsers.remove(user)); - }, - ), - if (!_isSearchActive && !_selectedUsers.isNotEmpty) - Container( - child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => NewGroupChatScreen()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - children: [ - NeumorphicButton( - child: Icon( - StreamIcons.group, - color: Color(0xFF006CFF), - ), - ), - SizedBox(width: 8), - Text( - 'Create a Group', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ], - ), - ), - ), - ), - if (_showUserList) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? "Matches for \"$_userNameQuery\"" - : 'On the platform', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: _showUserList - ? UsersBloc( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - _controller.clear(); - if (!_selectedUsers.contains(user)) { - _chipInputTextFieldState - ..addItem(user) - ..pauseItemAddition(); - } else { - _chipInputTextFieldState.removeItem(user); - } - }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - }, - }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, - ), - ), - Text( - 'No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ) - : MessageListView(), - ), - MessageInput( - focusNode: _messageInputFocusNode, - onMessageSent: (m) { - if (!m.isEphemeral) { - _updateChannelAndNavigate(context); - } else { - channel.on('message.new').first.then((_) { - _updateChannelAndNavigate(context); - }); - } - }, - ), - ], - ), - ), - ); - } - - void _updateChannelAndNavigate(BuildContext context) { - channel.update({ - 'draft': false, - }); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: channel, - ); - }, - ), - ); - } -} - -class NewGroupChatScreen extends StatefulWidget { - @override - _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); -} - -class _NewGroupChatScreenState extends State { - TextEditingController _controller; - - String _userNameQuery = ''; - - final _selectedUsers = {}; - - bool _isSearchActive = false; - - Timer _debounce; - - void _userNameListener() { - if (_debounce?.isActive ?? false) _debounce.cancel(); - _debounce = Timer(const Duration(milliseconds: 350), () { - if (mounted) - setState(() { - _userNameQuery = _controller.text; - _isSearchActive = _userNameQuery.isNotEmpty; - }); - }); - } - - @override - void initState() { - super.initState(); - _controller = TextEditingController()..addListener(_userNameListener); - } - - @override - void dispose() { - _controller?.clear(); - _controller?.removeListener(_userNameListener); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 1, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'Add Group Members', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - centerTitle: true, - actions: [ - if (_selectedUsers.isNotEmpty) - IconButton( - icon: Icon( - StreamIcons.arrow_right, - color: Color(0xFF006CFF), - ), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => GroupChatDetailsScreen( - selectedUsers: _selectedUsers.toList(growable: false), - ), - ), - ); - }, - ) - ], - ), - body: UsersBloc( - child: Column( - children: [ - Container( - height: 36, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all( - color: Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular(24), - ), - margin: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: TextField( - controller: _controller, - decoration: InputDecoration( - prefixIcon: Icon( - StreamIcons.search, - color: Colors.black, - size: 24, - ), - hintText: 'Search', - hintStyle: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 14, - ), - contentPadding: const EdgeInsets.all(0), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(24), - ), - ), - ), - ), - if (_selectedUsers.isNotEmpty) - Container( - height: 104, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: _selectedUsers.length, - padding: const EdgeInsets.all(8), - separatorBuilder: (_, __) => SizedBox(width: 16), - itemBuilder: (_, index) { - final user = _selectedUsers.elementAt(index); - return Column( - children: [ - Stack( - children: [ - UserAvatar( - onlineIndicatorAlignment: Alignment(0.9, 0.9), - user: user, - showOnlineStatus: true, - borderRadius: BorderRadius.circular(32), - constraints: BoxConstraints.tightFor( - height: 64, - width: 64, - ), - ), - Positioned( - top: -4, - right: -4, - child: GestureDetector( - onTap: () { - if (_selectedUsers.contains(user)) { - setState(() => _selectedUsers.remove(user)); - } - }, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - border: Border.all( - color: Colors.grey.shade100, - ), - ), - child: Padding( - padding: const EdgeInsets.all(0.0), - child: Icon( - StreamIcons.close, - size: 24, - ), - ), - ), - ), - ) - ], - ), - SizedBox(height: 4), - Text( - user.name.split(' ')[0], - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ], - ); - }, - ), - ), - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - _isSearchActive - ? 'Matches for \"$_userNameQuery\"' - : 'On the platform', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: UserListView( - selectedUsers: _selectedUsers, - groupAlphabetically: _isSearchActive ? false : true, - onUserTap: (user, _) { - if (!_selectedUsers.contains(user)) { - setState(() { - _selectedUsers.add(user); - }); - } else { - setState(() { - _selectedUsers.remove(user); - }); - } - }, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_userNameQuery.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, - } - }, - sort: [ - SortOption( - 'name', - direction: 1, - ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: Icon( - StreamIcons.search, - size: 96, - color: Colors.grey, - ), - ), - Text('No user matches these keywords...'), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -class GroupChatDetailsScreen extends StatefulWidget { - final List selectedUsers; - - const GroupChatDetailsScreen({ - Key key, - @required this.selectedUsers, - }) : super(key: key); - - @override - _GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState(); -} - -class _GroupChatDetailsScreenState extends State { - final _selectedUsers = []; - - TextEditingController _groupNameController; - - Channel _channel; - - bool _isGroupNameEmpty = true; - - int get _totalUsers => _selectedUsers.length; - - void _groupNameListener() { - final name = _groupNameController.text; - if (mounted) { - setState(() { - _isGroupNameEmpty = name.isEmpty; - }); - } - } - - @override - void initState() { - super.initState(); - _channel = StreamChat.of(context).client.channel('messaging'); - _selectedUsers.addAll(widget.selectedUsers); - _groupNameController = TextEditingController() - ..addListener(_groupNameListener); - } - - @override - void dispose() { - _groupNameController?.clear(); - _groupNameController?.removeListener(_groupNameListener); - _groupNameController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Color.fromRGBO(252, 252, 252, 1), - appBar: AppBar( - elevation: 1, - backgroundColor: Colors.white, - leading: const StreamBackButton(), - title: Text( - 'Name of Group Chat', - style: TextStyle( - color: Colors.black, - fontSize: 16, - ), - ), - centerTitle: true, - bottom: PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), - child: Row( - children: [ - Text( - 'NAME', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(0.5), - ), - ), - SizedBox(width: 16), - Expanded( - child: TextField( - controller: _groupNameController, - decoration: InputDecoration( - isDense: true, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: const EdgeInsets.all(0), - hintText: 'Choose a group chat name', - hintStyle: TextStyle( - fontSize: 14, color: Colors.black.withOpacity(.5)), - ), - ), - ), - ], - ), - ), - ), - actions: [ - NeumorphicButton( - child: IconButton( - padding: const EdgeInsets.all(0), - icon: Icon(StreamIcons.check), - color: Color(0xFF006CFF), - onPressed: _isGroupNameEmpty - ? null - : () async { - final groupName = _groupNameController.text; - final client = _channel.client; - _channel.extraData = { - 'members': [ - client.state.user.id, - ..._selectedUsers.map((e) => e.id), - ], - 'name': groupName, - }; - await _channel.watch(); - Navigator.of(context) - ..pop() - ..pushReplacement( - MaterialPageRoute( - builder: (context) { - return StreamChannel( - child: ChannelPage(), - channel: _channel, - ); - }, - ), - ); - }, - ), - ), - ], - ), - body: Column( - children: [ - Container( - width: double.maxFinite, - color: Colors.grey.shade50, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', - style: TextStyle( - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - child: ListView.separated( - itemCount: _selectedUsers.length + 1, - separatorBuilder: (_, __) => Container( - height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - ), - itemBuilder: (_, index) { - if (index == _selectedUsers.length) { - return Container( - height: 1, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.1) - : Colors.black.withOpacity(0.1), - ); - } - final user = _selectedUsers[index]; - return ListTile( - key: ObjectKey(user), - leading: UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - width: 40, - height: 40, - ), - ), - title: Text( - user.name, - style: TextStyle(fontWeight: FontWeight.bold), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - trailing: IconButton( - icon: Icon( - Icons.clear_rounded, - color: Colors.black, - ), - padding: const EdgeInsets.all(0), - splashRadius: 24, - onPressed: () { - setState(() { - _selectedUsers.remove(user); - }); - if (_selectedUsers.isEmpty) { - Navigator.pop(context); - } - }, - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart new file mode 100644 index 00000000..286f9a36 --- /dev/null +++ b/example/lib/new_chat_screen.dart @@ -0,0 +1,331 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'chips_input_text_field.dart'; +import 'main.dart'; +import 'neumorphic_button.dart'; +import 'new_group_chat_screen.dart'; + +class NewChatScreen extends StatefulWidget { + @override + _NewChatScreenState createState() => _NewChatScreenState(); +} + +class _NewChatScreenState extends State { + final _chipInputTextFieldStateKey = + GlobalKey>(); + + TextEditingController _controller; + + ChipInputTextFieldState get _chipInputTextFieldState => + _chipInputTextFieldStateKey.currentState; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + final _searchFocusNode = FocusNode(); + final _messageInputFocusNode = FocusNode(); + + bool _isSearchActive = false; + + Channel channel; + + Timer _debounce; + + bool _showUserList = true; + + void _userNameListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void initState() { + super.initState(); + channel = StreamChat.of(context).client.channel('messaging'); + _controller = TextEditingController()..addListener(_userNameListener); + + _searchFocusNode.addListener(() async { + if (_searchFocusNode.hasFocus && !_showUserList) { + if (channel.extraData['draft'] == true) { + await channel.stopWatching(); + channel.dispose(); + channel.client.state.channels.remove(channel.cid); + } + setState(() { + _showUserList = true; + }); + } + }); + + _messageInputFocusNode.addListener(() async { + if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { + final chatState = StreamChat.of(context); + + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + 'draft': true, + }, + ); + + if (!chatState.client.state.channels.containsKey(channel.cid)) { + await channel.watch(); + } + + setState(() { + _showUserList = false; + }); + } + }); + } + + @override + void dispose() { + _searchFocusNode.dispose(); + _messageInputFocusNode.dispose(); + _controller?.clear(); + _controller?.removeListener(_userNameListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'New Chat', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + centerTitle: true, + ), + body: StreamChannel( + showLoading: false, + channel: channel, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChipsInputTextField( + key: _chipInputTextFieldStateKey, + controller: _controller, + focusNode: _searchFocusNode, + chipBuilder: (context, user) { + return Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), + ), + ), + UserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), + ], + ); + }, + onChipAdded: (user) { + setState(() => _selectedUsers.add(user)); + }, + onChipRemoved: (user) { + setState(() => _selectedUsers.remove(user)); + }, + ), + if (!_isSearchActive && !_selectedUsers.isNotEmpty) + Container( + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => NewGroupChatScreen()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + NeumorphicButton( + child: Icon( + StreamIcons.group, + color: Color(0xFF006CFF), + ), + ), + SizedBox(width: 8), + Text( + 'Create a Group', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + if (_showUserList) + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? "Matches for \"$_userNameQuery\"" + : 'On the platform', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded( + child: _showUserList + ? UsersBloc( + child: UserListView( + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + _controller.clear(); + if (!_selectedUsers.contains(user)) { + _chipInputTextFieldState + ..addItem(user) + ..pauseItemAddition(); + } else { + _chipInputTextFieldState.removeItem(user); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + }, + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text( + 'No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ) + : MessageListView(), + ), + MessageInput( + focusNode: _messageInputFocusNode, + onMessageSent: (m) { + if (!m.isEphemeral) { + _updateChannelAndNavigate(context); + } else { + channel.on('message.new').first.then((_) { + _updateChannelAndNavigate(context); + }); + } + }, + ), + ], + ), + ), + ); + } + + void _updateChannelAndNavigate(BuildContext context) { + channel.update({ + 'draft': false, + }); + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) { + return StreamChannel( + child: ChannelPage(), + channel: channel, + ); + }, + ), + ); + } +} diff --git a/example/lib/new_group_chat_screen.dart b/example/lib/new_group_chat_screen.dart new file mode 100644 index 00000000..b75e7510 --- /dev/null +++ b/example/lib/new_group_chat_screen.dart @@ -0,0 +1,285 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'group_chat_details_screen.dart'; + +class NewGroupChatScreen extends StatefulWidget { + @override + _NewGroupChatScreenState createState() => _NewGroupChatScreenState(); +} + +class _NewGroupChatScreenState extends State { + TextEditingController _controller; + + String _userNameQuery = ''; + + final _selectedUsers = {}; + + bool _isSearchActive = false; + + Timer _debounce; + + void _userNameListener() { + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted) + setState(() { + _userNameQuery = _controller.text; + _isSearchActive = _userNameQuery.isNotEmpty; + }); + }); + } + + @override + void initState() { + super.initState(); + _controller = TextEditingController()..addListener(_userNameListener); + } + + @override + void dispose() { + _controller?.clear(); + _controller?.removeListener(_userNameListener); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), + appBar: AppBar( + elevation: 1, + backgroundColor: Colors.white, + leading: const StreamBackButton(), + title: Text( + 'Add Group Members', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + centerTitle: true, + actions: [ + if (_selectedUsers.isNotEmpty) + IconButton( + icon: Icon( + StreamIcons.arrow_right, + color: Color(0xFF006CFF), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => GroupChatDetailsScreen( + selectedUsers: _selectedUsers.toList(growable: false), + ), + ), + ); + }, + ) + ], + ), + body: UsersBloc( + child: Column( + children: [ + Container( + height: 36, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.shade300, + ), + borderRadius: BorderRadius.circular(24), + ), + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: TextField( + controller: _controller, + decoration: InputDecoration( + prefixIcon: Icon( + StreamIcons.search, + color: Colors.black, + size: 24, + ), + hintText: 'Search', + hintStyle: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 14, + ), + contentPadding: const EdgeInsets.all(0), + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(24), + ), + ), + ), + ), + if (_selectedUsers.isNotEmpty) + Container( + height: 104, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: _selectedUsers.length, + padding: const EdgeInsets.all(8), + separatorBuilder: (_, __) => SizedBox(width: 16), + itemBuilder: (_, index) { + final user = _selectedUsers.elementAt(index); + return Column( + children: [ + Stack( + children: [ + UserAvatar( + onlineIndicatorAlignment: Alignment(0.9, 0.9), + user: user, + showOnlineStatus: true, + borderRadius: BorderRadius.circular(32), + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), + ), + Positioned( + top: -4, + right: -4, + child: GestureDetector( + onTap: () { + if (_selectedUsers.contains(user)) { + setState(() => _selectedUsers.remove(user)); + } + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey.shade100, + ), + ), + child: Padding( + padding: const EdgeInsets.all(0.0), + child: Icon( + StreamIcons.close, + size: 24, + ), + ), + ), + ), + ) + ], + ), + SizedBox(height: 4), + Text( + user.name.split(' ')[0], + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ); + }, + ), + ), + Container( + width: double.maxFinite, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Colors.black.withOpacity(0.02), + Colors.white.withOpacity(0.05), + ], + stops: [0, 1], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: Text( + _isSearchActive + ? 'Matches for \"$_userNameQuery\"' + : 'On the platform', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + ), + ), + ), + ), + Expanded( + child: UserListView( + selectedUsers: _selectedUsers, + groupAlphabetically: _isSearchActive ? false : true, + onUserTap: (user, _) { + if (!_selectedUsers.contains(user)) { + setState(() { + _selectedUsers.add(user); + }); + } else { + setState(() { + _selectedUsers.remove(user); + }); + } + }, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_userNameQuery.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + } + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + StreamIcons.search, + size: 96, + color: Colors.grey, + ), + ), + Text('No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0fe6a0ad..9ded2435 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.62+64 +version: 1.0.63+65 environment: sdk: ">=2.2.2 <3.0.0" From 4b317d719c80e326a4e641234315b574860280c1 Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Mon, 23 Nov 2020 12:57:20 +0530 Subject: [PATCH 098/101] Refactor UserListView to support (crossAxisCount > 1) i.e: GridViews --- lib/src/user_avatar.dart | 75 ++++++++++++++++++----------- lib/src/user_list_view.dart | 94 ++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 34 deletions(-) diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 566ef603..2e1f3e0e 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -11,9 +11,13 @@ class UserAvatar extends StatelessWidget { this.constraints, this.onlineIndicatorConstraints, this.onTap, + this.onLongPress, this.showOnlineStatus = true, this.borderRadius, this.onlineIndicatorAlignment = Alignment.topRight, + this.selected = false, + this.selectionColor = const Color(0xFF006CFF), + this.selectionThickness = 4, }) : super(key: key); final User user; @@ -22,7 +26,11 @@ class UserAvatar extends StatelessWidget { final BorderRadius borderRadius; final BoxConstraints onlineIndicatorConstraints; final void Function(User) onTap; + final void Function(User) onLongPress; final bool showOnlineStatus; + final bool selected; + final Color selectionColor; + final double selectionThickness; @override Widget build(BuildContext context) { @@ -30,37 +38,48 @@ class UserAvatar extends StatelessWidget { user.extraData['image'] != null && user.extraData['image'] != ''; final streamChatTheme = StreamChatTheme.of(context); + + Widget avatar = ClipRRect( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius, + child: Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme.constraints, + decoration: BoxDecoration( + color: streamChatTheme.accentColor, + ), + child: hasImage + ? CachedNetworkImage( + filterQuality: FilterQuality.high, + imageUrl: user.extraData['image'], + errorWidget: (_, __, ___) { + return streamChatTheme.defaultUserImage(context, user); + }, + fit: BoxFit.cover, + ) + : streamChatTheme.defaultUserImage(context, user), + ), + ); + if (selected) { + avatar = ClipRRect( + borderRadius: (borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + color: selectionColor, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: avatar, + ), + ), + ); + } return GestureDetector( - onTap: onTap != null - ? () { - if (onTap != null) { - onTap(user); - } - } - : null, + onTap: onTap != null ? () => onTap(user) : null, + onLongPress: onLongPress != null ? () => onLongPress(user) : 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( - filterQuality: FilterQuality.high, - imageUrl: user.extraData['image'], - errorWidget: (_, __, ___) { - return streamChatTheme.defaultUserImage(context, user); - }, - fit: BoxFit.cover, - ) - : streamChatTheme.defaultUserImage(context, user), - ), - ), + avatar, if (showOnlineStatus && user.online == true) Positioned.fill( child: Align( diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 0f7b969e..51c645c3 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -65,7 +65,12 @@ class UserListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.groupAlphabetically = false, - }) : super(key: key); + this.crossAxisCount = 1, + }) : assert( + crossAxisCount == 1 || groupAlphabetically == false, + 'Cannot group alphabetically when crossAxisCount > 1', + ), + super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -130,6 +135,8 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + final int crossAxisCount; + @override _UserListViewState createState() => _UserListViewState(); } @@ -138,6 +145,8 @@ class _UserListViewState extends State with WidgetsBindingObserver { final ScrollController _scrollController = ScrollController(); + bool get _isListView => widget.crossAxisCount == 1; + @override void initState() { super.initState(); @@ -317,19 +326,40 @@ class _UserListViewState extends State ); } - return ListView.custom( + if (_isListView) { + return ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _listItemBuilder(context, i, items); + }, + childCount: (items.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = + items.indexWhere((item) => item.key == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + } + return GridView.custom( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + ), physics: AlwaysScrollableScrollPhysics(), controller: _scrollController, childrenDelegate: SliverChildBuilderDelegate( (context, i) { - return _itemBuilder(context, i, items); + return _gridItemBuilder(context, i, items); }, - childCount: (items.length * 2) + 1, + childCount: items.length, findChildIndexCallback: (key) { final ValueKey valueKey = key; final index = items.indexWhere((item) => item.key == valueKey.value); - return index != -1 ? (index * 2) : null; + return index != -1 ? index : null; }, ), ); @@ -337,7 +367,7 @@ class _UserListViewState extends State ); } - Widget _itemBuilder(BuildContext context, int i, List items) { + Widget _listItemBuilder(BuildContext context, int i, List items) { if (i % 2 != 0) { if (widget.separatorBuilder != null) { return widget.separatorBuilder(context, i); @@ -388,6 +418,58 @@ class _UserListViewState extends State } } + Widget _gridItemBuilder(BuildContext context, int i, List items) { + final usersProvider = UsersBloc.of(context); + if (i < items.length) { + final item = items[i]; + return item.when( + headerItem: (_) => Offstage(), + userItem: (user) { + final selected = widget.selectedUsers?.contains(user) ?? false; + return Container( + key: ValueKey('USER-${user.id}'), + child: widget.userItemBuilder != null + ? widget.userItemBuilder(context, user, selected) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + UserAvatar( + user: user, + borderRadius: BorderRadius.circular(32), + selected: selected, + constraints: BoxConstraints.tightFor( + height: 64, + width: 64, + ), + onTap: (user) => + widget.onUserTap(user, widget.userWidget), + onLongPress: widget.onUserLongPress, + ), + SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + user.name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + ); + }, + ); + } else { + return _buildQueryProgressIndicator(context, usersProvider); + } + } + Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) { return StreamBuilder( stream: usersProvider.queryUsersLoading, From 06037acede27eb596fd5f2dd095dbba3f01413ce Mon Sep 17 00:00:00 2001 From: xsahil03x Date: Mon, 23 Nov 2020 13:48:47 +0530 Subject: [PATCH 099/101] [UserListView] Add docComment --- lib/src/user_list_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 51c645c3..63285d8b 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -135,6 +135,7 @@ class UserListView extends StatefulWidget { /// defaults to false final bool groupAlphabetically; + /// The number of children in the cross axis. final int crossAxisCount; @override From e87c94586386cea4bf3c8ae006941486ee715e1a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 23 Nov 2020 10:02:30 +0100 Subject: [PATCH 100/101] add id for new groups --- example/lib/group_chat_details_screen.dart | 17 ++++++++--------- example/pubspec.yaml | 3 ++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index 8ba8c71f..6aff5711 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:uuid/uuid.dart'; import 'main.dart'; import 'neumorphic_button.dart'; @@ -21,8 +22,6 @@ class _GroupChatDetailsScreenState extends State { TextEditingController _groupNameController; - Channel _channel; - bool _isGroupNameEmpty = true; int get _totalUsers => _selectedUsers.length; @@ -39,7 +38,6 @@ class _GroupChatDetailsScreenState extends State { @override void initState() { super.initState(); - _channel = StreamChat.of(context).client.channel('messaging'); _selectedUsers.addAll(widget.selectedUsers); _groupNameController = TextEditingController() ..addListener(_groupNameListener); @@ -47,8 +45,8 @@ class _GroupChatDetailsScreenState extends State { @override void dispose() { - _groupNameController?.clear(); _groupNameController?.removeListener(_groupNameListener); + _groupNameController?.clear(); _groupNameController?.dispose(); super.dispose(); } @@ -114,15 +112,16 @@ class _GroupChatDetailsScreenState extends State { ? null : () async { final groupName = _groupNameController.text; - final client = _channel.client; - _channel.extraData = { + final client = StreamChat.of(context).client; + final channel = client + .channel('messaging', id: Uuid().v4(), extraData: { 'members': [ client.state.user.id, ..._selectedUsers.map((e) => e.id), ], 'name': groupName, - }; - await _channel.watch(); + }); + await channel.watch(); Navigator.of(context) ..pop() ..pushReplacement( @@ -130,7 +129,7 @@ class _GroupChatDetailsScreenState extends State { builder: (context) { return StreamChannel( child: ChannelPage(), - channel: _channel, + channel: channel, ); }, ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9ded2435..574345c2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.63+65 +version: 1.0.64+66 environment: sdk: ">=2.2.2 <3.0.0" @@ -15,6 +15,7 @@ dependencies: flutter_svg: ^0.19.1 flutter_secure_storage: ^3.3.5 yaml: ^2.2.1 + uuid: ^2.2.2 dev_dependencies: flutter_test: From 37891ae3c53cb9ff2ca2a051770cabce541f8c0e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 23 Nov 2020 10:30:58 +0100 Subject: [PATCH 101/101] remove chips tapping on them --- example/lib/new_chat_screen.dart | 59 ++++++++++++++++++++------------ example/pubspec.yaml | 2 +- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 286f9a36..9ec2ca7e 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -131,31 +131,46 @@ class _NewChatScreenState extends State { controller: _controller, focusNode: _searchFocusNode, chipBuilder: (context, user) { - return Stack( - alignment: AlignmentDirectional.centerStart, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.only(left: 24), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), - child: Text( - user.name, - style: TextStyle(color: Colors.black), + return GestureDetector( + onTap: () { + _chipInputTextFieldState.removeItem(user); + }, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.only(left: 24), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), + child: Text( + user.name, + style: TextStyle(color: Colors.black), + ), ), ), - ), - UserAvatar( - user: user, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + Opacity( + opacity: .8, + child: UserAvatar( + showOnlineStatus: false, + user: user, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + ), ), - ), - ], + Positioned( + child: Icon( + StreamIcons.close, + color: Colors.white, + ), + ), + ], + ), ); }, onChipAdded: (user) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 574345c2..b491fb92 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.64+66 +version: 1.0.65+67 environment: sdk: ">=2.2.2 <3.0.0"