Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feature/group-info-screen

This commit is contained in:
Sahil Kumar
2020-12-30 12:57:15 +05:30
17 changed files with 740 additions and 525 deletions
+1
View File
@@ -510,6 +510,7 @@ class ThreadPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: ThreadHeader( appBar: ThreadHeader(
parent: parent, parent: parent,
), ),
+41 -6
View File
@@ -52,6 +52,9 @@ class ChannelImage extends StatelessWidget {
this.onTap, this.onTap,
this.showOnlineStatus = true, this.showOnlineStatus = true,
this.borderRadius, this.borderRadius,
this.selected = false,
this.selectionColor = const Color(0xFF006CFF),
this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
final BorderRadius borderRadius; final BorderRadius borderRadius;
@@ -67,6 +70,12 @@ class ChannelImage extends StatelessWidget {
final bool showOnlineStatus; final bool showOnlineStatus;
final bool selected;
final Color selectionColor;
final double selectionThickness;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final streamChat = StreamChat.of(context); final streamChat = StreamChat.of(context);
@@ -94,11 +103,10 @@ class ChannelImage extends StatelessWidget {
.channelPreviewTheme .channelPreviewTheme
.avatarTheme .avatarTheme
.constraints, .constraints,
onTap: onTap != null onTap: onTap != null ? (_) => onTap() : null,
? (_) { selected: selected,
onTap(); selectionColor: selectionColor,
} selectionThickness: selectionThickness,
: null,
); );
}); });
} else { } else {
@@ -111,16 +119,20 @@ class ChannelImage extends StatelessWidget {
.toList(); .toList();
return GroupImage( return GroupImage(
images: images, images: images,
borderRadius: borderRadius,
constraints: constraints ?? constraints: constraints ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
.avatarTheme .avatarTheme
.constraints, .constraints,
onTap: onTap, onTap: onTap,
selected: selected,
selectionColor: selectionColor,
selectionThickness: selectionThickness,
); );
} }
return ClipRRect( Widget child = ClipRRect(
borderRadius: borderRadius ?? borderRadius: borderRadius ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
.channelPreviewTheme .channelPreviewTheme
@@ -169,6 +181,29 @@ class ChannelImage extends StatelessWidget {
), ),
), ),
); );
if (selected) {
child = ClipRRect(
borderRadius: (borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.constraints,
color: selectionColor,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: child,
),
),
);
}
return child;
}); });
} }
} }
+154 -70
View File
@@ -74,6 +74,8 @@ class ChannelListView extends StatefulWidget {
this.onStartChatPressed, this.onStartChatPressed,
this.swipeToAction = false, this.swipeToAction = false,
this.pullToRefresh = true, this.pullToRefresh = true,
this.crossAxisCount = 1,
this.selectedChannels = const [],
}) : super(key: key); }) : super(key: key);
/// The builder that will be used in case of error /// The builder that will be used in case of error
@@ -134,6 +136,11 @@ class ChannelListView extends StatefulWidget {
/// Callback used in the default empty list widget /// Callback used in the default empty list widget
final VoidCallback onStartChatPressed; final VoidCallback onStartChatPressed;
/// The number of children in the cross axis.
final int crossAxisCount;
final List<Channel> selectedChannels;
@override @override
_ChannelListViewState createState() => _ChannelListViewState(); _ChannelListViewState createState() => _ChannelListViewState();
} }
@@ -259,22 +266,33 @@ class _ChannelListViewState extends State<ChannelListView>
} }
if (channels.isNotEmpty) { if (channels.isNotEmpty) {
child = ListView.custom( if (widget.crossAxisCount > 1) {
physics: AlwaysScrollableScrollPhysics(), child = GridView.builder(
controller: _scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childrenDelegate: SliverChildBuilderDelegate( crossAxisCount: widget.crossAxisCount),
(context, i) { itemCount: channels.length,
return _itemBuilder(context, i, channels); physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
}, },
childCount: (channels.length * 2) + 1, );
findChildIndexCallback: (key) { } else {
final ValueKey<String> valueKey = key; child = ListView.separated(
final index = channels.indexWhere( physics: AlwaysScrollableScrollPhysics(),
(channel) => 'CHANNEL-${channel.id}' == valueKey.value); itemCount:
return index != -1 ? (index * 2) : null; channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
}, },
), itemBuilder: (context, index) {
); return _listItemBuilder(context, index, channels);
},
);
}
} }
} }
@@ -292,11 +310,13 @@ class _ChannelListViewState extends State<ChannelListView>
children: List.generate( children: List.generate(
25, 25,
(i) { (i) {
if (i % 2 != 0) { if (widget.crossAxisCount == 1) {
if (widget.separatorBuilder != null) { if (i % 2 != 0) {
return widget.separatorBuilder(context, i); if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
} }
return _separatorBuilder(context, i);
} }
return _buildLoadingItem(); return _buildLoadingItem();
}, },
@@ -305,63 +325,96 @@ class _ChannelListViewState extends State<ChannelListView>
} }
Shimmer _buildLoadingItem() { Shimmer _buildLoadingItem() {
return Shimmer.fromColors( if (widget.crossAxisCount > 1) {
baseColor: Color(0xffE5E5E5), return Shimmer.fromColors(
highlightColor: Color(0xffffffff), baseColor: Color(0xffE5E5E5),
child: ListTile( highlightColor: Color(0xffffffff),
leading: Container( child: Column(
decoration: BoxDecoration( children: [
color: Colors.white, SizedBox(
shape: BoxShape.circle, height: 4.0,
), ),
constraints: BoxConstraints.tightFor( Row(
height: 40, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
width: 40, children: [
), for (int i = 0; i < widget.crossAxisCount; i++)
Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
constraints: BoxConstraints.tightFor(
height: 70,
width: 70,
),
),
],
),
SizedBox(
height: 16.0,
),
],
), ),
title: Align( );
alignment: Alignment.centerLeft, } else {
child: Container( return Shimmer.fromColors(
baseColor: Color(0xffE5E5E5),
highlightColor: Color(0xffffffff),
child: ListTile(
leading: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(11), shape: BoxShape.circle,
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: 16, height: 40,
width: 82, width: 40,
), ),
), ),
), title: Align(
subtitle: Row( alignment: Alignment.centerLeft,
children: [ child: Container(
Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 238,
),
),
),
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(11), borderRadius: BorderRadius.circular(11),
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: 16, height: 16,
width: 42, width: 82,
), ),
), ),
], ),
subtitle: Row(
children: [
Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 238,
),
),
),
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 42,
),
),
],
),
), ),
), );
); }
} }
Widget _buildErrorWidget( Widget _buildErrorWidget(
@@ -428,20 +481,10 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} }
Widget _itemBuilder(context, int i, List<Channel> channels) { Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
if (i % 2 != 0) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
}
i = i ~/ 2;
final channelsProvider = ChannelsBloc.of(context); final channelsProvider = ChannelsBloc.of(context);
if (i < channels.length) { if (i < channels.length) {
final channel = channels[i]; final channel = channels[i];
ChannelTapCallback onTap; ChannelTapCallback onTap;
if (widget.onChannelTap != null) { if (widget.onChannelTap != null) {
onTap = widget.onChannelTap; onTap = widget.onChannelTap;
@@ -581,6 +624,47 @@ class _ChannelListViewState extends State<ChannelListView>
} }
} }
Widget _gridItemBuilder(BuildContext context, int i, List<Channel> channels) {
var channel = channels[i];
var selected = widget.selectedChannels.contains(channel);
return Container(
key: ValueKey<String>('CHANNEL-${channel.id}'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ChannelImage(
channel: channel,
borderRadius: BorderRadius.circular(32),
selected: selected,
constraints: BoxConstraints.tightFor(
width: 64,
height: 64,
),
onTap: () {
widget.onChannelTap(channel, null);
},
),
SizedBox(height: 7),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: StreamChannel(
child: ChannelName(
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
channel: channel,
),
),
],
),
);
}
Widget _buildQueryProgressIndicator( Widget _buildQueryProgressIndicator(
context, context,
ChannelsBlocState channelsProvider, ChannelsBlocState channelsProvider,
+36 -5
View File
@@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget {
@required this.images, @required this.images,
this.constraints, this.constraints,
this.onTap, this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor = const Color(0xFF006CFF),
this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
final List<String> images; final List<String> images;
final BoxConstraints constraints; final BoxConstraints constraints;
final VoidCallback onTap; final VoidCallback onTap;
final bool selected;
final BorderRadius borderRadius;
final Color selectionColor;
final double selectionThickness;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( var avatar;
final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector(
onTap: onTap, onTap: onTap,
child: ClipRRect( child: ClipRRect(
borderRadius: StreamChatTheme.of(context) borderRadius: borderRadius ??
.ownMessageTheme StreamChatTheme.of(context)
.avatarTheme .ownMessageTheme
.borderRadius, .avatarTheme
.borderRadius,
child: Container( child: Container(
constraints: constraints ?? constraints: constraints ??
StreamChatTheme.of(context) StreamChatTheme.of(context)
@@ -91,5 +103,24 @@ class GroupImage extends StatelessWidget {
), ),
), ),
); );
if (selected) {
avatar = ClipRRect(
borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
color: selectionColor,
height: 64.0,
width: 64.0,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
),
),
);
}
return avatar;
} }
} }
+79 -82
View File
@@ -16,8 +16,6 @@ import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_channel.dart';
class ImageFooter extends StatefulWidget { class ImageFooter extends StatefulWidget {
/// Callback to call when pressing the back button. /// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
@@ -64,11 +62,11 @@ class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false; bool _userSearchMode = false;
TextEditingController _searchController; TextEditingController _searchController;
TextEditingController _messageController = TextEditingController(); TextEditingController _messageController = TextEditingController();
FocusNode _messageFocusNode = FocusNode();
String _userNameQuery; String _channelNameQuery;
bool _isSearchActive = false;
Set<User> _selectedUsers = {}; List<Channel> _selectedChannels = [];
bool _loading = false; bool _loading = false;
Timer _debounce; Timer _debounce;
@@ -80,8 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) { if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() { modalSetStateCallback(() {
_userNameQuery = _searchController.text; _channelNameQuery = _searchController.text;
_isSearchActive = _userNameQuery.isNotEmpty;
}); });
} }
}); });
@@ -91,6 +88,9 @@ class _ImageFooterState extends State<ImageFooter> {
void initState() { void initState() {
super.initState(); super.initState();
_searchController = TextEditingController()..addListener(_userNameListener); _searchController = TextEditingController()..addListener(_userNameListener);
_messageFocusNode.addListener(() {
setState(() {});
});
} }
@override @override
@@ -258,7 +258,7 @@ class _ImageFooterState extends State<ImageFooter> {
); );
} }
Widget _buildShareModal(context) { void _buildShareModal(context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -266,7 +266,7 @@ class _ImageFooterState extends State<ImageFooter> {
modalSetStateCallback = modalSetState; modalSetStateCallback = modalSetState;
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: _userSearchMode top: _userSearchMode || _messageFocusNode.hasFocus
? 16.0 ? 16.0
: MediaQuery.of(context).size.height / 2, : MediaQuery.of(context).size.height / 2,
left: 8.0, left: 8.0,
@@ -282,74 +282,81 @@ class _ImageFooterState extends State<ImageFooter> {
child: Column( child: Column(
children: [ children: [
_buildTextInputSection(modalSetState), _buildTextInputSection(modalSetState),
if (_userSearchMode)
SizedBox(
height: 22.0,
),
Expanded( Expanded(
child: UserListView( child: ChannelsBloc(
selectedUsers: _selectedUsers, child: ChannelListView(
onUserTap: (user, _) { selectedChannels: _selectedChannels,
_searchController.clear(); onChannelTap: (channel, _) {
if (!_selectedUsers.contains(user)) { _searchController.clear();
modalSetState(() { if (!_selectedChannels.contains(channel)) {
_selectedUsers.add(user); modalSetState(() {
}); _selectedChannels.add(channel);
} else { });
modalSetState(() { } else {
_selectedUsers.remove(user); modalSetState(() {
}); _selectedChannels.remove(channel);
} });
}, }
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_searchController.text.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
}, },
}, crossAxisCount: 4,
sort: [ pagination: PaginationParams(
SortOption( limit: 25,
'name',
direction: 1,
), ),
], filter: {
emptyBuilder: (_) { if (_searchController.text.isNotEmpty)
return LayoutBuilder( 'name': {
builder: (context, viewportConstraints) { r'$autocomplete': _channelNameQuery,
return SingleChildScrollView( },
physics: AlwaysScrollableScrollPhysics(), 'id': {
child: ConstrainedBox( r'$ne': StreamChat.of(context).user.id,
constraints: BoxConstraints( },
minHeight: viewportConstraints.maxHeight, },
), sort: [
child: Center( SortOption(
child: Column( 'name',
children: [ direction: 1,
Padding( ),
padding: const EdgeInsets.all(24), ],
child: StreamSvgIcon.search( emptyBuilder: (_) {
size: 96, return LayoutBuilder(
color: Colors.grey, builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
), ),
), Text(
Text( 'No chat matches these keywords...'),
'No user matches these keywords...'), ],
], ),
), ),
), ),
), );
); },
}, );
); },
}, ),
), ),
), ),
if (_selectedUsers.isNotEmpty) if (_selectedChannels.isNotEmpty)
_buildShareTextInputSection(modalSetState), _buildShareTextInputSection(modalSetState),
if (!_userSearchMode && _selectedUsers.isEmpty) if (!_userSearchMode && _selectedChannels.isEmpty)
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Container( child: Container(
@@ -544,6 +551,7 @@ class _ImageFooterState extends State<ImageFooter> {
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: TextField( child: TextField(
controller: _messageController, controller: _messageController,
focusNode: _messageFocusNode,
onChanged: (val) { onChanged: (val) {
modalSetState(() {}); modalSetState(() {});
}, },
@@ -612,27 +620,16 @@ class _ImageFooterState extends State<ImageFooter> {
_messageController.clear(); _messageController.clear();
final client = StreamChat.of(context).client; for (var channel in _selectedChannels) {
for (var user in _selectedUsers) {
var c = client.channel('messaging', extraData: {
'members': [
user.id,
StreamChat.of(context).user.id,
],
});
await c.watch();
final message = Message( final message = Message(
text: text, text: text,
attachments: [attachments[widget.currentPage]], attachments: [attachments[widget.currentPage]],
); );
await c.sendMessage(message); await channel.sendMessage(message);
} }
_selectedUsers.clear(); _selectedChannels.clear();
Navigator.pop(context); Navigator.pop(context);
} }
+2 -1
View File
@@ -113,11 +113,12 @@ class MessageActionsModal extends StatelessWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showReplyIndicator: false, showThreadReplyIndicator: false,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showReactionPickerIndicator: true, showReactionPickerIndicator: true,
showInChannelIndicator: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
shape: messageShape, shape: messageShape,
), ),
+1 -1
View File
@@ -301,7 +301,7 @@ class MessageInputState extends State<MessageInput> {
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text('Send also as direct message'), child: Text('Also send as direct message'),
), ),
], ],
), ),
+24 -12
View File
@@ -241,7 +241,7 @@ class _MessageListViewState extends State<MessageListView> {
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Center( return Center(
child: CircularProgressIndicator(), child: const CircularProgressIndicator(),
); );
} }
@@ -336,18 +336,27 @@ class _MessageListViewState extends State<MessageListView> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
buildParentMessage(widget.parentMessage), buildParentMessage(widget.parentMessage),
Padding( Container(
padding: decoration: BoxDecoration(
const EdgeInsets.symmetric(horizontal: 32), gradient: LinearGradient(
child: Container( begin: Alignment.topCenter,
padding: const EdgeInsets.all(8), end: Alignment.bottomCenter,
colors: [
Color(0XFFF7F7F7),
Color(0XFFFCFCFC),
],
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'Start of thread', '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
), ),
color: Theme.of(context)
.accentColor
.withAlpha(50),
), ),
), ),
], ],
@@ -585,7 +594,7 @@ class _MessageListViewState extends State<MessageListView> {
if (!snapshot.data) { if (!snapshot.data) {
if (direction == QueryDirection.top) { if (direction == QueryDirection.top) {
return Container( return Container(
height: 50, height: 52,
width: double.infinity, width: double.infinity,
); );
} }
@@ -679,7 +688,8 @@ class _MessageListViewState extends State<MessageListView> {
final isMyMessage = message.user.id == StreamChat.of(context).user.id; final isMyMessage = message.user.id == StreamChat.of(context).user.id;
return MessageWidget( return MessageWidget(
showReplyIndicator: false, showThreadReplyIndicator: false,
showInChannelIndicator: false,
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showUsername: !isMyMessage, showUsername: !isMyMessage,
@@ -748,6 +758,8 @@ class _MessageListViewState extends State<MessageListView> {
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
top: 3, top: 3,
), ),
showInChannelIndicator: widget.parentMessage == null,
showThreadReplyIndicator: widget.parentMessage == null,
showUsername: !isMyMessage && !isNextUser, showUsername: !isMyMessage && !isNextUser,
showSendingIndicator: isMyMessage && showSendingIndicator: isMyMessage &&
(index == 0 || message.status != MessageSendingStatus.SENT) (index == 0 || message.status != MessageSendingStatus.SENT)
+1 -1
View File
@@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget {
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showUserAvatar: showUserAvatar, showUserAvatar: showUserAvatar,
showReplyIndicator: false, showThreadReplyIndicator: false,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone, showSendingIndicator: DisplayWidget.gone,
+303 -238
View File
@@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget {
final bool allRead; final bool allRead;
/// If true the widget will show the reply indicator /// If true the widget will show the thread reply indicator
final bool showReplyIndicator; final bool showThreadReplyIndicator;
/// If true the widget will show the show in channel indicator
final bool showInChannelIndicator;
/// The function called when tapping on UserAvatar /// The function called when tapping on UserAvatar
final void Function(User) onUserAvatarTap; final void Function(User) onUserAvatarTap;
@@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget {
/// Center user avatar with bottom of the message /// Center user avatar with bottom of the message
final bool translateUserAvatar; final bool translateUserAvatar;
///
MessageWidget({ MessageWidget({
Key key, Key key,
@required this.message, @required this.message,
@@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget {
this.showReactionPickerIndicator = false, this.showReactionPickerIndicator = false,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.showSendingIndicator = DisplayWidget.show, this.showSendingIndicator = DisplayWidget.show,
this.showReplyIndicator = true, this.showThreadReplyIndicator = true,
this.showInChannelIndicator = true,
this.onThreadTap, this.onThreadTap,
this.showUsername = true, this.showUsername = true,
this.showTimestamp = true, this.showTimestamp = true,
@@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget {
} }
class _MessageWidgetState extends State<MessageWidget> { class _MessageWidgetState extends State<MessageWidget> {
bool get showThreadReplyIndicator =>
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
bool get showUsername => widget.showUsername;
bool get showTimeStamp =>
widget.message.createdAt != null && widget.showTimestamp;
bool get showReadList => widget.readList?.isNotEmpty == true;
bool get showInChannel =>
widget.showInChannelIndicator && widget.message?.showInChannel == true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var leftPadding = widget.showUserAvatar != DisplayWidget.gone var leftPadding = widget.showUserAvatar != DisplayWidget.gone
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0 ? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
: 6.0; : 6.0;
final hasFiles = final hasFiles =
@@ -234,140 +252,151 @@ class _MessageWidgetState extends State<MessageWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Column( Stack(
crossAxisAlignment: CrossAxisAlignment.start, alignment: AlignmentDirectional.bottomStart,
mainAxisSize: MainAxisSize.min, children: [
children: <Widget>[ Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: [
if (widget.showUserAvatar == DisplayWidget.show) Row(
_buildUserAvatar(), mainAxisAlignment: MainAxisAlignment.start,
SizedBox( crossAxisAlignment: CrossAxisAlignment.end,
width: 6, mainAxisSize: MainAxisSize.min,
), children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.hide) if (widget.showUserAvatar == DisplayWidget.show)
SizedBox( _buildUserAvatar(),
width: widget.messageTheme.avatarTheme.constraints SizedBox(width: 6),
.maxWidth + if (widget.showUserAvatar == DisplayWidget.hide)
8, SizedBox(
), width: widget.messageTheme.avatarTheme
Flexible( .constraints.maxWidth +
child: PortalEntry( 8,
portal: Container( ),
transform: Matrix4.translationValues(-16, 2, 0), Flexible(
child: _buildReactionIndicator(context), child: PortalEntry(
constraints: BoxConstraints(maxWidth: 22 * 6.0), portal: Container(
), transform:
portalAnchor: Alignment(-1.0, -1.0), Matrix4.translationValues(-16, 2, 0),
childAnchor: Alignment(1, -1.0), child: _buildReactionIndicator(context),
child: Stack( constraints:
clipBehavior: Clip.none, BoxConstraints(maxWidth: 22 * 6.0),
children: [ ),
Padding( portalAnchor: Alignment(-1.0, -1.0),
padding: widget.showReactions childAnchor: Alignment(1, -1.0),
? EdgeInsets.only( child: Stack(
top: widget.message.reactionCounts clipBehavior: Clip.none,
?.isNotEmpty == children: [
true Padding(
? 18 padding: widget.showReactions
: 0, ? EdgeInsets.only(
) top: widget.message.reactionCounts
: EdgeInsets.zero, ?.isNotEmpty ==
child: (widget.message.isDeleted && true
widget.message.status != ? 18
MessageSendingStatus : 0,
.FAILED_DELETE) )
? Transform( : EdgeInsets.zero,
alignment: Alignment.center, child: (widget.message.isDeleted &&
widget.message.status !=
MessageSendingStatus
.FAILED_DELETE)
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: DeletedMessage(
reverse: widget.reverse,
borderRadiusGeometry:
widget.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: widget.borderSide ??
BorderSide(
color: Theme.of(context)
.brightness ==
Brightness
.dark
? Colors.white
.withAlpha(
24)
: Colors.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
..._parseAttachments(
context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
),
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY( transform: Matrix4.rotationY(
widget.reverse ? pi : 0), widget.reverse ? pi : 0),
child: DeletedMessage( child: CustomPaint(
reverse: widget.reverse, painter: ReactionBubblePainter(
borderRadiusGeometry: widget.messageTheme
widget.borderRadiusGeometry, .reactionsBackgroundColor,
borderSide: widget.borderSide, widget.messageTheme
shape: widget.shape, .reactionsBorderColor,
messageTheme: widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: widget.borderSide ??
BorderSide(
color: Theme.of(context)
.brightness ==
Brightness
.dark
? Colors.white
.withAlpha(24)
: Colors.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
..._parseAttachments(context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
), ),
), ),
), ),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme
.reactionsBackgroundColor,
widget.messageTheme
.reactionsBorderColor,
),
), ),
), ],
), ),
], ),
), ),
), ],
), ),
if (showThreadReplyIndicator ||
showUsername ||
showTimeStamp ||
showInChannel)
SizedBox(height: 20.0),
], ],
), ),
if (widget.showReplyIndicator && if (showThreadReplyIndicator ||
widget.message.replyCount > 0) showUsername ||
_buildReplyIndicator(leftPadding), showTimeStamp ||
showInChannel)
_buildBottomRow(leftPadding)
], ],
), ),
if ((widget.message.createdAt != null &&
widget.showTimestamp) ||
widget.showUsername ||
widget.readList?.isNotEmpty == true)
_buildBottomRow(leftPadding),
], ],
), ),
), ),
@@ -376,6 +405,119 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
Widget _buildBottomRow(double leftPadding) {
final deleted = widget.message.isDeleted;
var children = <Widget>[];
if (deleted) {
children.add(
Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5),
size: 16.0,
),
SizedBox(width: 8.0),
Text(
'Only visible to you',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 12.0,
),
),
],
),
);
} else if (showInChannel) {
final onThreadTap = () async {
try {
final channel = StreamChannel.of(context);
final message = await channel.getMessage(widget.message.parentId);
return widget.onThreadTap(message);
} catch (e, stk) {
print(e);
print(stk);
return null;
}
};
children.add(
InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null,
child: Text('Thread Reply', style: widget.messageTheme?.replies),
),
);
} else {
final showSendingIndicator =
widget.showSendingIndicator == DisplayWidget.show;
final replyCount = widget.message.replyCount;
final msg = replyCount != 0
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
: 'Thread Reply';
final onThreadTap = () async {
var message = widget.message;
return widget.onThreadTap(message);
};
children.addAll([
if (showSendingIndicator) _buildSendingIndicator(),
if (showReadList)
SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17),
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(),
),
),
if (showThreadReplyIndicator)
InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null,
child: Text(msg, style: widget.messageTheme?.replies),
),
if (showUsername)
Text(
widget.message.user.name,
style: widget.messageTheme.replies.copyWith(
color: widget.messageTheme.createdAt.color,
),
),
if (showTimeStamp)
Text(
Jiffy(widget.message.createdAt.toLocal()).jm,
style: widget.messageTheme.createdAt,
),
]);
}
if (widget.reverse) children = children.reversed.toList();
return Padding(
padding: EdgeInsets.only(left: leftPadding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!deleted && (showThreadReplyIndicator || showInChannel))
Container(
margin: EdgeInsets.only(
bottom: widget.messageTheme.replies.fontSize / 2),
child: CustomPaint(
size: const Size(16, 32),
painter: _ThreadReplyPainter(
color: widget.messageTheme.replyThreadColor,
),
),
),
...children.map(
(child) => Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: child,
),
),
].insertBetween(const SizedBox(width: 8.0)),
),
);
}
Widget _buildUrlAttachment() { Widget _buildUrlAttachment() {
var urlAttachment = widget.message.attachments var urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null); .firstWhere((element) => element.ogScrapeUrl != null);
@@ -394,87 +536,6 @@ class _MessageWidgetState extends State<MessageWidget> {
); );
} }
Padding _buildBottomRow(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
left: leftPadding,
top: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: RichText(
text: TextSpan(
style: widget.messageTheme.createdAt,
children: <TextSpan>[
if (widget.showUsername)
TextSpan(
text: widget.message.user.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.messageTheme.createdAt.color
.withOpacity(1)),
),
if (widget.message.createdAt != null && widget.showTimestamp)
TextSpan(
text: Jiffy(widget.message.createdAt.toLocal())
.format(' HH:mm'),
),
],
),
),
),
if (widget.showSendingIndicator == DisplayWidget.show)
_buildSendingIndicator(),
if (widget.readList?.isNotEmpty == true)
SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(),
),
),
),
if (widget.message.isDeleted)
Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5),
size: 16.0,
),
SizedBox(
width: 8.0,
),
Text(
'Only visible to you',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 12.0,
),
),
],
),
),
),
],
),
);
}
bool get isGiphy => bool get isGiphy =>
widget.message.attachments?.any((element) => element.type == 'giphy') == widget.message.attachments?.any((element) => element.type == 'giphy') ==
true; true;
@@ -565,7 +626,7 @@ class _MessageWidgetState extends State<MessageWidget> {
true, true,
showReactions: widget.showReactions, showReactions: widget.showReactions,
showReply: showReply:
widget.showReplyIndicator && widget.onThreadTap != null, widget.showThreadReplyIndicator && widget.onThreadTap != null,
), ),
); );
}); });
@@ -715,38 +776,13 @@ class _MessageWidgetState extends State<MessageWidget> {
return; return;
} }
Widget _buildReplyIndicator(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
left: leftPadding,
),
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: ReplyIndicator(
message: widget.message,
reversed: widget.reverse,
messageTheme: widget.messageTheme,
onTap: widget.onThreadTap != null
? () {
widget.onThreadTap(widget.message);
}
: null,
),
),
);
}
Widget _buildSendingIndicator() { Widget _buildSendingIndicator() {
return Padding( return Container(
padding: const EdgeInsets.only(right: 4.0), height: widget.messageTheme.createdAt.fontSize + 2,
child: Transform( width: widget.messageTheme.createdAt.fontSize + 2,
transform: Matrix4.rotationY(widget.reverse ? pi : 0), child: SendingIndicator(
alignment: Alignment.center, message: widget.message,
child: SendingIndicator( allRead: widget.allRead,
message: widget.message,
allRead: widget.allRead,
),
), ),
); );
} }
@@ -958,3 +994,32 @@ class _MessageWidgetState extends State<MessageWidget> {
} }
} }
} }
class _ThreadReplyPainter extends CustomPainter {
final Color color;
const _ThreadReplyPainter({@required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color ?? Color(0XFFDBDBDB)
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;
final path = Path()
..moveTo(0, 0)
..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50)
..quadraticBezierTo(
0,
size.height,
size.width,
size.height,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
-56
View File
@@ -1,56 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A reply button indicator
class ReplyIndicator extends StatelessWidget {
final Message message;
final VoidCallback onTap;
final bool reversed;
final MessageTheme messageTheme;
const ReplyIndicator({
Key key,
this.message,
this.onTap,
this.reversed = false,
this.messageTheme,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var row = [
Text(
'Replies: ${message.replyCount}',
style: messageTheme?.replies,
),
Transform(
transform: Matrix4.rotationY(reversed ? 0 : pi),
alignment: Alignment.center,
child: Icon(
Icons.subdirectory_arrow_left,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white12
: Colors.black12,
),
),
];
if (!reversed) {
row = row.reversed.toList();
}
return GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
),
);
}
}
+26 -12
View File
@@ -143,23 +143,24 @@ class StreamChannelState extends State<StreamChannel> {
if (_topPaginationEnded || _queryTopMessagesController.value) return; if (_topPaginationEnded || _queryTopMessagesController.value) return;
_queryTopMessagesController.add(true); _queryTopMessagesController.add(true);
if (!channel.state.threads.containsKey(parentId)) { Message message;
return _queryTopMessagesController.add(false); if (channel.state.threads.containsKey(parentId)) {
final thread = channel.state.threads[parentId];
if (thread.isNotEmpty) {
message = thread.first;
}
} }
final thread = channel.state.threads[parentId];
if (thread.isEmpty) return _queryTopMessagesController.add(false);
final message = thread.first;
try { try {
final state = await queryBeforeMessage( final response = await channel.getReplies(
message.id, parentId,
limit: limit, PaginationParams(
lessThan: message?.id,
limit: limit,
),
preferOffline: preferOffline, preferOffline: preferOffline,
); );
if (state.messages.isEmpty || state.messages.length < limit) { if (response.messages.isEmpty || response.messages.length < limit) {
_topPaginationEnded = true; _topPaginationEnded = true;
} }
_queryTopMessagesController.add(false); _queryTopMessagesController.add(false);
@@ -267,6 +268,19 @@ class StreamChannelState extends State<StreamChannel> {
return state; return state;
} }
///
Future<Message> getMessage(String messageId) async {
var message = channel.state.messages.firstWhere(
(it) => it.id == messageId,
orElse: () => null,
);
if (message == null) {
final response = await channel.getMessagesById([messageId]);
message = response.messages.first;
}
return message;
}
/// Reloads the channel with latest message /// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30); Future<void> reloadChannel() => queryAtMessage(before: 30);
+15 -4
View File
@@ -191,6 +191,8 @@ class StreamChatThemeData {
this.ownMessageTheme.messageBackgroundColor, this.ownMessageTheme.messageBackgroundColor,
avatarTheme: ownMessageTheme?.avatarTheme ?? avatarTheme: ownMessageTheme?.avatarTheme ??
this.ownMessageTheme.avatarTheme, this.ownMessageTheme.avatarTheme,
replyThreadColor: ownMessageTheme?.replyThreadColor ??
this.ownMessageTheme.replyThreadColor,
) ?? ) ??
this.ownMessageTheme, this.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.copyWith( otherMessageTheme: otherMessageTheme?.copyWith(
@@ -209,6 +211,8 @@ class StreamChatThemeData {
this.otherMessageTheme.messageBackgroundColor, this.otherMessageTheme.messageBackgroundColor,
avatarTheme: otherMessageTheme?.avatarTheme ?? avatarTheme: otherMessageTheme?.avatarTheme ??
this.otherMessageTheme.avatarTheme, this.otherMessageTheme.avatarTheme,
replyThreadColor: ownMessageTheme?.replyThreadColor ??
this.ownMessageTheme.replyThreadColor,
) ?? ) ??
this.otherMessageTheme, this.otherMessageTheme,
reactionIcons: reactionIcons ?? this.reactionIcons, reactionIcons: reactionIcons ?? this.reactionIcons,
@@ -297,16 +301,17 @@ class StreamChatThemeData {
color: isDark color: isDark
? Colors.white.withOpacity(.5) ? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5), : Colors.black.withOpacity(.5),
fontSize: 11, fontSize: 12,
), ),
replies: TextStyle( replies: TextStyle(
color: accentColor, color: accentColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
fontSize: 12, fontSize: 12,
), ),
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
reactionsBackgroundColor: isDark ? Colors.black : Colors.white, reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -330,17 +335,19 @@ class StreamChatThemeData {
color: isDark color: isDark
? Colors.white.withOpacity(.5) ? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5), : Colors.black.withOpacity(.5),
fontSize: 11, fontSize: 12,
), ),
replies: TextStyle( replies: TextStyle(
color: accentColor, color: accentColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w600,
fontSize: 12, fontSize: 12,
), ),
messageLinks: TextStyle( messageLinks: TextStyle(
color: accentColor, color: accentColor,
), ),
messageBackgroundColor: isDark ? Colors.black : Colors.white, messageBackgroundColor: isDark ? Colors.black : Colors.white,
replyThreadColor:
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
avatarTheme: AvatarTheme( avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -455,6 +462,7 @@ class MessageTheme {
final Color messageBackgroundColor; final Color messageBackgroundColor;
final Color reactionsBackgroundColor; final Color reactionsBackgroundColor;
final Color reactionsBorderColor; final Color reactionsBorderColor;
final Color replyThreadColor;
final AvatarTheme avatarTheme; final AvatarTheme avatarTheme;
const MessageTheme({ const MessageTheme({
@@ -465,6 +473,7 @@ class MessageTheme {
this.messageBackgroundColor, this.messageBackgroundColor,
this.reactionsBackgroundColor, this.reactionsBackgroundColor,
this.reactionsBorderColor, this.reactionsBorderColor,
this.replyThreadColor,
this.avatarTheme, this.avatarTheme,
this.createdAt, this.createdAt,
}); });
@@ -479,6 +488,7 @@ class MessageTheme {
AvatarTheme avatarTheme, AvatarTheme avatarTheme,
Color reactionsBackgroundColor, Color reactionsBackgroundColor,
Color reactionsBorderColor, Color reactionsBorderColor,
Color replyThreadColor,
}) => }) =>
MessageTheme( MessageTheme(
messageText: messageText ?? this.messageText, messageText: messageText ?? this.messageText,
@@ -492,6 +502,7 @@ class MessageTheme {
reactionsBackgroundColor: reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
); );
} }
+45 -36
View File
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'back_button.dart';
import 'channel_name.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png)
@@ -77,43 +79,50 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
return AppBar( return AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
elevation: 1, elevation: 1,
leading: showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
backgroundColor: backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[ centerTitle: true,
Container( title: Column(
child: showBackButton crossAxisAlignment: CrossAxisAlignment.center,
? AspectRatio( mainAxisAlignment: MainAxisAlignment.center,
aspectRatio: 1, children: [
child: IconButton( Text(
onPressed: onBackPressed ?? () => Navigator.pop(context), 'Thread Reply',
icon: StreamSvgIcon.close( style: StreamChatTheme.of(context)
size: 24, .channelTheme
color: Theme.of(context).brightness == Brightness.dark .channelHeaderTheme
? Colors.white .title,
: Colors.black, ),
), SizedBox(height: 2),
), Row(
) mainAxisSize: MainAxisSize.min,
: SizedBox(), crossAxisAlignment: CrossAxisAlignment.center,
), mainAxisAlignment: MainAxisAlignment.center,
], children: [
centerTitle: false, Text(
title: Text.rich( 'with ',
TextSpan( style: StreamChatTheme.of(context)
text: 'Thread', .channelTheme
children: [ .channelHeaderTheme
TextSpan( .lastMessageAt,
text: ),
' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}', Flexible(
style: StreamChatTheme.of(context) child: ChannelName(
.channelTheme textStyle: StreamChatTheme.of(context)
.channelHeaderTheme .channelTheme
.lastMessageAt, .channelHeaderTheme
), .lastMessageAt,
], ),
), ),
style: ],
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title, ),
],
), ),
); );
} }
+3
View File
@@ -61,12 +61,15 @@ class UserAvatar extends StatelessWidget {
: streamChatTheme.defaultUserImage(context, user), : streamChatTheme.defaultUserImage(context, user),
), ),
); );
if (selected) { if (selected) {
avatar = ClipRRect( avatar = ClipRRect(
borderRadius: (borderRadius ?? borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness), BorderRadius.circular(selectionThickness),
child: Container( child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
color: selectionColor, color: selectionColor,
child: Padding( child: Padding(
padding: EdgeInsets.all(selectionThickness), padding: EdgeInsets.all(selectionThickness),
+9
View File
@@ -92,3 +92,12 @@ Future<bool> showConfirmationDialog(
/// Get random png with initials /// Get random png with initials
String getRandomPicUrl(User user) => String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
/// List extension
extension ListX<T> on List<T> {
/// Insert any item<T> inBetween the list items
List<T> insertBetween(T item) => expand((e) sync* {
yield item;
yield e;
}).skip(1).toList(growable: false);
}
-1
View File
@@ -21,7 +21,6 @@ export 'src/message_list_view.dart';
export 'src/message_text.dart'; export 'src/message_text.dart';
export 'src/message_widget.dart'; export 'src/message_widget.dart';
export 'src/reaction_picker.dart'; export 'src/reaction_picker.dart';
export 'src/reply_indicator.dart';
export 'src/sending_indicator.dart'; export 'src/sending_indicator.dart';
export 'src/stream_channel.dart'; export 'src/stream_channel.dart';
export 'src/stream_chat.dart'; export 'src/stream_chat.dart';