From 412c721ab8abe5274ccd901902f4294053da5c56 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 19 Nov 2020 21:01:17 +0530 Subject: [PATCH] feat: Added sharing and added actions for buttons --- lib/src/full_screen_image.dart | 4 + lib/src/giphy_attachment.dart | 36 ++- lib/src/image_actions_modal.dart | 60 ++-- lib/src/image_attachment.dart | 18 +- lib/src/image_footer.dart | 533 +++++++++++++++++-------------- lib/src/image_group.dart | 15 +- lib/src/image_header.dart | 15 +- 7 files changed, 397 insertions(+), 284 deletions(-) diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart index 0001585f..ca302790 100644 --- a/lib/src/full_screen_image.dart +++ b/lib/src/full_screen_image.dart @@ -11,6 +11,7 @@ import '../stream_chat_flutter.dart'; class FullScreenImage extends StatefulWidget { /// The url of the image final List urls; + final Message message; final int startIndex; final String userName; @@ -20,6 +21,7 @@ class FullScreenImage extends StatefulWidget { const FullScreenImage({ Key key, @required this.urls, + this.message, this.startIndex = 0, this.userName = '', this.sentAt, @@ -109,11 +111,13 @@ class _FullScreenImageState extends State onBackPressed: () { Navigator.of(context).pop(); }, + message: widget.message, ), ImageFooter( currentPage: _currentPage, totalPages: widget.urls.length, urls: widget.urls, + message: widget.message, ), ], ), diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index f99ccb27..e5464987 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -64,14 +64,20 @@ class GiphyAttachment extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: GestureDetector( onTap: () { + final channel = StreamChannel.of(context).channel; + Navigator.push(context, MaterialPageRoute(builder: (_) { - return FullScreenImage( - urls: [attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl], - userName: message.user.name, - sentAt: message.createdAt, + return StreamChannel( + channel: channel, + child: FullScreenImage( + urls: [attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, + message: message, + ), ); })); }, @@ -299,13 +305,19 @@ class GiphyAttachment extends StatelessWidget { clipBehavior: Clip.antiAlias, child: GestureDetector( onTap: () { + final channel = StreamChannel.of(context).channel; + Navigator.push(context, MaterialPageRoute(builder: (_) { - return FullScreenImage( - urls: [attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl], - userName: message.user.name, - sentAt: message.createdAt, + return StreamChannel( + channel: channel, + child: FullScreenImage( + urls: [attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, + message: message, + ), ); })); }, diff --git a/lib/src/image_actions_modal.dart b/lib/src/image_actions_modal.dart index 03603795..8b39737d 100644 --- a/lib/src/image_actions_modal.dart +++ b/lib/src/image_actions_modal.dart @@ -6,10 +6,11 @@ import '../stream_chat_flutter.dart'; import 'stream_icons.dart'; class ImageActionsModal extends StatelessWidget { + final Message message; final String userName; final String sentAt; - ImageActionsModal({this.userName, this.sentAt}); + ImageActionsModal({this.message, this.userName, this.sentAt}); @override Widget build(BuildContext context) { @@ -21,20 +22,18 @@ class ImageActionsModal extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: Container( - decoration: BoxDecoration( + child: Container( + decoration: BoxDecoration( gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.8), - Colors.transparent, - ], - stops: [0.0, 0.4], - ) - ), - ) - ), + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.8), + Colors.transparent, + ], + stops: [0.0, 0.4], + )), + )), _buildPage(context), ], ), @@ -70,8 +69,10 @@ class ImageActionsModal extends StatelessWidget { ), Text( sentAt, - style: - StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(color: Colors.white.withOpacity(0.5)), + style: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + .copyWith(color: Colors.white.withOpacity(0.5)), ), ], ), @@ -104,11 +105,26 @@ class ImageActionsModal extends StatelessWidget { children: ListTile.divideTiles( context: context, tiles: [ - _buildButton(context, 'Reply', StreamIcons.curve_line_down_left, () {}), - _buildButton(context, 'Show in Chat', StreamIcons.eye, () {}), - _buildButton(context, 'Save Image', StreamIcons.save_1, () {}), + _buildButton(context, 'Reply', + StreamIcons.curve_line_down_left, () {}), + _buildButton(context, 'Show in Chat', StreamIcons.eye, + () { + Navigator.pop(context); + Navigator.pop(context); + }), + _buildButton( + context, 'Save Image', StreamIcons.save_1, () {}), _buildButton(context, 'Copy', StreamIcons.copy, () {}), - _buildButton(context, 'Delete', StreamIcons.delete, () {}, color: Colors.red), + if (StreamChat.of(context).user.id == message.user.id) + _buildButton(context, 'Delete', StreamIcons.delete, + () { + Navigator.pop(context); + Navigator.pop(context); + StreamChat.of(context).client.deleteMessage( + message, + StreamChannel.of(context).channel.cid, + ); + }, color: Colors.red), ], ).toList(), ), @@ -121,7 +137,9 @@ class ImageActionsModal extends StatelessWidget { ); } - Widget _buildButton(context, String title, IconData iconData, VoidCallback onTap, {Color color}) { + Widget _buildButton( + context, String title, IconData iconData, VoidCallback onTap, + {Color color}) { var titleStyle = TextStyle( fontSize: 14.5, color: Colors.black, diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index d493fa66..b429e064 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -43,12 +43,18 @@ class ImageAttachment extends StatelessWidget { context, MaterialPageRoute( builder: (_) { - return FullScreenImage( - urls: [attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl], - userName: message.user.name, - sentAt: message.createdAt, + final channel = StreamChannel.of(context).channel; + + return StreamChannel( + channel: channel, + child: FullScreenImage( + urls: [attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl], + userName: message.user.name, + sentAt: message.createdAt, + message: message, + ), ); }, ), diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index 1d74d017..02a8a6ef 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_icons.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import './channel_name.dart'; import 'channel_image.dart'; @@ -31,6 +32,7 @@ class ImageFooter extends StatefulWidget { final int totalPages; final List urls; + final Message message; /// Creates a channel header ImageFooter({ @@ -41,6 +43,7 @@ class ImageFooter extends StatefulWidget { this.currentPage = 0, this.totalPages = 0, this.urls, + this.message, }) : preferredSize = Size.fromHeight(kToolbarHeight), super(key: key); @@ -57,7 +60,7 @@ class _ImageFooterState extends State { TextEditingController _searchController = TextEditingController(); TextEditingController _messageController = TextEditingController(); - List selectedUsers = []; + List _selectedUsers = []; @override void initState() { @@ -134,8 +137,8 @@ class _ImageFooterState extends State { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( - topRight: Radius.circular(8.0), - topLeft: Radius.circular(8.0), + topRight: Radius.circular(16.0), + topLeft: Radius.circular(16.0), )), child: Stack( children: [ @@ -196,184 +199,197 @@ class _ImageFooterState extends State { } Widget _buildShareModal(context) { + final channel = StreamChannel.of(context).channel; + showDialog( context: context, builder: (context) { - print('KEY ${MediaQuery.of(context).viewInsets.bottom}'); - print(MediaQuery.of(context).viewInsets.bottom == 0.0); - return Scaffold( - backgroundColor: Colors.transparent, - body: StatefulBuilder(builder: (modalContext, modalSetState) { - return Align( - alignment: - _userSearchMode ? Alignment.topCenter : Alignment.bottomCenter, - child: Material( - color: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - child: Stack( - children: [ - ListView( - children: [ - if (!_userSearchMode && MediaQuery.of(context).viewInsets.bottom == 0.0) - GestureDetector( - child: Container( - height: MediaQuery.of(modalContext).size.height / 2, - color: Colors.transparent, + return StreamChannel( + channel: channel, + child: Scaffold( + backgroundColor: Colors.transparent, + body: StatefulBuilder(builder: (modalContext, modalSetState) { + return Align( + alignment: _userSearchMode + ? Alignment.topCenter + : Alignment.bottomCenter, + child: Material( + color: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + child: Stack( + children: [ + ListView( + children: [ + if (!_userSearchMode && + MediaQuery.of(context).viewInsets.bottom == 0.0) + GestureDetector( + child: Container( + height: + MediaQuery.of(modalContext).size.height / 2, + color: Colors.transparent, + ), + onTapUp: (val) { + Navigator.pop(modalContext); + }, ), - onTapUp: (val) { - Navigator.pop(modalContext); - }, + Container( + margin: EdgeInsets.symmetric( + horizontal: 8.0, + ), + padding: EdgeInsets.symmetric( + vertical: 4.0, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(16.0), + topLeft: Radius.circular(16.0), + )), + child: _buildTextInputSection(modalSetState), ), - Container( - margin: EdgeInsets.symmetric( - horizontal: 8.0, - ), - padding: EdgeInsets.symmetric( - vertical: 4.0, - ), - decoration: BoxDecoration( + Container( + margin: EdgeInsets.symmetric( + horizontal: 8.0, + ), + decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( - topRight: Radius.circular(16.0), - topLeft: Radius.circular(16.0), - )), - child: _buildTextInputSection(modalSetState), - ), - Container( - margin: EdgeInsets.symmetric( - horizontal: 8.0, - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomRight: - Radius.circular(_userSearchMode ? 16.0 : 0.0), - bottomLeft: - Radius.circular(_userSearchMode ? 16.0 : 0.0), + bottomRight: Radius.circular( + _userSearchMode ? 16.0 : 0.0), + bottomLeft: Radius.circular( + _userSearchMode ? 16.0 : 0.0), + ), ), - ), - child: FutureBuilder( - future: _userQuery, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - var filteredData = snapshot.data.users.toList(); - - if (_userSearchMode && - _searchController.text.length > 0) { - filteredData = snapshot.data.users - .where((element) => element.name - .toLowerCase() - .contains(_searchController.text - .toLowerCase())) - .toList(); - } - - return GridView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, position) { - var user = filteredData[position]; - var isUserSelected = - selectedUsers.contains(user); - - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.start, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Stack( - children: [ - CircleAvatar( - maxRadius: 32.0, - child: UserAvatar( - onTap: (user) { - if (isUserSelected) { - selectedUsers.remove(user); - } else { - selectedUsers.add(user); - } - modalSetState(() {}); - }, - user: user, - constraints: - BoxConstraints.tightFor( - height: - isUserSelected ? 56 : 64, - width: - isUserSelected ? 56 : 64, - ), - borderRadius: - BorderRadius.circular(32), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - user.name, - style: Theme.of(context) - .textTheme - .subtitle2, - textAlign: TextAlign.center, - maxLines: 2, - ), - ), - ], - ), + child: FutureBuilder( + future: _userQuery, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator(), ); - }, - itemCount: filteredData.length, - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 0.75, - ), - ); - }), - ), - ], - ), - if (!_userSearchMode && selectedUsers.isEmpty) - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: Colors.white, - height: 48.0, - margin: EdgeInsets.symmetric( - horizontal: 8.0, + } + + var filteredData = + snapshot.data.users.toList(); + + if (_userSearchMode && + _searchController.text.length > 0) { + filteredData = snapshot.data.users + .where((element) => element.name + .toLowerCase() + .contains(_searchController.text + .toLowerCase())) + .toList(); + } + + return GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, position) { + var user = filteredData[position]; + var isUserSelected = + _selectedUsers.contains(user); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Stack( + children: [ + CircleAvatar( + maxRadius: 32.0, + child: UserAvatar( + onTap: (user) { + if (isUserSelected) { + _selectedUsers + .remove(user); + } else { + _selectedUsers.add(user); + } + modalSetState(() {}); + }, + user: user, + constraints: + BoxConstraints.tightFor( + height: isUserSelected + ? 56 + : 64, + width: isUserSelected + ? 56 + : 64, + ), + borderRadius: + BorderRadius.circular( + 32), + ), + ), + ], + ), + Padding( + padding: + const EdgeInsets.all(8.0), + child: Text( + user.name, + style: Theme.of(context) + .textTheme + .subtitle2, + textAlign: TextAlign.center, + maxLines: 2, + ), + ), + ], + ), + ); + }, + itemCount: filteredData.length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.75, + ), + ); + }), ), - child: SizedBox.expand( - child: Center( - child: Text( - 'Save to Photos', - style: TextStyle( - color: StreamChatTheme.of(modalContext).accentColor, - fontWeight: FontWeight.bold, + ], + ), + if (!_userSearchMode && _selectedUsers.isEmpty) + Align( + alignment: Alignment.bottomCenter, + child: Container( + color: Colors.white, + height: 48.0, + margin: EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: SizedBox.expand( + child: Center( + child: Text( + 'Save to Photos', + style: TextStyle( + color: StreamChatTheme.of(modalContext) + .accentColor, + fontWeight: FontWeight.bold, + ), ), ), ), ), ), - ), - if (!_userSearchMode && selectedUsers.isNotEmpty) - _buildShareTextInputSection(modalSetState), - ], + if (!_userSearchMode && _selectedUsers.isNotEmpty) + _buildShareTextInputSection(modalSetState), + ], + ), ), - ), - ); - }), + ); + }), + ), ); }, ); @@ -487,91 +503,93 @@ class _ImageFooterState extends State { Widget _buildShareTextInputSection(modalSetState) { return Align( - alignment: Alignment.bottomCenter, - child: Container( - color: Colors.white, - height: 56.0, - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: TextField( - controller: _messageController, - onChanged: (val) { - modalSetState(() {}); - }, - onTap: () { - modalSetState(() {}); - setState(() {}); - }, - decoration: InputDecoration( - isDense: true, - prefixText: ' ', - hintText: 'Add a comment', - border: OutlineInputBorder( + alignment: Alignment.bottomCenter, + child: Container( + color: Colors.white, + height: 56.0, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: TextField( + controller: _messageController, + onChanged: (val) { + modalSetState(() {}); + }, + onTap: () { + modalSetState(() {}); + setState(() {}); + }, + decoration: InputDecoration( + isDense: true, + prefixText: ' ', + hintText: 'Add a comment', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: BorderSide( + color: Colors.black.withOpacity(0.16), + ), + ), + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0), borderSide: BorderSide( color: Colors.black.withOpacity(0.16), - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(32.0), - borderSide: BorderSide( - color: Colors.black.withOpacity(0.16), - ) - ), - contentPadding: EdgeInsets.symmetric(vertical: 12.0), - ), + )), + contentPadding: EdgeInsets.symmetric(vertical: 12.0), ), ), ), - AnimatedCrossFade( - crossFadeState: _messageController.text.isNotEmpty - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: IconTheme( - data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, - child: Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () { - // sendMessage(); - }, - child: Transform.rotate( - angle: -pi / 2, - child: Icon( - StreamIcons.send_message, - color: StreamChatTheme.of(context).accentColor, - ),), - ), - ), - ), - ), - secondChild: IconTheme( - data: - StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + ), + AnimatedCrossFade( + crossFadeState: _messageController.text.isNotEmpty + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: IconTheme( + data: StreamChatTheme.of(context) + .channelTheme + .messageInputButtonIconTheme, + child: Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: Center( - child: InkWell( - onTap: () {}, - child: Icon( - StreamIcons.send_message, - color: Colors.grey, - ), - )), + child: InkWell( + onTap: () { + sendMessage(); + }, + child: Transform.rotate( + angle: -pi / 2, + child: Icon( + StreamIcons.send_message, + color: StreamChatTheme.of(context).accentColor, + ), + ), + ), ), ), - duration: Duration(milliseconds: 300), - alignment: Alignment.center, ), - ], - ), + secondChild: IconTheme( + data: StreamChatTheme.of(context) + .channelTheme + .messageInputButtonIconTheme, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: InkWell( + onTap: () {}, + child: Icon( + StreamIcons.send_message, + color: Colors.grey, + ), + )), + ), + ), + duration: Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ], ), - ); + ), + ); } Future _queryUsers(context) { @@ -587,4 +605,43 @@ class _ImageFooterState extends State { ], ); } + + /// Sends the current message + void sendMessage() async { + var text = _messageController.text.trim(); + + final attachments = widget.message.attachments; + + _messageController.clear(); + + final client = StreamChat.of(context).client; + + for(var user in _selectedUsers) { + var c = client.channel('messaging', extraData: { + 'members': [ + user.id, + StreamChat.of(context).user.id, + ], + }); + + await c.create(); + + Future sendingFuture; + Message message; + + message = (Message()).copyWith( + text: text, + attachments: attachments, + mentionedUsers: [], + showInChannel: false, + ); + + sendingFuture = c.sendMessage(message); + + await sendingFuture; + } + + Navigator.pop(context); + Navigator.pop(context); + } } diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart index a97bde81..afccaf7a 100644 --- a/lib/src/image_group.dart +++ b/lib/src/image_group.dart @@ -4,6 +4,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/full_screen_image.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ImageGroup extends StatelessWidget { const ImageGroup({ @@ -107,14 +108,20 @@ class ImageGroup extends StatelessWidget { BuildContext context, [ int index, ]) { + final channel = StreamChannel.of(context).channel; + Navigator.push( context, MaterialPageRoute( builder: (context) => - FullScreenImage(urls: images.map((e) => e.imageUrl ?? e.thumbUrl ?? e.assetUrl).toList(), - startIndex: index, - userName: message.user.name, - sentAt: message.createdAt, + StreamChannel( + channel: channel, + child: FullScreenImage(urls: images.map((e) => e.imageUrl ?? e.thumbUrl ?? e.assetUrl).toList(), + startIndex: index, + userName: message.user.name, + sentAt: message.createdAt, + message: message, + ), ), ), ); diff --git a/lib/src/image_header.dart b/lib/src/image_header.dart index b4dfb36d..29c36ad9 100644 --- a/lib/src/image_header.dart +++ b/lib/src/image_header.dart @@ -25,12 +25,15 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { /// Callback to call when the image is tapped. final VoidCallback onImageTap; + final Message message; + final String userName; final String sentAt; /// Creates a channel header ImageHeader({ Key key, + this.message, this.showBackButton = true, this.onBackPressed, this.onTitleTap, @@ -103,12 +106,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { final Size preferredSize; void _showMessageActionModalBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showDialog( context: context, builder: (context) { - return ImageActionsModal( - userName: userName, - sentAt: sentAt, + return StreamChannel( + channel: channel, + child: ImageActionsModal( + userName: userName, + sentAt: sentAt, + message: message, + ), ); }); }