import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:dio/dio.dart'; import 'package:esys_flutter_share/esys_flutter_share.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:path_provider/path_provider.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ImageFooter extends StatefulWidget { /// Callback to call when pressing the back button. /// By default it calls [Navigator.pop] final VoidCallback onBackPressed; /// Callback to call when the header is tapped. final VoidCallback onTitleTap; /// Callback to call when the image is tapped. final VoidCallback onImageTap; final int currentPage; final int totalPages; final List mediaAttachments; final Message message; final List videoPackages; final ValueChanged mediaSelectedCallBack; /// Creates a channel header ImageFooter({ Key key, this.onBackPressed, this.onTitleTap, this.onImageTap, this.currentPage = 0, this.totalPages = 0, this.mediaAttachments, this.message, this.videoPackages, this.mediaSelectedCallBack, }) : preferredSize = Size.fromHeight(kToolbarHeight), super(key: key); @override _ImageFooterState createState() => _ImageFooterState(); @override final Size preferredSize; } class _ImageFooterState extends State { bool _userSearchMode = false; TextEditingController _searchController; TextEditingController _messageController = TextEditingController(); FocusNode _messageFocusNode = FocusNode(); String _channelNameQuery; List _selectedChannels = []; bool _loading = false; Timer _debounce; Function modalSetStateCallback; void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted && modalSetStateCallback != null) { modalSetStateCallback(() { _channelNameQuery = _searchController.text; }); } }); } @override void initState() { super.initState(); _searchController = TextEditingController()..addListener(_userNameListener); _messageFocusNode.addListener(() { setState(() {}); }); } @override void dispose() { _searchController?.clear(); _searchController?.removeListener(_userNameListener); _searchController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SafeArea( child: Container( color: StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, padding: EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: StreamSvgIcon.icon_SHARE( size: 24.0, color: StreamChatTheme.of(context).colorTheme.black, ), onPressed: () { _buildShareModal(context); }, ), InkWell( onTap: widget.onTitleTap, child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( '${widget.currentPage + 1} of ${widget.totalPages}', style: StreamChatTheme.of(context).textTheme.headlineBold, ), ], ), ), ), IconButton( icon: StreamSvgIcon.Icon_grid( color: StreamChatTheme.of(context).colorTheme.black, ), onPressed: () { _buildPhotosModal(context); }, ), ], ), ), ); } Widget _buildPhotosModal(context) { var videoAttachments = widget.mediaAttachments .where((element) => element.type == 'video') .toList(); showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), ), builder: (context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Container( decoration: BoxDecoration( color: StreamChatTheme.of(context).colorTheme.white, borderRadius: BorderRadius.only( topRight: Radius.circular(16.0), topLeft: Radius.circular(16.0), )), child: Stack( children: [ Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Photos', style: StreamChatTheme.of(context).textTheme.headlineBold, ), ), ), Align( alignment: Alignment.centerRight, child: IconButton( icon: StreamSvgIcon.close( color: StreamChatTheme.of(context).colorTheme.black, ), onPressed: () { Navigator.pop(context); }, ), ), ], ), ), Container( color: StreamChatTheme.of(context).colorTheme.white, child: GridView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemBuilder: (context, position) { if (widget.mediaAttachments[position].type == 'video') { var controllerPackage = widget.videoPackages[ videoAttachments .indexOf(widget.mediaAttachments[position])]; return InkWell( onTap: () { widget.mediaSelectedCallBack(position); }, child: FittedBox( fit: BoxFit.cover, child: Chewie( controller: controllerPackage.chewieController, ), ), ); } else { return InkWell( onTap: () { widget.mediaSelectedCallBack(position); }, child: Padding( padding: const EdgeInsets.all(1.0), child: AspectRatio( child: CachedNetworkImage( imageUrl: widget .mediaAttachments[position].imageUrl ?? widget.mediaAttachments[position].assetUrl ?? widget.mediaAttachments[position].thumbUrl, fit: BoxFit.cover, ), aspectRatio: 1.0, ), ), ); } }, itemCount: widget.mediaAttachments.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3), ), ), ], ); }, ); } void _buildShareModal(context) { showDialog( context: context, builder: (context) { return StatefulBuilder(builder: (context, modalSetState) { modalSetStateCallback = modalSetState; return Padding( padding: EdgeInsets.only( top: _userSearchMode || _messageFocusNode.hasFocus ? 16.0 : MediaQuery.of(context).size.height / 2, left: 8.0, right: 8.0), child: Material( borderRadius: BorderRadius.only( topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0), ), clipBehavior: Clip.antiAlias, child: Scaffold( body: UsersBloc( child: Column( children: [ _buildTextInputSection(modalSetState), if (_userSearchMode) SizedBox( height: 22.0, ), Expanded( child: ChannelsBloc( child: ChannelListView( selectedChannels: _selectedChannels, onChannelTap: (channel, _) { _searchController.clear(); if (!_selectedChannels.contains(channel)) { modalSetState(() { _selectedChannels.add(channel); }); } else { modalSetState(() { _selectedChannels.remove(channel); }); } }, crossAxisCount: 4, pagination: PaginationParams( limit: 25, ), filter: { if (_channelNameQuery?.trim()?.isNotEmpty == true) 'name': { r'$autocomplete': _channelNameQuery, }, 'id': { r'$ne': StreamChat.of(context).user.id, }, }, sort: [ SortOption( 'name', direction: SortOption.ASC, ), ], 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: StreamSvgIcon.search( size: 96, color: Colors.grey, ), ), Text( 'No chat matches these keywords...'), ], ), ), ), ); }, ); }, ), ), ), if (_selectedChannels.isNotEmpty) _buildShareTextInputSection(modalSetState), if (!_userSearchMode && _selectedChannels.isEmpty) Align( alignment: Alignment.bottomCenter, child: Container( color: StreamChatTheme.of(context).colorTheme.white, height: 48.0, child: Material( color: StreamChatTheme.of(context).colorTheme.white, child: InkWell( onTap: () async { var url = widget .mediaAttachments[widget.currentPage] .imageUrl ?? widget .mediaAttachments[widget.currentPage] .assetUrl ?? widget .mediaAttachments[widget.currentPage] .thumbUrl; if (widget .mediaAttachments[widget.currentPage] .type == 'video') { await _saveVideo(url); Navigator.pop(context); } else { await _saveImage(url); Navigator.pop(context); } }, child: SizedBox.expand( child: Center( child: Text( 'Save to Photos', style: TextStyle( color: StreamChatTheme.of(context) .colorTheme .accentBlue, fontWeight: FontWeight.bold, ), ), ), ), ), ), ), ), ], ), ), ), ), ); }); }, ); } Widget _buildTextInputSection(modalSetState) { if (_userSearchMode) { return Column( children: [ SizedBox( height: 16.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: 16.0, ), Expanded( child: TextField( controller: _searchController, cursorColor: StreamChatTheme.of(context).colorTheme.black, autofocus: true, decoration: InputDecoration( isDense: true, prefixIconConstraints: BoxConstraints.tight(Size(36.0, 44.0)), prefixIcon: Padding( padding: const EdgeInsets.symmetric( vertical: 2.0, horizontal: 6.0), child: StreamSvgIcon.search( color: StreamChatTheme.of(context).colorTheme.black, ), ), hintText: 'Search', border: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0), borderSide: BorderSide( color: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.08)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0), borderSide: BorderSide( color: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.08)), ), contentPadding: EdgeInsets.zero, ), ), ), SizedBox( width: 8.0, ), IconButton( icon: StreamSvgIcon.close_small( color: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.5), ), onPressed: () { modalSetState(() { _userSearchMode = false; }); setState(() {}); }, ) ], ), ], ); } else { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: IconButton( icon: StreamSvgIcon.search( color: StreamChatTheme.of(context).colorTheme.black, ), iconSize: 24.0, onPressed: () { modalSetState(() { _userSearchMode = true; }); }, ), ), Padding( padding: const EdgeInsets.all(16.0), child: Text( 'Select a Chat to Share', style: StreamChatTheme.of(context).textTheme.headlineBold, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: IconButton( icon: StreamSvgIcon.share_arrow( color: StreamChatTheme.of(context).colorTheme.black, ), onPressed: () async { var url = widget.mediaAttachments[widget.currentPage].imageUrl ?? widget.mediaAttachments[widget.currentPage].assetUrl ?? widget.mediaAttachments[widget.currentPage].thumbUrl; var type = widget.mediaAttachments[widget.currentPage].type == 'image' ? 'jpg' : url?.split('?')?.first?.split('.')?.last ?? 'jpg'; var request = await HttpClient().getUrl(Uri.parse(url)); var response = await request.close(); var bytes = await consolidateHttpClientResponseBytes(response); await Share.file('File', 'image.$type', bytes, 'image/$type'); }, ), ), ], ); } } Widget _buildShareTextInputSection(modalSetState) { return Align( alignment: Alignment.bottomCenter, child: Container( color: StreamChatTheme.of(context).colorTheme.white, height: 56.0, child: _loading ? Center( child: Padding( padding: const EdgeInsets.all(8.0), child: CircularProgressIndicator(), ), ) : Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(left: 8.0), child: TextField( controller: _messageController, focusNode: _messageFocusNode, 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: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.16), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(32.0), borderSide: BorderSide( color: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.16), )), contentPadding: EdgeInsets.symmetric(vertical: 12.0), ), ), ), ), IconTheme( data: StreamChatTheme.of(context) .channelTheme .messageInputButtonIconTheme, child: Center( child: Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { modalSetState(() { _loading = true; }); await sendMessage(); modalSetState(() { _loading = false; }); }, child: Transform.rotate( angle: -pi / 2, child: StreamSvgIcon.Icon_send_message( color: StreamChatTheme.of(context) .colorTheme .accentBlue, ), ), ), ), ), ), ], ), ), ); } /// Sends the current message Future sendMessage() async { var text = _messageController.text.trim(); final attachments = widget.message.attachments; _messageController.clear(); for (var channel in _selectedChannels) { final message = Message( text: text, attachments: [attachments[widget.currentPage]], ); await channel.sendMessage(message); } _selectedChannels.clear(); Navigator.pop(context); } Future _saveImage(String url) async { var response = await Dio() .get(url, options: Options(responseType: ResponseType.bytes)); final result = await ImageGallerySaver.saveImage( Uint8List.fromList(response.data), quality: 60, name: "${DateTime.now().millisecondsSinceEpoch}"); return result; } Future _saveVideo(String url) async { var appDocDir = await getTemporaryDirectory(); var savePath = appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4"; await Dio().download(url, savePath); final result = await ImageGallerySaver.saveFile(savePath); print(result); } } /// Used for clipping textfield prefix icon class IconClipper extends CustomClipper { @override Path getClip(Size size) { var leftX = size.width / 5; var rightX = 4 * size.width / 5; var topY = size.height / 5; var bottomY = 4 * size.height / 5; final path = Path(); path.moveTo(leftX, topY); path.lineTo(leftX, bottomY); path.lineTo(rightX, bottomY); path.lineTo(rightX, topY); path.lineTo(leftX, topY); path.lineTo(0.0, 0.0); path.close(); return path; } @override bool shouldReclip(CustomClipper oldClipper) { return false; } }