diff --git a/example/lib/threads.dart b/example/lib/threads.dart index a06c6890..20481baf 100644 --- a/example/lib/threads.dart +++ b/example/lib/threads.dart @@ -22,15 +22,6 @@ class MyApp extends StatelessWidget { return MaterialApp( home: Container( child: StreamChat( - streamChatThemeData: StreamChatThemeData( - accentColor: Colors.red, - channelTheme: ChannelTheme( - inputGradient: LinearGradient(colors: [ - Colors.red, - Colors.yellow, - ]), - ), - ), client: client, child: ChannelListPage(), ), @@ -104,7 +95,9 @@ class ThreadPage extends StatelessWidget { parentMessage: parent, ), ), - MessageInput(), + MessageInput( + parentMessage: parent, + ), ], ), ); diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index a11b2d54..0576e867 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -8,11 +8,11 @@ class MessageInput extends StatefulWidget { MessageInput({ Key key, this.onMessageSent, - this.parent, + this.parentMessage, }) : super(key: key); final void Function(Message) onMessageSent; - final Message parent; + final Message parentMessage; @override _MessageInputState createState() => _MessageInputState(); @@ -45,7 +45,10 @@ class _MessageInputState extends State { decoration: BoxDecoration( color: StreamChatTheme.of(context).channelTheme.inputBackground, borderRadius: BorderRadius.circular(10.0), - border: Border.all(color: Colors.black.withOpacity(.2)), + border: Border.all( + color: _typingStarted + ? Colors.transparent + : Colors.black.withOpacity(.2)), ), child: Flex( direction: Axis.horizontal, @@ -127,7 +130,7 @@ class _MessageInputState extends State { .channel .sendMessage( Message( - parentId: widget.parent?.id, + parentId: widget.parentMessage?.id, text: text, ), ) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 35bfbe16..75092334 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -40,7 +40,6 @@ class _MessageListViewState extends State { bool _topWasVisible = false; List _messages = []; List _newMessageList = []; - Function _onThreadTap; @override @@ -79,9 +78,10 @@ class _MessageListViewState extends State { key: ValueKey( 'PARENT-MESSAGE-${widget.parentMessage.id}'), previousMessage: null, - message: widget.parentMessage.copyWith(replyCount: 0), + message: widget.parentMessage, nextMessage: null, onThreadTap: _onThreadTap, + isParent: true, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), @@ -306,7 +306,7 @@ class _MessageListViewState extends State { void _getOnThreadTap() { if (widget.onThreadTap != null) { - _onThreadTap = (message) { + _onThreadTap = (Message message) { widget.onThreadTap( message, widget.threadBuilder != null @@ -314,14 +314,24 @@ class _MessageListViewState extends State { : null); }; } else if (widget.threadBuilder != null) { - _onThreadTap = (message) { + _onThreadTap = (Message message) { Navigator.push( context, MaterialPageRoute(builder: (_) { - return StreamChannel( - channel: StreamChannel.of(context).channel, - child: widget.threadBuilder(context, message), - ); + return StreamBuilder( + stream: StreamChannel.of(context) + .channel + .state + .messagesStream + .map((messages) => + messages.firstWhere((m) => m.id == message.id)), + initialData: message, + builder: (_, snapshot) { + return StreamChannel( + channel: StreamChannel.of(context).channel, + child: widget.threadBuilder(context, snapshot.data), + ); + }); }), ); }; diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c6a1cc2c..d9d83ce3 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:date_format/date_format.dart'; @@ -21,12 +23,14 @@ class MessageWidget extends StatefulWidget { @required this.message, @required this.nextMessage, this.onThreadTap, + this.isParent = false, }) : super(key: key); final Message previousMessage; final Message message; final Message nextMessage; final void Function(Message) onThreadTap; + final bool isParent; @override _MessageWidgetState createState() => _MessageWidgetState(); @@ -63,7 +67,7 @@ class _MessageWidgetState extends State isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ _buildBubble(context, isMyMessage, isLastUser), - _buildThreadIndicator(context), + _buildThreadIndicator(context, isMyMessage), isNextUser ? SizedBox() : _buildTimestamp(isMyMessage, alignment), ], ), @@ -125,10 +129,33 @@ class _MessageWidgetState extends State ); } - Widget _buildThreadIndicator(BuildContext context) { + Widget _buildThreadIndicator(BuildContext context, bool isMyMessage) { + var row = [ + Text( + 'Replies: ${widget.message.replyCount}', + style: + Theme.of(context).textTheme.subtitle.copyWith(color: Colors.blue), + ), + Transform( + transform: Matrix4.rotationY(isMyMessage ? 0 : pi), + alignment: Alignment.center, + child: Icon( + Icons.subdirectory_arrow_left, + color: Colors.black12, + ), + ), + ]; + + if (!isMyMessage) { + row = row.reversed.toList(); + } + return widget.message.replyCount > 0 ? GestureDetector( onTap: () { + if (widget.isParent) { + return; + } if (widget.onThreadTap != null) { widget.onThreadTap(widget.message); } @@ -136,19 +163,7 @@ class _MessageWidgetState extends State child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( - children: [ - Text( - 'Replies: ${widget.message.replyCount}', - style: Theme.of(context) - .textTheme - .subtitle - .copyWith(color: Colors.blue), - ), - Icon( - Icons.subdirectory_arrow_left, - color: Colors.black12, - ), - ], + children: row, ), ), ) @@ -332,9 +347,16 @@ class _MessageWidgetState extends State void _showMessageBottomSheet(BuildContext context, bool isMyMessage) { showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), context: context, builder: (context) { - final fontSize = 40.0; + final fontSize = 30.0; final textStyle = TextStyle( fontSize: fontSize, ); @@ -342,42 +364,49 @@ class _MessageWidgetState extends State mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: reactionToEmoji.keys.map((reactionType) { - final ownReactionIndex = widget.message.ownReactions - .indexWhere((reaction) => reaction.type == reactionType); - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - iconSize: fontSize + 10, - icon: Text( - reactionToEmoji[reactionType], - style: textStyle, + Container( + color: Colors.black87, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: reactionToEmoji.keys.map((reactionType) { + final ownReactionIndex = widget.message.ownReactions + .indexWhere( + (reaction) => reaction.type == reactionType); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + iconSize: fontSize + 10, + icon: Text( + reactionToEmoji[reactionType], + style: textStyle, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction(reactionType); + } else { + sendReaction(reactionType); + } + }, ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction(reactionType); - } else { - sendReaction(reactionType); - } - }, - ), - ownReactionIndex != -1 - ? Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text(widget - .message.ownReactions[ownReactionIndex].score - .toString()), - ) - : SizedBox(), - ], - ); - }).toList(), + ownReactionIndex != -1 + ? Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + widget.message.ownReactions[ownReactionIndex] + .score + .toString(), + style: TextStyle(color: Colors.white), + ), + ) + : SizedBox(), + ], + ); + }).toList(), + ), ), isMyMessage ? FlatButton( @@ -399,6 +428,19 @@ class _MessageWidgetState extends State }, ) : SizedBox(), + FlatButton( + child: Padding( + padding: const EdgeInsets.all(28.0), + child: Text( + 'Start a thread', + style: Theme.of(context).textTheme.headline, + ), + ), + onPressed: () { + Navigator.pop(context); + widget.onThreadTap(widget.message); + }, + ), ], ); }); diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index f8fbd449..25b77794 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -69,10 +69,15 @@ class StreamChannelState extends State { Future getReplies(String parentId) async { _queryMessageController.add(true); + print('PARENT $parentId'); String firstId; if (widget.channel.state.threads.containsKey(parentId)) { - firstId = widget.channel.state.threads[parentId].first.id; + final thread = widget.channel.state.threads[parentId]; + + if (thread != null && thread.isNotEmpty) { + firstId = thread?.first?.id; + } } return widget.channel diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index 23200939..45e07723 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -51,6 +51,8 @@ class StreamChatState extends State { builder: (context) => Theme( data: Theme.of(context).copyWith( accentColor: StreamChatTheme.of(context).accentColor, + scaffoldBackgroundColor: theme.primaryColor, + backgroundColor: theme.primaryColor, ), child: WillPopScope( onWillPop: () async { @@ -82,8 +84,21 @@ class StreamChatState extends State { final theme = defaultTheme.copyWith( primaryColor: widget.streamChatThemeData?.primaryColor, channelTheme: defaultTheme.channelTheme.copyWith( - channelHeaderTheme: - widget.streamChatThemeData?.channelTheme?.channelHeaderTheme, + channelHeaderTheme: defaultTheme.channelTheme.channelHeaderTheme + .copyWith( + color: widget.streamChatThemeData?.channelTheme + ?.channelHeaderTheme?.color, + lastMessageAt: widget.streamChatThemeData?.channelTheme + ?.channelHeaderTheme?.lastMessageAt, + title: widget.streamChatThemeData?.channelTheme + ?.channelHeaderTheme?.title, + avatarTheme: + defaultTheme.channelPreviewTheme.avatarTheme.copyWith( + constraints: widget.streamChatThemeData?.channelTheme + ?.channelHeaderTheme?.avatarTheme?.constraints, + borderRadius: widget.streamChatThemeData?.channelTheme + ?.channelHeaderTheme?.avatarTheme?.borderRadius, + )), inputBackground: widget.streamChatThemeData?.channelTheme?.inputBackground, messageInputButtonIconTheme: widget