add channel bottom sheet

This commit is contained in:
Salvatore Giordano
2020-07-17 16:48:12 +02:00
parent 7b46992e7b
commit 2541d5e474
7 changed files with 346 additions and 96 deletions
+148
View File
@@ -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: <Widget>[
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<List<Member>>(
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<bool>(
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: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 22.0),
child: Icon(LineAwesomeIcons.volume_off),
),
Text('Mute'),
],
),
);
}),
],
),
),
);
}
Widget _buildMembers(
BuildContext context,
AsyncSnapshot<List<Member>> 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: <Widget>[
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(),
),
);
}
}
+6 -20
View File
@@ -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<DateTime>(
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),
+9 -5
View File
@@ -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: <Widget>[
image != null
? CachedNetworkImage(
+59
View File
@@ -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<List<Member>>(
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,
);
},
);
}
}
}
+55 -11
View File
@@ -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<ChannelListView>
],
);
} 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: <Widget>[
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;
+62 -56
View File
@@ -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: <Widget>[
Flexible(
child: ChannelName(
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
if (channel.state.unreadCount > 0)
UnreadIndicator(
channel: channel,
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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<bool>(
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: <Widget>[
Flexible(
child: ChannelName(
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
if (channel.state.unreadCount > 0)
UnreadIndicator(
channel: channel,
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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) {
+7 -4
View File
@@ -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: <Widget>[
ClipRRect(
borderRadius: StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)