From 2541d5e4746066b707140f8b9d798e391c60e163 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 17 Jul 2020 16:48:12 +0200 Subject: [PATCH] add channel bottom sheet --- lib/src/channel_bottom_sheet.dart | 148 ++++++++++++++++++++++++++++++ lib/src/channel_header.dart | 26 ++---- lib/src/channel_image.dart | 14 ++- lib/src/channel_info.dart | 59 ++++++++++++ lib/src/channel_list_view.dart | 66 ++++++++++--- lib/src/channel_preview.dart | 118 +++++++++++++----------- lib/src/user_avatar.dart | 11 ++- 7 files changed, 346 insertions(+), 96 deletions(-) create mode 100644 lib/src/channel_bottom_sheet.dart create mode 100644 lib/src/channel_info.dart diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart new file mode 100644 index 00000000..c3057d31 --- /dev/null +++ b/lib/src/channel_bottom_sheet.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:line_awesome_icons/line_awesome_icons.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'channel_info.dart'; +import 'channel_name.dart'; +import 'stream_chat.dart'; +import 'stream_chat_theme.dart'; +import 'user_avatar.dart'; + +class ChannelBottomSheet extends StatelessWidget { + const ChannelBottomSheet({ + Key key, + @required this.channel, + }) : super(key: key); + + final Channel channel; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, + ), + child: Center( + child: ChannelName( + channel: channel, + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.title, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, + ), + child: Center( + child: ChannelInfo( + channel: channel, + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.subtitle, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + ), + child: Center( + child: StreamBuilder>( + stream: channel.state.membersStream.map((event) => event + .where((m) => m.userId != StreamChat.of(context).user.id) + .toList()), + initialData: channel.state.members + .where((m) => m.userId != StreamChat.of(context).user.id) + .toList(), + builder: _buildMembers, + ), + ), + ), + Divider(), + StreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, snapshot) { + return SwitchListTile( + onChanged: (bool muted) async { + if (muted) { + await channel.mute(); + } else { + await channel.unmute(); + } + }, + value: snapshot.data, + title: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 22.0), + child: Icon(LineAwesomeIcons.volume_off), + ), + Text('Mute'), + ], + ), + ); + }), + ], + ), + ), + ); + } + + Widget _buildMembers( + BuildContext context, + AsyncSnapshot> snapshot, + ) { + if (snapshot.data.isEmpty) { + return SizedBox(); + } + + return Container( + height: 83, + child: ListView( + padding: EdgeInsets.only( + left: (MediaQuery.of(context).size.width / 2) - 48, + ), + scrollDirection: Axis.horizontal, + children: snapshot.data.map((m) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + ), + child: Column( + children: [ + UserAvatar( + showOnlineStatus: true, + user: m.user, + borderRadius: BorderRadius.circular(32), + constraints: BoxConstraints.tight( + Size.square(64), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: Text( + m.user.name?.split(' ')?.elementAt(0), + style: StreamChatTheme.of(context) + .channelPreviewTheme + .title + .copyWith( + fontSize: 12, + ), + ), + ), + ], + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 5a7a146c..e257fbbc 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; @@ -107,7 +107,11 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { .channelHeaderTheme .title, ), - _buildLastActive(context, channel), + ChannelInfo( + channel: channel, + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.subtitle, + ), ], ), ), @@ -115,24 +119,6 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { ); } - Widget _buildLastActive(BuildContext context, Channel channel) { - return StreamBuilder( - stream: channel.lastMessageAtStream, - initialData: channel.lastMessageAt, - builder: (context, snapshot) { - return (snapshot.data != null) - ? Text( - 'Active ${Jiffy(snapshot.data.toLocal()).fromNow()}', - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ) - : SizedBox(); - }, - ); - } - Padding _buildBackButton(BuildContext context) { return Padding( padding: const EdgeInsets.all(14.0), diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index 113d6de0..3bdc3b2a 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -51,8 +51,11 @@ class ChannelImage extends StatelessWidget { this.constraints, this.onTap, this.showOnlineStatus = true, + this.borderRadius, }) : super(key: key); + final BorderRadius borderRadius; + /// The channel to show the image of final Channel channel; @@ -84,6 +87,7 @@ class ChannelImage extends StatelessWidget { initialData: otherMember.user, builder: (context, snapshot) { return UserAvatar( + borderRadius: borderRadius, user: snapshot.data ?? otherMember.user, constraints: constraints ?? StreamChatTheme.of(context) @@ -117,10 +121,11 @@ class ChannelImage extends StatelessWidget { } return ClipRRect( - borderRadius: StreamChatTheme.of(context) - .channelPreviewTheme - .avatarTheme - .borderRadius, + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .borderRadius, child: Container( constraints: constraints ?? StreamChatTheme.of(context) @@ -131,7 +136,6 @@ class ChannelImage extends StatelessWidget { color: StreamChatTheme.of(context).accentColor, ), child: Stack( - fit: StackFit.expand, children: [ image != null ? CachedNetworkImage( diff --git a/lib/src/channel_info.dart b/lib/src/channel_info.dart new file mode 100644 index 00000000..af1915a3 --- /dev/null +++ b/lib/src/channel_info.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class ChannelInfo extends StatelessWidget { + final Channel channel; + + /// The style of the text displayed + final TextStyle textStyle; + + const ChannelInfo({ + Key key, + @required this.channel, + this.textStyle, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (channel.memberCount > 2) { + return Text( + '${channel.memberCount} Members, ${channel.state.watcherCount} Online', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ); + } else { + return StreamBuilder>( + stream: channel.state.membersStream, + initialData: channel.state.members, + builder: (context, snapshot) { + final otherMember = snapshot.data.firstWhere( + (element) => element.userId != StreamChat.of(context).user.id, + orElse: () => null, + ); + + if (otherMember == null) { + return SizedBox(); + } + + if (otherMember.user.online) { + return Text( + 'Online', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ); + } + + return Text( + 'Last seen ${Jiffy(otherMember.user.lastActive).fromNow()}', + style: textStyle, + ); + }, + ); + } + } +} diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 9b4cab61..3329382f 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/channels_bloc.dart'; import '../stream_chat_flutter.dart'; +import 'channel_bottom_sheet.dart'; import 'channel_preview.dart'; import 'stream_channel.dart'; import 'stream_chat.dart'; @@ -60,11 +62,15 @@ class ChannelListView extends StatefulWidget { this.channelPreviewBuilder, this.errorBuilder, this.onImageTap, + this.swipeToAction = false, }) : super(key: key); /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; + /// If true a default swipe to action behaviour will be added to this widget + final bool swipeToAction; + /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. @@ -274,17 +280,55 @@ class _ChannelListViewState extends State ], ); } else { - child = ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: widget.onImageTap != null - ? () { - widget.onImageTap(channel); - } - : null, - onTap: (channel) { - onTap(channel, widget.channelWidget); - }, + child = Slidable( + enabled: widget.swipeToAction, + actionPane: SlidableBehindActionPane(), + actionExtentRatio: 0.12, + closeOnScroll: true, + secondaryActions: [ + IconSlideAction( + color: Color(0xffEBEBEB), + icon: Icons.more_horiz, + onTap: () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) { + return ChannelBottomSheet(channel: channel); + }, + ); + }, + ), + IconSlideAction( + color: Color(0xffEBEBEB), + icon: Icons.notifications_none, + ), + IconSlideAction( + color: Color(0xffEBEBEB), + icon: Icons.delete, + ), + ], + child: Container( + color: StreamChatTheme.of(context).backgroundColor, + child: ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: widget.onImageTap != null + ? () { + widget.onImageTap(channel); + } + : null, + onTap: (channel) { + onTap(channel, widget.channelWidget); + }, + ), + ), ); } return child; diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 31a986ce..de6c8222 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -43,65 +43,71 @@ class ChannelPreview extends StatelessWidget { @override Widget build(BuildContext context) { - return Opacity( - opacity: channel.isMuted ? 0.5 : 1, - child: ListTile( - onTap: () { - if (onTap != null) { - onTap(channel); - } - }, - onLongPress: () { - if (onLongPress != null) { - onLongPress(channel); - } - }, - leading: ChannelImage( - onTap: onImageTap, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: ChannelName( - textStyle: - StreamChatTheme.of(context).channelPreviewTheme.title, - ), - ), - if (channel.state.unreadCount > 0) - UnreadIndicator( - channel: channel, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: _buildSubtitle(context)), - Builder( - builder: (context) { - if (channel.state.lastMessage?.user?.id == - StreamChat.of(context).user.id) { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: SendingIndicator( - message: channel.state.lastMessage, - allRead: channel.state.read - .where((element) => element.lastRead - .isAfter(channel.state.lastMessage.createdAt)) - .length == - channel.memberCount - 1, - ), - ); + return StreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, snapshot) { + return Opacity( + opacity: snapshot.data ? 0.5 : 1, + child: ListTile( + onTap: () { + if (onTap != null) { + onTap(channel); } - return SizedBox(); }, + onLongPress: () { + if (onLongPress != null) { + onLongPress(channel); + } + }, + leading: ChannelImage( + onTap: onImageTap, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: ChannelName( + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.title, + ), + ), + if (channel.state.unreadCount > 0) + UnreadIndicator( + channel: channel, + ), + ], + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: _buildSubtitle(context)), + Builder( + builder: (context) { + if (channel.state.lastMessage?.user?.id == + StreamChat.of(context).user.id) { + return Padding( + padding: const EdgeInsets.only(right: 4.0), + child: SendingIndicator( + message: channel.state.lastMessage, + allRead: channel.state.read + .where((element) => element.lastRead + .isAfter(channel + .state.lastMessage.createdAt)) + .length == + channel.memberCount - 1, + ), + ); + } + return SizedBox(); + }, + ), + _buildDate(context), + ], + ), ), - _buildDate(context), - ], - ), - ), - ); + ); + }); } Widget _buildDate(BuildContext context) { diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 33e8ab90..a3d5b6f5 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -12,10 +12,12 @@ class UserAvatar extends StatelessWidget { this.onlineIndicatorConstraints, this.onTap, this.showOnlineStatus = true, + this.borderRadius, }) : super(key: key); final User user; final BoxConstraints constraints; + final BorderRadius borderRadius; final BoxConstraints onlineIndicatorConstraints; final void Function(User) onTap; final bool showOnlineStatus; @@ -33,10 +35,11 @@ class UserAvatar extends StatelessWidget { child: Stack( children: [ ClipRRect( - borderRadius: StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .borderRadius, + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .borderRadius, child: Container( constraints: constraints ?? StreamChatTheme.of(context)