diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index f4894b02..d2c7eb04 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -1,193 +1,208 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import '../stream_chat_flutter.dart'; import 'channel_info.dart'; -import 'channel_name.dart'; -import 'stream_chat.dart'; -import 'stream_chat_theme.dart'; -import 'user_avatar.dart'; +import 'option_list_tile.dart'; -class ChannelBottomSheet extends StatelessWidget { - const ChannelBottomSheet({ - Key key, - }) : super(key: key); +class ChannelBottomSheet extends StatefulWidget { + VoidCallback onViewInfoTap; + + ChannelBottomSheet({this.onViewInfoTap}); + @override + _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); +} + +class _ChannelBottomSheetState extends State { @override Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; - 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: StreamChannel( - showLoading: false, - channel: channel, - child: ChannelName( - 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(), - if (channel.isGroup && !channel.isDistinct) - ListTile( - leading: StreamSvgIcon.userRemove( - size: 24, - color: StreamChatTheme.of(context).colorTheme.grey, - ), - title: Text( - 'Leave Group', - style: TextStyle(fontWeight: FontWeight.bold), - ), - onTap: () async { - final confirm = await showConfirmationDialog( - context, - title: 'Leave Group', - okText: 'LEAVE', - question: 'Are you sure you want to leave this group?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.userRemove( - color: Colors.red, - ), - ); - if (confirm == true) { - await channel - .removeMembers([StreamChat.of(context).user.id]); - Navigator.pop(context); - } - }, - ), - if ([ - 'admin', - 'owner', - ].contains(channel.state.members - .firstWhere((m) => m.userId == channel.client.state.user.id, - orElse: () => null) - ?.role)) - ListTile( - leading: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, - size: 24, - ), - title: Text( - 'Delete chat', - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.accentRed, - ), - ), - onTap: () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( - color: StreamChatTheme.of(context).colorTheme.accentRed, - ), - ); - var channel = StreamChannel.of(context).channel; - if (res == true) { - await channel.delete().then((value) { - Navigator.pop(context); - }); - } - }, - ), - ], + var channel = StreamChannel.of(context).channel; + + var members = channel.state.members; + + var userAsMember = + members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id); + var isOwner = userAsMember.role == 'owner'; + + return Material( + color: StreamChatTheme.of(context).colorTheme.white, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), ), ), - ); - } - - 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, + children: [ + SizedBox( + height: 24.0, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: ChannelName( + textStyle: StreamChatTheme.of(context).textTheme.headlineBold, + ), ), - child: Column( - children: [ + ), + SizedBox( + height: 5.0, + ), + Center( + child: ChannelInfo( + showTypingIndicator: false, + channel: StreamChannel.of(context).channel, + textStyle: + StreamChatTheme.of(context).channelPreviewTheme.subtitle, + ), + ), + SizedBox( + height: 17.0, + ), + if (channel.isDistinct && channel.memberCount == 2) + Column( + children: [ UserAvatar( - showOnlineStatus: true, - user: m.user, - borderRadius: BorderRadius.circular(32), - constraints: BoxConstraints.tight( - Size.square(64), + user: members + .firstWhere((e) => e.user.id != userAsMember.user.id) + .user, + constraints: BoxConstraints( + maxHeight: 64.0, + maxWidth: 64.0, ), + borderRadius: BorderRadius.circular(32.0), ), - 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, - ), - ), + SizedBox( + height: 6.0, + ), + Text( + members + .firstWhere((e) => e.user.id != userAsMember.user.id) + .user + .name, + style: StreamChatTheme.of(context).textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), - ); - }).toList(), + if (!(channel.isDistinct && channel.memberCount == 2)) + Container( + height: 94.0, + alignment: Alignment.center, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: members.length, + shrinkWrap: true, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + children: [ + UserAvatar( + user: members[index].user, + constraints: BoxConstraints( + maxHeight: 64.0, + maxWidth: 64.0, + ), + borderRadius: BorderRadius.circular(32.0), + ), + SizedBox( + height: 6.0, + ), + Text( + members[index].user.name, + style: StreamChatTheme.of(context) + .textTheme + .footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + }, + ), + ), + SizedBox( + height: 24.0, + ), + OptionListTile( + leading: 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, + ), + title: 'Leave Group', + onTap: () async { + _showLeaveDialog(); + }, + ), + if (isOwner) + OptionListTile( + leading: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + title: 'Delete Conversation', + titleColor: StreamChatTheme.of(context).colorTheme.accentRed, + onTap: () async { + _showDeleteDialog(); + }, + ), + OptionListTile( + leading: StreamSvgIcon.close_small( + color: StreamChatTheme.of(context).colorTheme.grey, + ), + title: 'Cancel', + onTap: () { + Navigator.pop(context); + }, + ), + ], ), ); } + + void _showDeleteDialog() async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + ); + var channel = StreamChannel.of(context).channel; + if (res == true) { + await channel.delete(); + Navigator.pop(context); + } + } + + void _showLeaveDialog() async { + final res = await showConfirmationDialog( + context, + title: 'Leave conversation', + okText: 'LEAVE', + question: 'Are you sure you want to leave this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.userRemove( + color: StreamChatTheme.of(context).colorTheme.accentRed, + ), + ); + var channel = StreamChannel.of(context).channel; + if (res == true) { + await channel.removeMembers([StreamChat.of(context).user.id]); + Navigator.pop(context); + } + } } diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 777fae57..273a112a 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -567,7 +567,27 @@ class _ChannelListViewState extends State context: context, builder: (context) { return StreamChannel( - child: ChannelBottomSheet(), + 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 + }, + ), channel: channel, ); }, diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 7b86bf56..c0c6f558 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -162,12 +162,18 @@ class ChannelPreview extends StatelessWidget { String stringDate; final now = DateTime.now(); - if (now.year != lastMessageAt.year || - now.month != lastMessageAt.month || - now.day != lastMessageAt.day) { - stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); - } else { + var startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { + stringDate = 'Yesterday'; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy'); } return Text( diff --git a/lib/src/channel_unread_indicator.dart b/lib/src/channel_unread_indicator.dart index 8080b1e9..c15db13a 100644 --- a/lib/src/channel_unread_indicator.dart +++ b/lib/src/channel_unread_indicator.dart @@ -19,6 +19,7 @@ class ChannelUnreadIndicator extends StatelessWidget { if (!snapshot.hasData || snapshot.data == 0) { return SizedBox(); } + return Material( borderRadius: BorderRadius.circular(8), color: StreamChatTheme.of(context) @@ -33,7 +34,7 @@ class ChannelUnreadIndicator extends StatelessWidget { ), child: Center( child: Text( - '${snapshot.data}', + '${snapshot.data > 99 ? '99+' : snapshot.data}', style: TextStyle( fontSize: 11, color: Colors.white, diff --git a/lib/src/option_list_tile.dart b/lib/src/option_list_tile.dart new file mode 100644 index 00000000..d551d33e --- /dev/null +++ b/lib/src/option_list_tile.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; + +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.greyGainsboro, + height: 1.0, + ), + Material( + color: 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) + SizedBox( + width: 16.0, + ), + Expanded( + flex: 4, + child: Text( + title, + style: titleColor == null + ? StreamChatTheme.of(context).textTheme.bodyBold + : StreamChatTheme.of(context) + .textTheme + .bodyBold + .copyWith( + color: titleColor, + ), + ), + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Align( + alignment: Alignment.centerRight, + child: trailing ?? Container(), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 1cef26a6..d2dfc692 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -43,3 +43,4 @@ 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';