Merge branch 'feature/new-ui' into feature/theme

This commit is contained in:
Salvatore Giordano
2020-12-30 17:16:18 +01:00
5 changed files with 313 additions and 163 deletions
+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
@@ -171,6 +183,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();
} }
@@ -268,22 +275,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);
},
);
}
} }
} }
@@ -301,11 +319,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();
}, },
@@ -314,63 +334,96 @@ class _ChannelListViewState extends State<ChannelListView>
} }
Shimmer _buildLoadingItem() { Shimmer _buildLoadingItem() {
return Shimmer.fromColors( if (widget.crossAxisCount > 1) {
baseColor: Color(0xffE5E5E5), return Shimmer.fromColors(
highlightColor: StreamChatTheme.of(context).colorTheme.white, baseColor: Color(0xffE5E5E5),
child: ListTile( highlightColor: Color(0xffffffff),
leading: Container( child: Column(
decoration: BoxDecoration( children: [
color: StreamChatTheme.of(context).colorTheme.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: StreamChatTheme.of(context).colorTheme.white,
child: ListTile(
leading: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.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: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 238,
),
),
),
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.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: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 238,
),
),
),
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 42,
),
),
],
),
), ),
), );
); }
} }
Widget _buildErrorWidget( Widget _buildErrorWidget(
@@ -437,20 +490,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;
@@ -590,6 +633,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
@@ -251,7 +251,7 @@ class _ImageFooterState extends State<ImageFooter> {
); );
} }
Widget _buildShareModal(context) { void _buildShareModal(context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
@@ -259,7 +259,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,
@@ -275,74 +275,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(
@@ -543,6 +550,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(() {});
}, },
@@ -619,27 +627,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);
} }
+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),