diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 8ec56ee7..91797672 100644 Binary files a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 2f7fdaa7..103da98b 100644 Binary files a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 2c20055d..1718335e 100644 Binary files a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 89492aaa..a111f507 100644 Binary files a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index dd30e06e..fc254c25 100644 Binary files a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/assets/android_icon.png b/example/assets/android_icon.png new file mode 100644 index 00000000..3b85d6c3 Binary files /dev/null and b/example/assets/android_icon.png differ diff --git a/example/assets/icon.png b/example/assets/ios_icon.png similarity index 100% rename from example/assets/icon.png rename to example/assets/ios_icon.png diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 53d6d58a..7e63f46d 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -58,8 +58,6 @@ remote-notification - UIUserInterfaceStyle - Light UIViewControllerBasedStatusBarAppearance diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 483e0902..e24bd5da 100644 --- a/example/lib/advanced_options_page.dart +++ b/example/lib/advanced_options_page.dart @@ -350,8 +350,7 @@ class _AdvancedOptionsPageState extends State { return MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), - //TODO change to system once dark theme is implemented - themeMode: ThemeMode.light, + themeMode: ThemeMode.system, builder: (context, widget) { return StreamChat( child: widget, diff --git a/example/lib/main.dart b/example/lib/main.dart index 17f497a4..84084dba 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -60,7 +60,10 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, + themeMode: + WidgetsBinding.instance.window.platformBrightness == Brightness.light + ? ThemeMode.light + : ThemeMode.dark, onGenerateRoute: AppRoutes.generateRoute, initialRoute: client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 944be796..4f76c695 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.102+105 +version: 1.0.106+109 environment: sdk: ">=2.2.2 <3.0.0" @@ -22,7 +22,7 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - flutter_launcher_icons: ^0.8.0 + flutter_launcher_icons: ^0.8.1 test: any flutter: @@ -34,4 +34,5 @@ flutter: flutter_icons: android: true ios: true - image_path: "assets/icon.png" + image_path_android: "assets/android_icon.png" + image_path_ios: "assets/ios_icon.png" 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..1988b9e1 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -380,6 +380,10 @@ class _ChannelListViewState extends State width: 40, ), ), + contentPadding: const EdgeInsets.only( + left: 8, + right: 8, + ), title: Align( alignment: Alignment.centerLeft, child: Container( @@ -394,6 +398,7 @@ class _ChannelListViewState extends State ), ), subtitle: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Align( alignment: Alignment.centerLeft, @@ -567,7 +572,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..7f976688 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -130,11 +130,14 @@ class ChannelPreview extends StatelessWidget { .lastMessageAt .fontSize, isMessageRead: channel.state.read - .where((element) => element.lastRead + ?.where((element) => + element.user.id != + channel.client.state.user.id) + ?.where((element) => element.lastRead .isAfter(channel .state.lastMessage.createdAt)) - .length == - ((channel.memberCount ?? 0) - 1), + ?.isNotEmpty == + true, ), ); } @@ -162,12 +165,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/image_group.dart b/lib/src/image_group.dart index 7bce22f9..e9b52f88 100644 --- a/lib/src/image_group.dart +++ b/lib/src/image_group.dart @@ -1,6 +1,4 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:carousel_slider/carousel_options.dart'; -import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart'; @@ -81,12 +79,10 @@ class ImageGroup extends StatelessWidget { color: Colors.black38, child: Center( child: Text( - '${images.length - 4} +', + '+ ${images.length - 4}', style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .white, - fontSize: 28, + color: Colors.white, + fontSize: 26, ), ), ), diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 14a49255..7cd68eb3 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -186,10 +186,10 @@ class _MessageListViewState extends State { bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; int _messageListLength; + StreamChannelState streamChannel; int get _initialIndex { if (widget.initialScrollIndex != null) return widget.initialScrollIndex; - final streamChannel = StreamChannel.of(context); if (streamChannel.initialMessageId != null) { final messages = streamChannel.channel.state.messages; final totalMessages = messages.length; @@ -209,11 +209,10 @@ class _MessageListViewState extends State { } bool _isInitialMessage(String id) { - final streamChannel = StreamChannel.of(context); return streamChannel.initialMessageId == id; } - bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate; + bool get _upToDate => streamChannel.channel.state.isUpToDate; bool _topPaginationActive = false; bool _bottomPaginationActive = false; @@ -229,270 +228,259 @@ class _MessageListViewState extends State { @override Widget build(BuildContext context) { - final streamChannel = StreamChannel.of(context); - final messagesStream = widget.parentMessage != null ? streamChannel.channel.state.threadsStream .where((threads) => threads.containsKey(widget.parentMessage.id)) .map((threads) => threads[widget.parentMessage.id]) : streamChannel.channel.state?.messagesStream; - return WillPopScope( - onWillPop: () async { - if (!_upToDate) { - await streamChannel.reloadChannel(); - } - return true; - }, - child: StreamBuilder>( - stream: messagesStream?.map((messages) => messages - ?.where((e) => - !e.isDeleted || - (e.isDeleted && - e.user.id == streamChannel.channel.client.state.user.id)) - ?.toList()), - builder: (context, snapshot) { - if (!snapshot.hasData) { + return StreamBuilder>( + stream: messagesStream?.map((messages) => messages + ?.where((e) => + !e.isDeleted || + (e.isDeleted && + e.user.id == streamChannel.channel.client.state.user.id)) + ?.toList()), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: const CircularProgressIndicator(), + ); + } + + final messageList = snapshot.data?.reversed?.toList() ?? []; + if (messageList.isEmpty) { + if (_upToDate) { return Center( - child: const CircularProgressIndicator(), + child: Text( + 'No chats here yet...', + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5)), + ), ); } + } else { + messages = messageList; + } - final messageList = snapshot.data?.reversed?.toList() ?? []; - if (messageList.isEmpty) { - if (_upToDate) { - return Center( - child: Text( - 'No chats here yet...', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5)), - ), - ); - } - } else { - messages = messageList; - } + final newMessagesListLength = messages.length; - final newMessagesListLength = messages.length; - - if (_messageListLength != null) { - if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == - true) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; - } + if (_messageListLength != null) { + if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == + true) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; } + } - _messageListLength = newMessagesListLength; + _messageListLength = newMessagesListLength; - return Stack( - alignment: Alignment.center, - children: [ - LazyLoadScrollView( - onStartOfPage: () async { - _inBetweenList = false; - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData( - streamChannel, - QueryDirection.bottom, - ); - } - }, - onEndOfPage: () async { - _inBetweenList = false; - _topPaginationActive = true; - _bottomPaginationActive = false; + return Stack( + alignment: Alignment.center, + children: [ + LazyLoadScrollView( + onStartOfPage: () async { + _inBetweenList = false; + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; return _paginateData( streamChannel, - QueryDirection.top, + QueryDirection.bottom, ); - }, - onInBetweenOfPage: () { - _inBetweenList = true; - }, - child: ScrollablePositionedList.builder( - key: ValueKey(initialIndex + initialAlignment), - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 2 + - (widget.parentMessage != null ? 1 : 0), - itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( - context, - widget.parentMessage, - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildParentMessage(widget.parentMessage), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - StreamChatTheme.of(context) - .colorTheme - .whiteSmoke, - StreamChatTheme.of(context) - .colorTheme - .whiteSnow, - ], - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', - textAlign: TextAlign.center, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ), - ), - ], - ); - } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; - final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; - - Widget messageWidget; - - if (i == 1) { - messageWidget = _buildBottomMessage( + } + }, + onEndOfPage: () async { + _inBetweenList = false; + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; + }, + child: ScrollablePositionedList.builder( + key: ValueKey(initialIndex + initialAlignment), + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 2 + + (widget.parentMessage != null ? 1 : 0), + itemBuilder: (context, i) { + if (i == messages.length + 2) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, + widget.parentMessage, ); } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (context) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); - } - } - - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), Units.DAY)) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - messageWidget, - Padding( - padding: - const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), + buildParentMessage(widget.parentMessage), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + StreamChatTheme.of(context) + .colorTheme + .whiteSmoke, + StreamChatTheme.of(context) + .colorTheme + .whiteSnow, + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', + textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ), ), ], ); } + } + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); + } + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; - return messageWidget; - }, - ), + Widget messageWidget; + + if (i == 1) { + messageWidget = _buildBottomMessage( + context, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, + ); + } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (context) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.DAY)) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + messageWidget, + Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), + ), + ], + ); + } + + return messageWidget; + }, ), - if (widget.showScrollToBottom) _buildScrollToBottom(), - Positioned( - top: 20.0, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { - final items = _itemPositionListener.itemPositions?.value; - if (items.isEmpty || messages.isEmpty) { - return SizedBox(); - } + ), + if (widget.showScrollToBottom) _buildScrollToBottom(), + Positioned( + top: 20.0, + child: ValueListenableBuilder>( + valueListenable: _itemPositionListener.itemPositions, + builder: (context, values, _) { + final items = _itemPositionListener.itemPositions?.value; + if (items.isEmpty || messages.isEmpty) { + return SizedBox(); + } - var index = _getTopElement(values).index; + var index = _getTopElement(values).index; - if (index > messages.length) { - return SizedBox(); - } + if (index > messages.length) { + return SizedBox(); + } - if (index == messages.length) { - index = max(index - 1, 0); - } + if (index == messages.length) { + index = max(index - 1, 0); + } - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); - }, - ), + return widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + messages[index].createdAt.toLocal(), + ) + : DateDivider( + dateTime: messages[index].createdAt.toLocal(), + ); + }, ), - ], - ); - }), - ); + ), + ], + ); + }); } Future _paginateData( @@ -512,7 +500,6 @@ class _MessageListViewState extends State { } Widget _buildScrollToBottom() { - final streamChannel = StreamChannel.of(context); return StreamBuilder>( stream: Rx.combineLatest2( streamChannel.channel.state.isUpToDateStream, @@ -758,7 +745,7 @@ class _MessageListViewState extends State { final isNextUser = index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; - final channel = StreamChannel.of(context).channel; + final channel = streamChannel.channel; final readList = channel.state?.read ?.where((element) => element.user.id != userId) ?.where((read) => @@ -864,7 +851,7 @@ class _MessageListViewState extends State { _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); - final streamChannel = StreamChannel.of(context); + streamChannel = StreamChannel.of(context); initialIndex = _initialIndex; initialAlignment = _initialAlignment; @@ -907,16 +894,13 @@ class _MessageListViewState extends State { context, MaterialPageRoute(builder: (_) { return StreamBuilder( - stream: StreamChannel.of(context) - .channel - .state - .messagesStream - .map((messages) => + stream: streamChannel.channel.state.messagesStream.map( + (messages) => messages.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, snapshot) { return StreamChannel( - channel: StreamChannel.of(context).channel, + channel: streamChannel.channel, child: widget.threadBuilder(context, snapshot.data), ); }); @@ -928,6 +912,9 @@ class _MessageListViewState extends State { @override void dispose() { + if (!_upToDate) { + streamChannel.reloadChannel(); + } _messageNewListener?.cancel(); super.dispose(); } diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index a344068f..36364416 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -855,12 +855,13 @@ class _MessageWidgetState extends State { if (isMessageRead) { child = Row( children: [ - Text( - widget.readList.length.toString(), - style: style.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + if (StreamChannel.of(context).channel.memberCount > 2) + Text( + widget.readList.length.toString(), + style: style.copyWith( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), ), - ), SizedBox(width: 2), child, ], 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';