diff --git a/lib/src/chat_info_screen.dart b/example/lib/chat_info_screen.dart similarity index 66% rename from lib/src/chat_info_screen.dart rename to example/lib/chat_info_screen.dart index 3a9c25ee..5eb0df7d 100644 --- a/lib/src/chat_info_screen.dart +++ b/example/lib/chat_info_screen.dart @@ -1,8 +1,12 @@ +import 'package:emojis/emojis.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import '../stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'main.dart'; +import 'routes/routes.dart'; /// Detail screen for a 1:1 chat correspondence class ChatInfoScreen extends StatefulWidget { @@ -24,12 +28,14 @@ class _ChatInfoScreenState extends State { body: ListView( children: [ _buildUserHeader(), - SizedBox( + Container( height: 8.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, ), _buildOptionListTiles(), - SizedBox( + Container( height: 8.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, ), if ([ 'admin', @@ -73,8 +79,9 @@ class _ChatInfoScreenState extends State { SizedBox(height: 7.0), _buildConnectedTitleState(), SizedBox(height: 15.0), - _OptionListTile( + OptionListTile( title: '@${widget.user.id}', + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( @@ -129,14 +136,19 @@ class _ChatInfoScreenState extends State { StreamBuilder( stream: StreamChannel.of(context).channel.isMutedStream, builder: (context, snapshot) { - return _OptionListTile( + return OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, title: 'Mute user', - leading: StreamSvgIcon.mute( - size: 23.0, - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.5), + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: StreamSvgIcon.mute( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), ), trailing: snapshot.data == null ? CircularProgressIndicator() @@ -171,40 +183,118 @@ class _ChatInfoScreenState extends State { // ), // onTap: () {}, // ), - _OptionListTile( + OptionListTile( title: 'Photos & Videos', - leading: StreamSvgIcon.pictures( - size: 32.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.pictures( + size: 36.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, ), - trailing: StreamSvgIcon.right(), onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => _MediaDisplayScreen())); + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: ChannelMediaDisplayScreen( + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); }, ), - _OptionListTile( + OptionListTile( title: 'Files', - leading: StreamSvgIcon.files( - size: 32.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: StreamSvgIcon.files( + size: 32.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, ), - trailing: StreamSvgIcon.right(), onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => _FileDisplayScreen())); + final channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: ChannelFileDisplayScreen( + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + ), + ), + ), + ), + ); }, ), - _OptionListTile( + OptionListTile( title: 'Shared groups', - leading: StreamSvgIcon.Icon_group( - size: 24.0, - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: StreamSvgIcon.Icon_group( + size: 24.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, ), - trailing: StreamSvgIcon.right(), onTap: () { Navigator.push( context, @@ -218,16 +308,23 @@ class _ChatInfoScreenState extends State { } Widget _buildDeleteListTile() { - return _OptionListTile( - title: 'Delete conversation', - leading: StreamSvgIcon.delete( - color: Colors.red, - size: 24.0, + return OptionListTile( + title: 'Delete Conversation', + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22.0), + child: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, + size: 24.0, + ), ), onTap: () { _showDeleteDialog(); }, - titleColor: Colors.red, + titleColor: StreamChatTheme.of(context).colorTheme.accentRed, ); } @@ -239,7 +336,7 @@ class _ChatInfoScreenState extends State { question: 'Are you sure you want to delete this conversation?', cancelText: 'CANCEL', icon: StreamSvgIcon.delete( - color: Colors.red, + color: StreamChatTheme.of(context).colorTheme.accentRed, ), ); var channel = StreamChannel.of(context).channel; @@ -302,72 +399,6 @@ class _ChatInfoScreenState extends State { } } -class _OptionListTile extends StatelessWidget { - final String title; - final StreamSvgIcon leading; - final Widget trailing; - final VoidCallback onTap; - final Color titleColor; - - _OptionListTile({ - this.title, - this.leading, - this.trailing, - this.onTap, - this.titleColor, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - color: StreamChatTheme.of(context).colorTheme.white, - height: 2.0, - ), - Material( - color: StreamChatTheme.of(context).colorTheme.whiteSnow, - child: Container( - height: 56.0, - child: InkWell( - onTap: onTap, - child: Row( - children: [ - if (leading != null) - Expanded( - child: Center(child: leading), - ), - if (leading == null) - SizedBox( - width: 16.0, - ), - Expanded( - flex: 4, - child: Text( - title, - style: TextStyle( - fontWeight: FontWeight.w600, color: titleColor), - )), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Align( - alignment: Alignment.centerRight, - child: trailing ?? Container(), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} - class _SharedGroupsScreen extends StatefulWidget { final User mainUser; final User otherUser; @@ -430,12 +461,46 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { }, ), builder: (context, snapshot) { - if (snapshot.data == null) { + if (!snapshot.hasData) { return Center( child: CircularProgressIndicator(), ); } + if (snapshot.data.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.message( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No Shared Groups', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + SizedBox(height: 8.0), + Text( + 'Group shared with User will appear here.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + return ListView.builder( itemCount: snapshot.data.length, itemBuilder: (context, position) { @@ -530,75 +595,3 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ); } } - -class _MediaDisplayScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Photos & Videos', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, - fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - width: 24.0, - height: 24.0, - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - ), - ); - } -} - -class _FileDisplayScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - appBar: AppBar( - brightness: Theme.of(context).brightness, - elevation: 1, - centerTitle: true, - title: Text( - 'Files', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.black, - fontSize: 16.0), - ), - leading: Center( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - child: StreamSvgIcon.left( - color: StreamChatTheme.of(context).colorTheme.black, - size: 24.0, - ), - width: 24.0, - height: 24.0, - ), - ), - ), - backgroundColor: StreamChatTheme.of(context).colorTheme.white, - ), - ); - } -} diff --git a/example/lib/group_info_screen.dart b/example/lib/group_info_screen.dart new file mode 100644 index 00000000..ffab48df --- /dev/null +++ b/example/lib/group_info_screen.dart @@ -0,0 +1,1039 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'chat_info_screen.dart'; +import 'main.dart'; +import 'routes/routes.dart'; + +class GroupInfoScreen extends StatefulWidget { + @override + _GroupInfoScreenState createState() => _GroupInfoScreenState(); +} + +class _GroupInfoScreenState extends State { + TextEditingController _nameController; + + TextEditingController _searchController; + String _userNameQuery = ''; + + Timer _debounce; + Function modalSetStateCallback; + + final FocusNode _focusNode = FocusNode(); + + bool listExpanded = false; + + void _userNameListener() { + if (_searchController.text == _userNameQuery) { + return; + } + if (_debounce?.isActive ?? false) _debounce.cancel(); + _debounce = Timer(const Duration(milliseconds: 350), () { + if (mounted && modalSetStateCallback != null) { + modalSetStateCallback(() { + _userNameQuery = _searchController.text; + }); + } + }); + } + + @override + void initState() { + super.initState(); + var channel = StreamChannel.of(context); + _nameController = TextEditingController.fromValue( + TextEditingValue(text: channel.channel.extraData['name'] ?? '')); + _searchController = TextEditingController()..addListener(_userNameListener); + + _nameController.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + var channel = StreamChannel.of(context); + + return StreamBuilder>( + stream: channel.channel.state.membersStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Container( + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + child: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + appBar: AppBar( + elevation: 1.0, + toolbarHeight: 56.0, + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + leading: InkWell( + onTap: () { + Navigator.pop(context); + }, + child: Padding( + padding: const EdgeInsets.only(left: 12.0), + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + ), + leadingWidth: 36.0, + title: Column( + children: [ + StreamBuilder( + stream: channel.channelStateStream, + builder: (context, state) { + if (!state.hasData) { + return Text( + 'Loading...', + style: TextStyle( + color: + StreamChatTheme.of(context).colorTheme.black, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + return Text( + _getChannelName( + 2 * MediaQuery.of(context).size.width / 3, + members: snapshot.data, + extraData: state.data.channel.extraData, + maxFontSize: 16.0), + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }), + SizedBox( + height: 3.0, + ), + Text( + '${channel.channel.memberCount} Members, ${snapshot?.data?.where((e) => e.user.online)?.length ?? 0} Online', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + fontSize: 12.0, + ), + ), + ], + ), + centerTitle: true, + actions: [ + StreamNeumorphicButton( + child: InkWell( + onTap: () { + _buildAddUserModal(context); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon.userAdd( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue), + ), + ), + ), + ], + ), + body: ListView( + children: [ + _buildMembers(snapshot.data), + Container( + height: 8.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + _buildNameTile(), + _buildOptionListTiles(), + ], + ), + ); + }); + } + + Widget _buildMembers(List members) { + int groupMemberListLength; + + if (listExpanded) { + groupMemberListLength = members.length; + } else { + groupMemberListLength = members.length > 6 ? 6 : members.length; + } + + return Column( + children: [ + ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: groupMemberListLength, + itemBuilder: (context, position) { + return Material( + child: InkWell( + onTap: () { + var userMember = members.firstWhere( + (e) => e.user.id == StreamChat.of(context).user.id); + _showUserInfoModal( + members[position].user, userMember.role == 'owner'); + }, + child: Container( + height: 65.0, + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 12.0), + child: UserAvatar( + user: members[position].user, + constraints: BoxConstraints( + maxHeight: 40.0, maxWidth: 40.0), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + members[position].user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox( + height: 1.0, + ), + Text( + _getLastSeen(members[position].user), + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + members[position].role == 'owner' ? 'Owner' : '', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), + ), + ), + ], + ), + Container( + height: 1.0, + color: StreamChatTheme.of(context) + .colorTheme + .greyGainsboro, + ), + ], + ), + ), + ), + color: StreamChatTheme.of(context).colorTheme.whiteSnow, + ); + }, + ), + if (groupMemberListLength != members.length) + InkWell( + onTap: () { + setState(() { + listExpanded = true; + }); + }, + child: Material( + color: StreamChatTheme.of(context).colorTheme.whiteSnow, + child: Container( + height: 65.0, + child: Column( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 21.0, vertical: 12.0), + child: StreamSvgIcon.down( + color: + StreamChatTheme.of(context).colorTheme.grey, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${members.length - groupMemberListLength} more', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .grey), + ), + ], + ), + ), + ], + ), + ), + Container( + height: 1.0, + color: + StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildNameTile() { + var channel = StreamChannel.of(context).channel; + var channelName = channel.extraData['name'] ?? ''; + + return Material( + color: StreamChatTheme.of(context).colorTheme.whiteSnow, + child: Container( + height: 56.0, + alignment: Alignment.center, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(7.0), + child: Text( + 'NAME', + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), + ), + ), + SizedBox( + width: 7.0, + ), + Expanded( + child: TextField( + focusNode: _focusNode, + controller: _nameController, + cursorColor: StreamChatTheme.of(context).colorTheme.black, + decoration: InputDecoration.collapsed( + hintText: 'Add a group name', + hintStyle: StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5))), + style: TextStyle( + fontWeight: FontWeight.bold, + height: 0.82, + ), + ), + ), + if ((channelName == null) || + (channelName != _nameController.text.trim())) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + child: StreamSvgIcon.close_small(), + onTap: () { + setState(() { + _nameController.text = + _getChannelName(MediaQuery.of(context).size.width); + _focusNode.unfocus(); + }); + }, + ), + Padding( + padding: const EdgeInsets.only(right: 16.0, left: 8.0), + child: InkWell( + child: StreamSvgIcon.check( + color: + StreamChatTheme.of(context).colorTheme.accentBlue, + size: 24.0, + ), + onTap: () { + StreamChannel.of(context).channel.update({ + 'name': _nameController.text.trim(), + }).catchError((err) { + setState(() { + _nameController.text = channelName; + _focusNode.unfocus(); + }); + }); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildOptionListTiles() { + var channel = StreamChannel.of(context); + + return Column( + children: [ + // OptionListTile( + // title: 'Notifications', + // leading: StreamSvgIcon.Icon_notification( + // size: 24.0, + // color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + // ), + // trailing: CupertinoSwitch( + // value: true, + // onChanged: (val) {}, + // ), + // onTap: () {}, + // ), + StreamBuilder( + stream: StreamChannel.of(context).channel.isMutedStream, + builder: (context, snapshot) { + return OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + separatorColor: + StreamChatTheme.of(context).colorTheme.greyGainsboro, + title: 'Mute group', + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.mute( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + trailing: snapshot.data == null + ? CircularProgressIndicator() + : CupertinoSwitch( + value: snapshot.data, + onChanged: (val) { + if (snapshot.data) { + channel.channel.unmute(); + } else { + channel.channel.mute(); + } + }, + ), + onTap: () {}, + ); + }), + OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + separatorColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, + title: 'Photos & Videos', + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: StreamSvgIcon.pictures( + size: 32.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + var channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: ChannelMediaDisplayScreen( + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + await Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, + ), + ), + ), + ), + ); + }, + ), + OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + separatorColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, + title: 'Files', + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: StreamSvgIcon.files( + size: 32.0, + color: + StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + ), + ), + trailing: StreamSvgIcon.right( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + onTap: () { + var channel = StreamChannel.of(context).channel; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: MessageSearchBloc( + child: ChannelFileDisplayScreen( + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + ), + ), + ), + ), + ); + }, + ), + if (!channel.channel.isDistinct) + OptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, + separatorColor: + StreamChatTheme.of(context).colorTheme.greyGainsboro, + title: 'Leave Group', + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.userRemove( + size: 24.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + trailing: Container( + height: 24.0, + width: 24.0, + ), + onTap: () async { + await channel.channel + .removeMembers([StreamChat.of(context).user.id]); + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ], + ); + } + + void _buildAddUserModal(context) { + var channel = StreamChannel.of(context).channel; + + showDialog( + context: context, + builder: (context) { + return StatefulBuilder(builder: (context, modalSetState) { + modalSetStateCallback = modalSetState; + return Padding( + padding: EdgeInsets.only(top: 16.0, 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), + Expanded( + child: UserListView( + selectedUsers: {}, + onUserTap: (user, _) async { + _searchController.clear(); + + await channel.addMembers([user.id]); + Navigator.pop(context); + setState(() {}); + }, + crossAxisCount: 4, + pagination: PaginationParams( + limit: 25, + ), + filter: { + if (_searchController.text.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$nin': [ + StreamChat.of(context).user.id, + ...channel.state.members.map((e) => e.userId), + ], + }, + }, + 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: StreamSvgIcon.search( + size: 96, + color: StreamChatTheme.of(context) + .colorTheme + .grey, + ), + ), + Text( + 'No user matches these keywords...'), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + }); + }, + ); + } + + Widget _buildTextInputSection(modalSetState) { + 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: () { + Navigator.pop(context); + }, + ) + ], + ), + ], + ); + } + + void _showUserInfoModal(User user, bool isUserAdmin) { + var channel = StreamChannel.of(context).channel; + + showModalBottomSheet( + context: context, + clipBehavior: Clip.antiAlias, + isScrollControlled: true, + builder: (context) { + return StreamChannel( + channel: channel, + child: Material( + color: StreamChatTheme.of(context).colorTheme.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 24.0, + ), + Center( + child: Text( + user.name, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox( + height: 5.0, + ), + _buildConnectedTitleState(user), + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: UserAvatar( + user: user, + constraints: BoxConstraints( + maxHeight: 64.0, + minHeight: 64.0, + ), + borderRadius: BorderRadius.circular(32.0), + ), + ), + ), + if (StreamChat.of(context).user.id != user.id) + _buildModalListTile( + context, + StreamSvgIcon.user( + color: StreamChatTheme.of(context).colorTheme.grey, + size: 24.0, + ), + 'View info', + () async { + var client = StreamChat.of(context).client; + + var c = client.channel('messaging', extraData: { + 'members': [ + user.id, + StreamChat.of(context).user.id, + ], + }); + + await c.watch(); + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: c, + child: ChatInfoScreen( + user: user, + ), + ), + ), + ); + }, + ), + if (StreamChat.of(context).user.id != user.id) + _buildModalListTile( + context, + StreamSvgIcon.message( + color: StreamChatTheme.of(context).colorTheme.grey, + size: 24.0, + ), + 'Message', + () async { + var client = StreamChat.of(context).client; + + var c = client.channel('messaging', extraData: { + 'members': [ + user.id, + StreamChat.of(context).user.id, + ], + }); + + await c.watch(); + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: c, + child: ChannelPage(), + ), + ), + ); + }, + ), + if (!channel.isDistinct && + StreamChat.of(context).user.id != user.id && + isUserAdmin) + _buildModalListTile( + context, + StreamSvgIcon.Icon_user_settings( + color: StreamChatTheme.of(context).colorTheme.grey, + size: 24.0, + ), + 'Make Owner', () { + // TODO: Add make owner implementation (Remaining from backend) + }), + if (!channel.isDistinct && + StreamChat.of(context).user.id != user.id && + isUserAdmin) + _buildModalListTile( + context, + StreamSvgIcon.userRemove( + color: StreamChatTheme.of(context).colorTheme.accentRed, + size: 24.0, + ), + 'Remove From Group', () async { + await channel.removeMembers([user.id]); + Navigator.pop(context); + }, color: StreamChatTheme.of(context).colorTheme.accentRed), + _buildModalListTile( + context, + StreamSvgIcon.close_small( + color: StreamChatTheme.of(context).colorTheme.grey, + size: 24.0, + ), + 'Cancel', () { + Navigator.pop(context); + }), + ], + ), + ), + ); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + ), + ), + ); + } + + void _showRemoveUserModal(User user) {} + + Widget _buildConnectedTitleState(User user) { + var alternativeWidget; + + final otherMember = user; + + if (otherMember != null) { + if (otherMember.online) { + alternativeWidget = Text( + 'Online', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), + ); + } else { + alternativeWidget = Text( + 'Last seen ${Jiffy(otherMember.lastActive).fromNow()}', + style: TextStyle( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5)), + ); + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (user.online) + Material( + type: MaterialType.circle, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + constraints: BoxConstraints.tightFor( + width: 28, + height: 12, + ), + child: Material( + shape: CircleBorder(), + color: Color(0xff20E070), + ), + ), + color: StreamChatTheme.of(context).colorTheme.white, + ), + alternativeWidget, + ], + ); + } + + Widget _buildModalListTile( + BuildContext context, Widget leading, String title, VoidCallback onTap, + {Color color}) { + color ??= StreamChatTheme.of(context).colorTheme.black; + + return Material( + color: StreamChatTheme.of(context).colorTheme.white, + child: InkWell( + onTap: onTap, + child: Column( + children: [ + Container( + height: 1.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + Container( + height: 64.0, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: leading, + ), + Expanded( + child: Text( + title, + style: + TextStyle(color: color, fontWeight: FontWeight.bold), + ), + ) + ], + ), + ), + ], + ), + ), + ); + } + + String _getChannelName(double width, + {List members, Map extraData, double maxFontSize}) { + String title; + var client = StreamChat.of(context); + if (extraData['name'] == null) { + final otherMembers = + members.where((member) => member.user.id != client.user.id); + if (otherMembers.isNotEmpty) { + final maxWidth = width; + final maxChars = maxWidth / maxFontSize; + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = currentChars + element.user.name.length; + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = otherMembers.length - currentMembers.length; + title = + '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } else { + title = 'No title'; + } + } else { + title = extraData['name']; + } + return title; + } + + String _getLastSeen(User user) { + if (user.online) { + return 'Online'; + } else { + return 'Last seen ${Jiffy(user.lastActive).fromNow()}'; + } + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 8d436614..b004271b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:io'; +import 'package:example/chat_info_screen.dart'; import 'package:example/choose_user_page.dart'; +import 'package:example/group_info_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -394,6 +396,32 @@ class _ChannelListPageState extends State { limit: 20, ), channelWidget: ChannelPage(), + onViewInfoTap: (channel) { + if (channel.memberCount == 2 && + channel.isDistinct) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + user: channel.state.members.first.user, + ), + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: GroupInfoScreen(), + ), + ), + ); + } + }, ), ), ), @@ -434,6 +462,44 @@ class ChannelPage extends StatelessWidget { backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, appBar: ChannelHeader( showTypingIndicator: false, + onImageTap: () async { + var channel = StreamChannel.of(context).channel; + + if (channel.memberCount == 2 && channel.isDistinct) { + final currentUser = StreamChat.of(context).user; + final otherUser = channel.state.members.firstWhere( + (element) => element.user.id != currentUser.id, + orElse: () => null, + ); + if (otherUser != null) { + final pop = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: ChatInfoScreen( + user: otherUser.user, + ), + channel: channel, + ), + ), + ); + + if (pop == true) { + Navigator.pop(context); + } + } + } else { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + child: GroupInfoScreen(), + channel: channel, + ), + ), + ); + } + }, ), body: Column( children: [ @@ -449,6 +515,25 @@ class ChannelPage extends StatelessWidget { parent: parentMessage, ); }, + onShowMessage: (m, c) async { + final client = StreamChat.of(context).client; + final message = m; + final channel = client.channel( + c.type, + id: c.id, + ); + if (channel.state == null) { + await channel.watch(); + } + Navigator.pushReplacementNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialMessage: message, + ), + ); + }, ), Positioned( bottom: 0, diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 8082245f..20725b26 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -7,6 +7,8 @@ import '../main.dart'; import '../group_chat_details_screen.dart'; import '../new_group_chat_screen.dart'; import '../new_chat_screen.dart'; +import '../chat_info_screen.dart'; +import '../group_info_screen.dart'; class AppRoutes { /// Add entry for new route here @@ -63,6 +65,20 @@ class AppRoutes { selectedUsers: args, ); }); + case Routes.CHAT_INFO_SCREEN: + return MaterialPageRoute( + settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN), + builder: (_) { + return ChatInfoScreen( + user: args, + ); + }); + case Routes.GROUP_INFO_SCREEN: + return MaterialPageRoute( + settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN), + builder: (_) { + return GroupInfoScreen(); + }); } } } diff --git a/example/lib/routes/routes.dart b/example/lib/routes/routes.dart index 0e0b1e57..0d2f4e5f 100644 --- a/example/lib/routes/routes.dart +++ b/example/lib/routes/routes.dart @@ -7,4 +7,6 @@ class Routes { static const String NEW_CHAT = '/new_chat'; static const String NEW_GROUP_CHAT = '/new_group_chat'; static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; + static const String CHAT_INFO_SCREEN = '/chat_info_screen'; + static const String GROUP_INFO_SCREEN = '/group_info_screen'; } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4f76c695..67ff213d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.106+109 +version: 1.0.107+110 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index 4bfa3fd0..26f3a152 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -129,16 +129,22 @@ class _ChannelBottomSheetState extends State { height: 24.0, ), OptionListTile( - leading: StreamSvgIcon.user( - color: StreamChatTheme.of(context).colorTheme.grey, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.user( + color: StreamChatTheme.of(context).colorTheme.grey, + ), ), title: 'View Info', onTap: widget.onViewInfoTap, ), if (!channel.isDistinct) OptionListTile( - leading: StreamSvgIcon.userRemove( - color: StreamChatTheme.of(context).colorTheme.grey, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.userRemove( + color: StreamChatTheme.of(context).colorTheme.grey, + ), ), title: 'Leave Group', onTap: () async { @@ -147,8 +153,11 @@ class _ChannelBottomSheetState extends State { ), if (isOwner) OptionListTile( - leading: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), ), title: 'Delete Conversation', titleColor: StreamChatTheme.of(context).colorTheme.accentRed, @@ -157,8 +166,11 @@ class _ChannelBottomSheetState extends State { }, ), OptionListTile( - leading: StreamSvgIcon.close_small( - color: StreamChatTheme.of(context).colorTheme.grey, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: StreamSvgIcon.close_small( + color: StreamChatTheme.of(context).colorTheme.grey, + ), ), title: 'Cancel', onTap: () { diff --git a/lib/src/channel_file_display_screen.dart b/lib/src/channel_file_display_screen.dart new file mode 100644 index 00000000..6feac224 --- /dev/null +++ b/lib/src/channel_file_display_screen.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelFileDisplayScreen extends StatefulWidget { + /// 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 sortOptions; + + /// 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 paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder emptyBuilder; + + const ChannelFileDisplayScreen({ + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + }); + + @override + _ChannelFileDisplayScreenState createState() => + _ChannelFileDisplayScreenState(); +} + +class _ChannelFileDisplayScreenState extends State { + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: { + 'cid': { + r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['file'], + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Files', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.files( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No Files', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + SizedBox(height: 8.0), + Text( + 'Files sent in this chat will appear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = {}; + + for (var item in snapshot.data) { + item.message.attachments.where((e) => e.type == 'file').forEach((e) { + media[e] = item.message; + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: { + 'cid': { + r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['file'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: ListView.builder( + itemBuilder: (context, position) { + var channel = StreamChannel.of(context).channel; + + return Padding( + padding: const EdgeInsets.all(1.0), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FileAttachment( + attachment: media.keys.toList()[position], + ), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } +} diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 65f63cc4..4517d7a0 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -8,7 +8,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import '../stream_chat_flutter.dart'; import './channel_name.dart'; import 'channel_image.dart'; -import 'chat_info_screen.dart'; import 'stream_channel.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) @@ -99,33 +98,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { padding: const EdgeInsets.only(right: 10.0), child: Center( child: ChannelImage( - onTap: onImageTap ?? - () async { - if (channel.memberCount == 2 && channel.isDistinct) { - final currentUser = StreamChat.of(context).user; - final otherUser = channel.state.members.firstWhere( - (element) => element.user.id != currentUser.id, - orElse: () => null, - ); - if (otherUser != null) { - final pop = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - user: otherUser.user, - ), - ), - ), - ); - - if (pop == true) { - Navigator.pop(context); - } - } - } - }, + onTap: onImageTap, ), ), ), diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 1988b9e1..6c4e967e 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -22,6 +22,8 @@ typedef ChannelTapCallback = void Function(Channel, Widget); /// Builder used to create a custom [ChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); +typedef ViewInfoCallback = void Function(Channel); + /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png) /// @@ -75,7 +77,9 @@ class ChannelListView extends StatefulWidget { this.swipeToAction = false, this.pullToRefresh = true, this.crossAxisCount = 1, + this.padding, this.selectedChannels = const [], + this.onViewInfoTap, }) : super(key: key); /// The builder that will be used in case of error @@ -139,8 +143,13 @@ class ChannelListView extends StatefulWidget { /// The number of children in the cross axis. final int crossAxisCount; + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry padding; + final List selectedChannels; + final ViewInfoCallback onViewInfoTap; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -277,6 +286,7 @@ class _ChannelListViewState extends State if (channels.isNotEmpty) { if (widget.crossAxisCount > 1) { child = GridView.builder( + padding: widget.padding, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: widget.crossAxisCount), itemCount: channels.length, @@ -288,6 +298,7 @@ class _ChannelListViewState extends State ); } else { child = ListView.separated( + padding: widget.padding, physics: AlwaysScrollableScrollPhysics(), itemCount: channels.isNotEmpty ? channels.length + 1 : channels.length, @@ -315,6 +326,7 @@ class _ChannelListViewState extends State Widget _buildLoadingWidget() { return ListView( + padding: widget.padding, physics: AlwaysScrollableScrollPhysics(), children: List.generate( 25, @@ -340,9 +352,7 @@ class _ChannelListViewState extends State highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke, child: Column( children: [ - SizedBox( - height: 4.0, - ), + SizedBox(height: 4.0), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -574,23 +584,7 @@ class _ChannelListViewState extends State return StreamChannel( child: ChannelBottomSheet( onViewInfoTap: () { - if (channel.memberCount == 2 && - channel.isDistinct) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - user: - channel.state.members.first.user, - ), - ), - ), - ); - } - - // TODO: Add group screen + widget.onViewInfoTap(channel); }, ), channel: channel, diff --git a/lib/src/channel_media_display_screen.dart b/lib/src/channel_media_display_screen.dart new file mode 100644 index 00000000..cd5238bc --- /dev/null +++ b/lib/src/channel_media_display_screen.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; + +class ChannelMediaDisplayScreen extends StatefulWidget { + /// 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 sortOptions; + + /// 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 paginationParams; + + /// The builder used when the file list is empty. + final WidgetBuilder emptyBuilder; + + final ShowMessageCallback onShowMessage; + + const ChannelMediaDisplayScreen({ + this.sortOptions, + this.paginationParams, + this.emptyBuilder, + this.onShowMessage, + }); + + @override + _ChannelMediaDisplayScreenState createState() => + _ChannelMediaDisplayScreenState(); +} + +class _ChannelMediaDisplayScreenState extends State { + @override + void initState() { + super.initState(); + final messageSearchBloc = MessageSearchBloc.of(context); + messageSearchBloc.search( + filter: { + 'cid': { + r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['image', 'video'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Photos & Videos', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.black, + fontSize: 16.0, + ), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: StreamChatTheme.of(context).colorTheme.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).colorTheme.white, + ), + body: _buildMediaGrid(), + ); + } + + Widget _buildMediaGrid() { + final messageSearchBloc = MessageSearchBloc.of(context); + + return StreamBuilder>( + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + if (snapshot.data.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context); + } + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.pictures( + size: 136.0, + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + ), + SizedBox(height: 16.0), + Text( + 'No Media', + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + ), + SizedBox(height: 8.0), + Text( + 'Photos or video sent in this chat will \nappear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0, + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(0.5), + ), + ), + ], + ), + ); + } + + final media = <_AssetPackage>[]; + + for (var item in snapshot.data) { + item.message.attachments + .where((e) => e.type == 'image' || e.type == 'video') + .forEach((e) { + VideoPlayerController controller; + if (e.type == 'video') { + controller = VideoPlayerController.network(e.assetUrl); + controller.initialize(); + } + media.add(_AssetPackage(e, item.message, controller)); + }); + } + + return LazyLoadScrollView( + onEndOfPage: () => messageSearchBloc.search( + filter: { + 'cid': { + r'$in': ['messaging:${StreamChannel.of(context).channel.id}'] + } + }, + messageFilter: { + 'attachments.type': { + r'$in': ['image', 'video'] + }, + }, + sort: widget.sortOptions, + pagination: widget.paginationParams.copyWith( + offset: messageSearchBloc.messageResponses?.length ?? 0, + ), + ), + child: GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + itemBuilder: (context, position) { + var channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.all(1.0), + child: InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: FullScreenMedia( + mediaAttachments: + media.map((e) => e.attachment).toList(), + startIndex: position, + message: media[position].message, + sentAt: media[position].message.createdAt, + userName: media[position].message.user.name, + onShowMessage: widget.onShowMessage, + ), + ), + ), + ); + }, + child: media[position].attachment.type == 'image' + ? IgnorePointer( + child: ImageAttachment( + attachment: media[position].attachment, + message: media[position].message, + showTitle: false, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + ), + ) + : VideoPlayer(media[position].videoPlayer), + ), + ); + }, + itemCount: media.length, + ), + ); + }, + stream: messageSearchBloc.messagesStream, + ); + } +} + +class _AssetPackage { + Attachment attachment; + Message message; + VideoPlayerController videoPlayer; + + _AssetPackage(this.attachment, this.message, this.videoPlayer); +} diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 7f976688..fe602b4a 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -7,7 +7,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; import 'channel_name.dart'; import 'channel_unread_indicator.dart'; -import 'chat_info_screen.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -64,29 +63,7 @@ class ChannelPreview extends StatelessWidget { } }, leading: ChannelImage( - onTap: onImageTap ?? - () { - if (channel.memberCount == 2 && channel.isDistinct) { - final currentUser = StreamChat.of(context).user; - final otherUser = channel.state.members.firstWhere( - (element) => element.user.id != currentUser.id, - orElse: () => null, - ); - if (otherUser != null) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - user: otherUser.user, - ), - ), - ), - ); - } - } - }, + onTap: onImageTap, ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/src/full_screen_media.dart b/lib/src/full_screen_media.dart index 7e3f3d7d..e164eec8 100644 --- a/lib/src/full_screen_media.dart +++ b/lib/src/full_screen_media.dart @@ -6,9 +6,12 @@ import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/image_footer.dart'; import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:video_player/video_player.dart'; +import 'stream_channel.dart'; import '../stream_chat_flutter.dart'; +typedef ShowMessageCallback = void Function(Message message, Channel channel); + /// A full screen image widget class FullScreenMedia extends StatefulWidget { /// The url of the image @@ -18,6 +21,7 @@ class FullScreenMedia extends StatefulWidget { final int startIndex; final String userName; final DateTime sentAt; + final ShowMessageCallback onShowMessage; /// Instantiate a new FullScreenImage const FullScreenMedia({ @@ -27,6 +31,7 @@ class FullScreenMedia extends StatefulWidget { this.startIndex = 0, this.userName = '', this.sentAt, + this.onShowMessage, }) : super(key: key); @override @@ -167,6 +172,10 @@ class _FullScreenMediaState extends State message: widget.message, urls: widget.mediaAttachments, currentIndex: _currentPage, + onShowMessage: () { + widget.onShowMessage( + widget.message, StreamChannel.of(context).channel); + }, ), ImageFooter( currentPage: _currentPage, diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index cc1a319f..c879cd5b 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -12,6 +12,7 @@ class GiphyAttachment extends StatelessWidget { final MessageTheme messageTheme; final Message message; final Size size; + final ShowMessageCallback onShowMessage; const GiphyAttachment({ Key key, @@ -19,6 +20,7 @@ class GiphyAttachment extends StatelessWidget { this.messageTheme, this.message, this.size, + this.onShowMessage, }) : super(key: key); @override @@ -75,6 +77,7 @@ class GiphyAttachment extends StatelessWidget { userName: message.user.name, sentAt: message.createdAt, message: message, + onShowMessage: onShowMessage, ), ); })); @@ -327,6 +330,7 @@ class GiphyAttachment extends StatelessWidget { userName: message.user.name, sentAt: message.createdAt, message: message, + onShowMessage: onShowMessage, ), ); })); diff --git a/lib/src/image_actions_modal.dart b/lib/src/image_actions_modal.dart index 168581c0..0001df6f 100644 --- a/lib/src/image_actions_modal.dart +++ b/lib/src/image_actions_modal.dart @@ -15,9 +15,15 @@ class ImageActionsModal extends StatelessWidget { final String sentAt; final List urls; final currentIndex; + final VoidCallback onShowMessage; ImageActionsModal( - {this.message, this.userName, this.sentAt, this.urls, this.currentIndex}); + {this.message, + this.userName, + this.sentAt, + this.urls, + this.currentIndex, + this.onShowMessage}); @override Widget build(BuildContext context) { @@ -134,10 +140,8 @@ class ImageActionsModal extends StatelessWidget { size: 24.0, color: StreamChatTheme.of(context).colorTheme.black, - ), () { - Navigator.pop(context); - Navigator.pop(context); - }), + ), + onShowMessage), _buildButton( context, 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}', diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index 2345b2b8..f9e3290b 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -12,6 +12,8 @@ class ImageAttachment extends StatelessWidget { final Message message; final MessageTheme messageTheme; final Size size; + final bool showTitle; + final ShowMessageCallback onShowMessage; const ImageAttachment({ Key key, @@ -19,6 +21,8 @@ class ImageAttachment extends StatelessWidget { @required this.message, @required this.size, this.messageTheme, + this.showTitle = true, + this.onShowMessage, }) : super(key: key); @override @@ -54,6 +58,7 @@ class ImageAttachment extends StatelessWidget { userName: message.user.name, sentAt: message.createdAt, message: message, + onShowMessage: onShowMessage, ), ); }, @@ -83,7 +88,7 @@ class ImageAttachment extends StatelessWidget { ), ), ), - if (attachment.title != null) + if (showTitle && attachment.title != null) Material( color: messageTheme.messageBackgroundColor, child: AttachmentTitle( @@ -93,7 +98,8 @@ class ImageAttachment extends StatelessWidget { ), ], ), - if (attachment.titleLink != null || attachment.ogScrapeUrl != null) + if (showTitle && + (attachment.titleLink != null || attachment.ogScrapeUrl != null)) Positioned.fill( child: Material( color: Colors.transparent, diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index ec92693d..3a6cfbcc 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -12,11 +12,10 @@ 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 { +class ImageFooter extends StatefulWidget implements PreferredSizeWidget { /// Callback to call when pressing the back button. /// By default it calls [Navigator.pop] final VoidCallback onBackPressed; @@ -48,21 +47,25 @@ class ImageFooter extends StatefulWidget { this.message, this.videoPackages, this.mediaSelectedCallBack, - }) : super(key: key); + }) : 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(); + final TextEditingController _messageController = TextEditingController(); + final FocusNode _messageFocusNode = FocusNode(); String _channelNameQuery; - List _selectedChannels = []; + final List _selectedChannels = []; bool _loading = false; Timer _debounce; @@ -99,54 +102,73 @@ class _ImageFooterState extends State { @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, + return SizedBox.fromSize( + size: Size( + MediaQuery.of(context).size.width, + MediaQuery.of(context).padding.bottom + widget.preferredSize.height, + ), + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: BottomAppBar( + color: StreamChatTheme.of(context).colorTheme.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: StreamSvgIcon.icon_SHARE( + size: 24.0, + color: StreamChatTheme.of(context).colorTheme.black, + ), + onPressed: () async { + final attachment = + widget.mediaAttachments[widget.currentPage]; + var url = attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl; + var type = attachment.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'); + }, ), - 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, - ), - ], + 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, + IconButton( + icon: StreamSvgIcon.Icon_grid( + color: StreamChatTheme.of(context).colorTheme.black, + ), + onPressed: () { + _buildPhotosModal(context); + }, ), - onPressed: () { - _buildPhotosModal(context); - }, - ), - ], + ], + ), ), ), ); } - void _buildPhotosModal(context) { + Widget _buildPhotosModal(context) { var videoAttachments = widget.mediaAttachments .where((element) => element.type == 'video') .toList(); @@ -247,305 +269,26 @@ class _ImageFooterState extends State { ); } - 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: BottomAppBar( + child: Container( + height: 40.0, + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 8, + ), + child: _loading + ? Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), + ) + : Row( + children: [ + Expanded( child: TextField( controller: _messageController, focusNode: _messageFocusNode, @@ -557,11 +300,10 @@ class _ImageFooterState extends State { setState(() {}); }, decoration: InputDecoration( - isDense: true, prefixText: ' ', hintText: 'Add a comment', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(32.0), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24.0), borderSide: BorderSide( color: StreamChatTheme.of(context) .colorTheme @@ -570,49 +312,48 @@ class _ImageFooterState extends State { ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(32.0), + borderRadius: BorderRadius.circular(24.0), borderSide: BorderSide( color: StreamChatTheme.of(context) .colorTheme .black .withOpacity(0.16), )), - contentPadding: EdgeInsets.symmetric(vertical: 12.0), + contentPadding: const EdgeInsets.all(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, - ), + SizedBox(width: 8), + IconTheme( + data: StreamChatTheme.of(context) + .channelTheme + .messageInputButtonIconTheme, + child: IconButton( + onPressed: () async { + modalSetState(() => _loading = true); + await sendMessage(); + modalSetState(() => _loading = false); + }, + splashRadius: 24, + visualDensity: VisualDensity.compact, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + padding: EdgeInsets.zero, + icon: Transform.rotate( + angle: -pi / 2, + child: StreamSvgIcon.Icon_send_message( + color: StreamChatTheme.of(context) + .colorTheme + .accentBlue, ), ), ), ), - ), - ], - ), + ], + ), + ), ), ); } diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart index e9b52f88..bb577a4b 100644 --- a/lib/src/image_group.dart +++ b/lib/src/image_group.dart @@ -10,11 +10,13 @@ class ImageGroup extends StatelessWidget { @required this.images, @required this.message, @required this.size, + this.onShowMessage, }) : super(key: key); final List images; final Message message; final Size size; + final ShowMessageCallback onShowMessage; @override Widget build(BuildContext context) { @@ -119,6 +121,7 @@ class ImageGroup extends StatelessWidget { userName: message.user.name, sentAt: message.createdAt, message: message, + onShowMessage: onShowMessage, ), ), ), diff --git a/lib/src/image_header.dart b/lib/src/image_header.dart index a9f1173f..6139bc7c 100644 --- a/lib/src/image_header.dart +++ b/lib/src/image_header.dart @@ -13,6 +13,9 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it calls [Navigator.pop] final VoidCallback onBackPressed; + /// Callback to call when pressing the show message button. + final VoidCallback onShowMessage; + /// Callback to call when the header is tapped. final VoidCallback onTitleTap; @@ -35,6 +38,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { this.currentIndex, this.showBackButton = true, this.onBackPressed, + this.onShowMessage, this.onTitleTap, this.onImageTap, this.userName = '', @@ -111,6 +115,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { message: message, urls: urls, currentIndex: currentIndex, + onShowMessage: onShowMessage, ), ); }); diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 40680991..1bcce91f 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -116,6 +116,7 @@ class MessageListView extends StatefulWidget { this.scrollController, this.itemPositionListener, this.highlightInitialMessage = false, + this.onShowMessage, }) : super(key: key); /// Function used to build a custom message widget @@ -162,6 +163,8 @@ class MessageListView extends StatefulWidget { /// Also See [StreamChannel] final bool highlightInitialMessage; + final ShowMessageCallback onShowMessage; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -711,6 +714,7 @@ class _MessageListViewState extends State { messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme : StreamChatTheme.of(context).otherMessageTheme, + onShowMessage: widget.onShowMessage, ); } @@ -788,6 +792,7 @@ class _MessageListViewState extends State { : StreamChatTheme.of(context).otherMessageTheme, readList: readList, allRead: allRead, + onShowMessage: widget.onShowMessage, ); if (!initialMessageHighlightComplete && diff --git a/lib/src/message_search_bloc.dart b/lib/src/message_search_bloc.dart index 0128bb22..ab277c4c 100644 --- a/lib/src/message_search_bloc.dart +++ b/lib/src/message_search_bloc.dart @@ -55,6 +55,7 @@ class MessageSearchBlocState extends State /// Calls [Client.search] updating [queryMessagesLoading] stream Future search({ Map filter, + Map messageFilter, List sort, String query, PaginationParams pagination, @@ -78,6 +79,7 @@ class MessageSearchBlocState extends State sort, query, pagination, + messageFilters: messageFilter, ); if (clear) { diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 6ef537f6..30f737c1 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -119,6 +119,8 @@ class MessageWidget extends StatefulWidget { final List readList; + final ShowMessageCallback onShowMessage; + /// If true show the users username next to the timestamp of the message final bool showUsername; final bool showTimestamp; @@ -157,6 +159,7 @@ class MessageWidget extends StatefulWidget { this.onUserAvatarTap, this.onLinkTap, this.onMessageActions, + this.onShowMessage, this.editMessageInputBuilder, this.textBuilder, Map customAttachmentBuilders, @@ -179,6 +182,7 @@ class MessageWidget extends StatefulWidget { MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.height * 0.3, ), + onShowMessage: onShowMessage, ); }, 'video': (context, message, attachment) { @@ -190,6 +194,7 @@ class MessageWidget extends StatefulWidget { MediaQuery.of(context).size.height * 0.3, ), message: message, + onShowMessage: onShowMessage, ); }, 'giphy': (context, message, attachment) { @@ -201,6 +206,7 @@ class MessageWidget extends StatefulWidget { MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.height * 0.3, ), + onShowMessage: onShowMessage, ); }, 'file': (context, message, attachment) { @@ -710,6 +716,7 @@ class _MessageWidgetState extends State { ), images: images, message: widget.message, + onShowMessage: widget.onShowMessage, ), ), ), diff --git a/lib/src/option_list_tile.dart b/lib/src/option_list_tile.dart index d551d33e..1b87c9d7 100644 --- a/lib/src/option_list_tile.dart +++ b/lib/src/option_list_tile.dart @@ -4,10 +4,13 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; class OptionListTile extends StatelessWidget { final String title; - final StreamSvgIcon leading; + final Widget leading; final Widget trailing; final VoidCallback onTap; final Color titleColor; + final Color tileColor; + final Color separatorColor; + final TextStyle titleTextStyle; OptionListTile({ this.title, @@ -15,6 +18,9 @@ class OptionListTile extends StatelessWidget { this.trailing, this.onTap, this.titleColor, + this.tileColor, + this.separatorColor, + this.titleTextStyle, }); @override @@ -22,21 +28,19 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: StreamChatTheme.of(context).colorTheme.greyGainsboro, + color: separatorColor ?? + StreamChatTheme.of(context).colorTheme.greyGainsboro, height: 1.0, ), Material( - color: StreamChatTheme.of(context).colorTheme.white, + color: tileColor ?? StreamChatTheme.of(context).colorTheme.white, child: Container( height: 63.0, child: InkWell( onTap: onTap, child: Row( children: [ - if (leading != null) - Expanded( - child: Center(child: leading), - ), + if (leading != null) Center(child: leading), if (leading == null) SizedBox( width: 16.0, @@ -45,14 +49,15 @@ class OptionListTile extends StatelessWidget { flex: 4, child: Text( title, - style: titleColor == null - ? StreamChatTheme.of(context).textTheme.bodyBold - : StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: titleColor, - ), + style: titleTextStyle ?? + (titleColor == null + ? StreamChatTheme.of(context).textTheme.bodyBold + : StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: titleColor, + )), ), ), Expanded( diff --git a/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart index 63eef658..a8c66957 100644 --- a/lib/src/stream_svg_icon.dart +++ b/lib/src/stream_svg_icon.dart @@ -781,4 +781,16 @@ class StreamSvgIcon extends StatelessWidget { height: size, ); } + + factory StreamSvgIcon.Icon_user_settings({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'Icon_user_settings.svg', + color: color, + width: size, + height: size, + ); + } } diff --git a/lib/src/video_attachment.dart b/lib/src/video_attachment.dart index 21ede87a..83763aee 100644 --- a/lib/src/video_attachment.dart +++ b/lib/src/video_attachment.dart @@ -14,6 +14,7 @@ class VideoAttachment extends StatefulWidget { final MessageTheme messageTheme; final Size size; final Message message; + final ShowMessageCallback onShowMessage; VideoAttachment({ Key key, @@ -21,6 +22,7 @@ class VideoAttachment extends StatefulWidget { @required this.messageTheme, this.message, this.size, + this.onShowMessage, }) : super(key: key); @override @@ -95,6 +97,7 @@ class _VideoAttachmentState extends State { userName: widget.message.user.name, sentAt: widget.message.createdAt, message: widget.message, + onShowMessage: widget.onShowMessage, ), ), ), diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index d2dfc692..09eab2a4 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -43,4 +43,6 @@ export 'src/message_search_bloc.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/unread_indicator.dart'; -export 'src/chat_info_screen.dart'; +export 'src/option_list_tile.dart'; +export 'src/channel_file_display_screen.dart'; +export 'src/channel_media_display_screen.dart'; diff --git a/lib/svgs/Icon_user_settings.svg b/lib/svgs/Icon_user_settings.svg new file mode 100644 index 00000000..36ce6115 --- /dev/null +++ b/lib/svgs/Icon_user_settings.svg @@ -0,0 +1,5 @@ + + + + +