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.showOnlineStatus = true,
this.borderRadius,
this.selected = false,
this.selectionColor = const Color(0xFF006CFF),
this.selectionThickness = 4,
}) : super(key: key);
final BorderRadius borderRadius;
@@ -67,6 +70,12 @@ class ChannelImage extends StatelessWidget {
final bool showOnlineStatus;
final bool selected;
final Color selectionColor;
final double selectionThickness;
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
@@ -94,11 +103,10 @@ class ChannelImage extends StatelessWidget {
.channelPreviewTheme
.avatarTheme
.constraints,
onTap: onTap != null
? (_) {
onTap();
}
: null,
onTap: onTap != null ? (_) => onTap() : null,
selected: selected,
selectionColor: selectionColor,
selectionThickness: selectionThickness,
);
});
} else {
@@ -111,16 +119,20 @@ class ChannelImage extends StatelessWidget {
.toList();
return GroupImage(
images: images,
borderRadius: borderRadius,
constraints: constraints ??
StreamChatTheme.of(context)
.channelPreviewTheme
.avatarTheme
.constraints,
onTap: onTap,
selected: selected,
selectionColor: selectionColor,
selectionThickness: selectionThickness,
);
}
return ClipRRect(
Widget child = ClipRRect(
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.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.swipeToAction = false,
this.pullToRefresh = true,
this.crossAxisCount = 1,
this.selectedChannels = const [],
}) : super(key: key);
/// 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
final VoidCallback onStartChatPressed;
/// The number of children in the cross axis.
final int crossAxisCount;
final List<Channel> selectedChannels;
@override
_ChannelListViewState createState() => _ChannelListViewState();
}
@@ -268,22 +275,33 @@ class _ChannelListViewState extends State<ChannelListView>
}
if (channels.isNotEmpty) {
child = ListView.custom(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
return _itemBuilder(context, i, channels);
if (widget.crossAxisCount > 1) {
child = GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
},
childCount: (channels.length * 2) + 1,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index = channels.indexWhere(
(channel) => 'CHANNEL-${channel.id}' == valueKey.value);
return index != -1 ? (index * 2) : null;
);
} else {
child = ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount:
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(
25,
(i) {
if (i % 2 != 0) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
if (widget.crossAxisCount == 1) {
if (i % 2 != 0) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
}
return _buildLoadingItem();
},
@@ -314,63 +334,96 @@ class _ChannelListViewState extends State<ChannelListView>
}
Shimmer _buildLoadingItem() {
return Shimmer.fromColors(
baseColor: Color(0xffE5E5E5),
highlightColor: StreamChatTheme.of(context).colorTheme.white,
child: ListTile(
leading: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
shape: BoxShape.circle,
),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
if (widget.crossAxisCount > 1) {
return Shimmer.fromColors(
baseColor: Color(0xffE5E5E5),
highlightColor: Color(0xffffffff),
child: Column(
children: [
SizedBox(
height: 4.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
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,
child: Container(
);
} else {
return Shimmer.fromColors(
baseColor: Color(0xffE5E5E5),
highlightColor: StreamChatTheme.of(context).colorTheme.white,
child: ListTile(
leading: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
shape: BoxShape.circle,
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 82,
height: 40,
width: 40,
),
),
),
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),
title: Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
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(
@@ -437,20 +490,10 @@ class _ChannelListViewState extends State<ChannelListView>
);
}
Widget _itemBuilder(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;
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
final channelsProvider = ChannelsBloc.of(context);
if (i < channels.length) {
final channel = channels[i];
ChannelTapCallback onTap;
if (widget.onChannelTap != null) {
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(
context,
ChannelsBlocState channelsProvider,
+36 -5
View File
@@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget {
@required this.images,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor = const Color(0xFF006CFF),
this.selectionThickness = 4,
}) : super(key: key);
final List<String> images;
final BoxConstraints constraints;
final VoidCallback onTap;
final bool selected;
final BorderRadius borderRadius;
final Color selectionColor;
final double selectionThickness;
@override
Widget build(BuildContext context) {
return GestureDetector(
var avatar;
final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
child: Container(
constraints: constraints ??
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/stream_chat_flutter.dart';
import 'stream_channel.dart';
class ImageFooter extends StatefulWidget {
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
@@ -64,11 +62,11 @@ class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false;
TextEditingController _searchController;
TextEditingController _messageController = TextEditingController();
FocusNode _messageFocusNode = FocusNode();
String _userNameQuery;
bool _isSearchActive = false;
String _channelNameQuery;
Set<User> _selectedUsers = {};
List<Channel> _selectedChannels = [];
bool _loading = false;
Timer _debounce;
@@ -80,8 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() {
_userNameQuery = _searchController.text;
_isSearchActive = _userNameQuery.isNotEmpty;
_channelNameQuery = _searchController.text;
});
}
});
@@ -91,6 +88,9 @@ class _ImageFooterState extends State<ImageFooter> {
void initState() {
super.initState();
_searchController = TextEditingController()..addListener(_userNameListener);
_messageFocusNode.addListener(() {
setState(() {});
});
}
@override
@@ -251,7 +251,7 @@ class _ImageFooterState extends State<ImageFooter> {
);
}
Widget _buildShareModal(context) {
void _buildShareModal(context) {
showDialog(
context: context,
builder: (context) {
@@ -259,7 +259,7 @@ class _ImageFooterState extends State<ImageFooter> {
modalSetStateCallback = modalSetState;
return Padding(
padding: EdgeInsets.only(
top: _userSearchMode
top: _userSearchMode || _messageFocusNode.hasFocus
? 16.0
: MediaQuery.of(context).size.height / 2,
left: 8.0,
@@ -275,74 +275,81 @@ class _ImageFooterState extends State<ImageFooter> {
child: Column(
children: [
_buildTextInputSection(modalSetState),
if (_userSearchMode)
SizedBox(
height: 22.0,
),
Expanded(
child: UserListView(
selectedUsers: _selectedUsers,
onUserTap: (user, _) {
_searchController.clear();
if (!_selectedUsers.contains(user)) {
modalSetState(() {
_selectedUsers.add(user);
});
} else {
modalSetState(() {
_selectedUsers.remove(user);
});
}
},
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_searchController.text.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
child: ChannelsBloc(
child: ChannelListView(
selectedChannels: _selectedChannels,
onChannelTap: (channel, _) {
_searchController.clear();
if (!_selectedChannels.contains(channel)) {
modalSetState(() {
_selectedChannels.add(channel);
});
} else {
modalSetState(() {
_selectedChannels.remove(channel);
});
}
},
},
sort: [
SortOption(
'name',
direction: 1,
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
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,
filter: {
if (_searchController.text.isNotEmpty)
'name': {
r'$autocomplete': _channelNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
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(
'No user matches these keywords...'),
],
Text(
'No chat matches these keywords...'),
],
),
),
),
),
);
},
);
},
);
},
);
},
),
),
),
if (_selectedUsers.isNotEmpty)
if (_selectedChannels.isNotEmpty)
_buildShareTextInputSection(modalSetState),
if (!_userSearchMode && _selectedUsers.isEmpty)
if (!_userSearchMode && _selectedChannels.isEmpty)
Align(
alignment: Alignment.bottomCenter,
child: Container(
@@ -543,6 +550,7 @@ class _ImageFooterState extends State<ImageFooter> {
padding: const EdgeInsets.only(left: 8.0),
child: TextField(
controller: _messageController,
focusNode: _messageFocusNode,
onChanged: (val) {
modalSetState(() {});
},
@@ -619,27 +627,16 @@ class _ImageFooterState extends State<ImageFooter> {
_messageController.clear();
final client = StreamChat.of(context).client;
for (var user in _selectedUsers) {
var c = client.channel('messaging', extraData: {
'members': [
user.id,
StreamChat.of(context).user.id,
],
});
await c.watch();
for (var channel in _selectedChannels) {
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await c.sendMessage(message);
await channel.sendMessage(message);
}
_selectedUsers.clear();
_selectedChannels.clear();
Navigator.pop(context);
}
+3
View File
@@ -61,12 +61,15 @@ class UserAvatar extends StatelessWidget {
: streamChatTheme.defaultUserImage(context, user),
),
);
if (selected) {
avatar = ClipRRect(
borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
color: selectionColor,
child: Padding(
padding: EdgeInsets.all(selectionThickness),