feat: Added fixes and user_list_view.dart

This commit is contained in:
Deven Joshi
2021-01-25 20:24:22 +05:30
parent 0f7a16dc6d
commit 5594a9a116
4 changed files with 429 additions and 516 deletions
@@ -135,287 +135,284 @@ class _NewChatScreenState extends State<NewChatScreen> {
centerTitle: true, centerTitle: true,
), ),
body: ValueListenableBuilder<ConnectionStatus>( body: ValueListenableBuilder<ConnectionStatus>(
valueListenable: StreamChat.of(context).client.wsConnectionStatus, valueListenable: StreamChat.of(context).client.wsConnectionStatus,
builder: (context, status, _) { builder: (context, status, _) {
String statusString = ''; String statusString = '';
bool showStatus = true; bool showStatus = true;
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
statusString = 'Connected'; statusString = 'Connected';
showStatus = false; showStatus = false;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
statusString = 'Reconnecting...'; statusString = 'Reconnecting...';
break; break;
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
statusString = 'Disconnected'; statusString = 'Disconnected';
break; break;
} }
return InfoTile( return InfoTile(
showMessage: showStatus, showMessage: showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
message: statusString, message: statusString,
child: StreamChannel( child: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ChipsInputTextField<User>( ChipsInputTextField<User>(
key: _chipInputTextFieldStateKey, key: _chipInputTextFieldStateKey,
controller: _controller, controller: _controller,
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
chipBuilder: (context, user) { chipBuilder: (context, user) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState.removeItem(user);
_searchFocusNode.requestFocus(); _searchFocusNode.requestFocus();
}, },
child: Stack( child: Stack(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
children: [
Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(left: 24),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
child: Text(
user.name,
maxLines: 1,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black,
),
),
),
),
Container(
foregroundDecoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.overlay,
shape: BoxShape.circle,
),
child: UserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
StreamSvgIcon.close(),
],
),
);
},
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
),
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
Container(
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [ children: [
Container( StreamNeumorphicButton(
decoration: BoxDecoration( child: Center(
color: StreamChatTheme.of(context) child: StreamSvgIcon.contacts(
.colorTheme color: StreamChatTheme.of(context)
.greyGainsboro, .colorTheme
borderRadius: BorderRadius.circular(12), .accentBlue,
), size: 24,
padding: const EdgeInsets.only(left: 24),
child: Padding(
padding:
const EdgeInsets.fromLTRB(8, 4, 12, 4),
child: Text(
user.name,
maxLines: 1,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black,
),
), ),
), ),
), ),
Container( SizedBox(width: 8),
foregroundDecoration: BoxDecoration( Text(
color: StreamChatTheme.of(context) 'Create a Group',
.colorTheme style: StreamChatTheme.of(context)
.overlay, .textTheme
shape: BoxShape.circle, .bodyBold,
),
child: UserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
), ),
StreamSvgIcon.close(),
], ],
), ),
); ),
}, ),
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
), ),
if (!_isSearchActive && !_selectedUsers.isNotEmpty) if (_showUserList)
Container( Container(
child: InkWell( width: double.maxFinite,
onTap: () { decoration: BoxDecoration(
Navigator.pushNamed( gradient:
context, StreamChatTheme.of(context).colorTheme.bgGradient,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
StreamNeumorphicButton(
child: Center(
child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
size: 24,
),
),
),
SizedBox(width: 8),
Text(
'Create a Group',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold,
),
],
),
),
),
), ),
if (_showUserList) child: Padding(
Container( padding: const EdgeInsets.symmetric(
width: double.maxFinite, vertical: 8,
decoration: BoxDecoration( horizontal: 8,
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? "Matches for \"$_userNameQuery\""
: 'On the platform',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
), ),
child: Text(
_isSearchActive
? "Matches for \"$_userNameQuery\""
: 'On the platform',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
), ),
Expanded( ),
child: _showUserList Expanded(
? GestureDetector( child: _showUserList
behavior: HitTestBehavior.opaque, ? GestureDetector(
onPanDown: (_) => behavior: HitTestBehavior.opaque,
FocusScope.of(context).unfocus(), onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc( child: UsersBloc(
child: UserListView( child: UserListView(
selectedUsers: _selectedUsers, selectedUsers: _selectedUsers,
groupAlphabetically: groupAlphabetically:
_isSearchActive ? false : true, _isSearchActive ? false : true,
onUserTap: (user, _) { onUserTap: (user, _) {
_controller.clear(); _controller.clear();
if (!_selectedUsers.contains(user)) { if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState _chipInputTextFieldState
..addItem(user) ..addItem(user)
..pauseItemAddition(); ..pauseItemAddition();
} else { } else {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState.removeItem(user);
} }
}, },
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: {
if (_userNameQuery.isNotEmpty) if (_userNameQuery.isNotEmpty)
'name': { 'name': {
r'$autocomplete': _userNameQuery, r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
}, },
'id': {
r'$ne': StreamChat.of(context).user.id,
}, },
sort: [ },
SortOption( sort: [
'name', SortOption(
direction: 1, 'name',
), direction: 1,
], ),
emptyBuilder: (_) { ],
return LayoutBuilder( emptyBuilder: (_) {
builder: (context, viewportConstraints) { return LayoutBuilder(
return SingleChildScrollView( builder: (context, viewportConstraints) {
physics: return SingleChildScrollView(
AlwaysScrollableScrollPhysics(), physics:
child: ConstrainedBox( AlwaysScrollableScrollPhysics(),
constraints: BoxConstraints( child: ConstrainedBox(
minHeight: constraints: BoxConstraints(
viewportConstraints.maxHeight, minHeight:
), viewportConstraints.maxHeight,
child: Center( ),
child: Column( child: Center(
children: [ child: Column(
Padding( children: [
padding: Padding(
const EdgeInsets.all( padding:
24), const EdgeInsets.all(24),
child: StreamSvgIcon.search( child: StreamSvgIcon.search(
size: 96, size: 96,
color: Colors.grey, color: Colors.grey,
),
), ),
Text( ),
'No user matches these keywords...', Text(
style: StreamChatTheme.of( 'No user matches these keywords...',
context) style: StreamChatTheme.of(
.textTheme context)
.footnote .textTheme
.copyWith( .footnote
color: StreamChatTheme .copyWith(
.of(context) color: StreamChatTheme
.colorTheme .of(context)
.black .colorTheme
.withOpacity( .black
.5)), .withOpacity(.5)),
), ),
], ],
),
), ),
), ),
); ),
}, );
); },
}, );
), },
), ),
)
: FutureBuilder<bool>(
future: channel.initialized,
builder: (context, snapshot) {
if (snapshot.data == true) {
return MessageListView();
}
return Center(
child: Text(
'No chats here yet...',
style: TextStyle(
fontSize: 12,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
),
);
},
), ),
), )
MessageInput( : FutureBuilder<bool>(
focusNode: _messageInputFocusNode, future: channel.initialized,
preMessageSending: (message) async { builder: (context, snapshot) {
await channel.watch(); if (snapshot.data == true) {
return message; return MessageListView();
}, }
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil( return Center(
context, child: Text(
Routes.CHANNEL_PAGE, 'No chats here yet...',
ModalRoute.withName(Routes.HOME), style: TextStyle(
arguments: ChannelPageArgs(channel: channel), fontSize: 12,
); color: StreamChatTheme.of(context)
}, .colorTheme
), .black
], .withOpacity(.5),
), ),
),
);
},
),
),
MessageInput(
focusNode: _messageInputFocusNode,
preMessageSending: (message) async {
await channel.watch();
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
);
},
),
],
), ),
); ),
}), );
},
),
); );
} }
} }
@@ -159,34 +159,36 @@ class _ChannelListViewState extends State<ChannelListView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!widget.pullToRefresh) { var child = ChannelListCore(
_channelListController.loadData(); channelListController: _channelListController,
} listBuilder: (context, list) {
return _buildListView(list);
return RefreshIndicator(
onRefresh: () async {
_channelListController.loadData();
}, },
child: ChannelListCore( emptyBuilder: (BuildContext context) {
channelListController: _channelListController, return _buildEmptyWidget();
listBuilder: (context, list) { },
return _buildListView(list); errorBuilder: (Error error) {
}, return _buildErrorWidget(context);
emptyBuilder: (BuildContext context) { },
return _buildEmptyWidget(); loadingBuilder: (BuildContext context) {
}, return _buildLoadingWidget();
errorBuilder: (Error error) { },
return _buildErrorWidget(context); pagination: widget.pagination,
}, options: widget.options,
loadingBuilder: (BuildContext context) { sort: widget.sort,
return _buildLoadingWidget(); filter: widget.filter,
},
pagination: widget.pagination,
options: widget.options,
sort: widget.sort,
filter: widget.filter,
),
); );
if (!widget.pullToRefresh) {
return child;
} else {
return RefreshIndicator(
onRefresh: () async {
_channelListController.loadData();
},
child: child,
);
}
} }
Widget _buildListView( Widget _buildListView(
@@ -140,209 +140,170 @@ class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver { with WidgetsBindingObserver {
bool get _isListView => widget.crossAxisCount == 1; bool get _isListView => widget.crossAxisCount == 1;
@override UserListController _userListController = UserListController();
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final usersBloc = UsersBloc.of(context); var child = UserListCore(
errorBuilder: (err) {
if (!widget.pullToRefresh) { return _buildError(err);
return _buildListView(usersBloc); },
} emptyBuilder: (context) {
return _buildEmpty();
return RefreshIndicator( },
onRefresh: () async { loadingBuilder: (context) {
return usersBloc.queryUsers( return LayoutBuilder(
filter: widget.filter, builder: (context, viewportConstraints) {
sort: widget.sort, return SingleChildScrollView(
options: widget.options, physics: AlwaysScrollableScrollPhysics(),
pagination: widget.pagination, child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
); );
}, },
child: _buildListView(usersBloc), listBuilder: (context, list) {
return _buildListView(list);
},
pagination: widget.pagination,
options: widget.options,
sort: widget.sort,
filter: widget.filter,
groupAlphabetically: widget.groupAlphabetically,
userListController: _userListController,
); );
if (!widget.pullToRefresh) {
return child;
} else {
return RefreshIndicator(
onRefresh: () async {
_userListController.loadData();
},
child: child,
);
}
} }
bool get isListAlreadySorted => bool get isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false; widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream( Widget _buildError(Error error) {
UsersBlocState usersBlocState, print((error).stackTrace);
) {
return usersBlocState.usersStream.map( if (widget.errorBuilder != null) {
(users) { return widget.errorBuilder(error);
if (widget.groupAlphabetically) { }
var temp = users;
if (!isListAlreadySorted) { var message = error.toString();
temp = users..sort((curr, next) => curr.name.compareTo(next.name)); if (error is DioError) {
} final dioError = error as DioError;
final groupedUsers = <String, List<User>>{}; if (dioError.type == DioErrorType.RESPONSE) {
for (var e in temp) { message = dioError.message;
final alphabet = e.name[0]?.toUpperCase(); } else {
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; message = 'Check your connection and retry';
} }
final items = <ListItem>[]; }
for (var key in groupedUsers.keys) { return Center(
items.add(ListHeaderItem(key)); child: Column(
items.addAll(groupedUsers[key].map((e) => ListUserItem(e))); mainAxisAlignment: MainAxisAlignment.center,
} children: <Widget>[
return items; Text.rich(
} TextSpan(
return users.map((e) => ListUserItem(e)).toList(); children: [
}, WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
_userListController.loadData();
},
child: Text('Retry'),
),
],
),
); );
} }
StreamBuilder<List<ListItem>> _buildListView( Widget _buildEmpty() {
UsersBlocState usersBlocState, if (widget.emptyBuilder != null) {
) { return widget.emptyBuilder(context);
return StreamBuilder( }
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) { if (widget.emptyBuilder == null) {
return widget.errorBuilder(snapshot.error); return LayoutBuilder(
} builder: (context, viewportConstraints) {
return SingleChildScrollView(
var message = snapshot.error.toString(); physics: AlwaysScrollableScrollPhysics(),
if (snapshot.error is DioError) { child: ConstrainedBox(
final dioError = snapshot.error as DioError; constraints: BoxConstraints(
if (dioError.type == DioErrorType.RESPONSE) { minHeight: viewportConstraints.maxHeight,
message = dioError.message; ),
} else { child: Center(
message = 'Check your connection and retry'; child: Text('There are no users currently'),
} ),
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
usersBlocState.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
},
child: Text('Retry'),
),
],
), ),
); );
} },
);
}
}
if (!snapshot.hasData) { Widget _buildListView(
return LayoutBuilder( List<ListItem> items,
builder: (context, viewportConstraints) { ) {
return SingleChildScrollView( final child = _isListView
physics: AlwaysScrollableScrollPhysics(), ? ListView.separated(
child: ConstrainedBox( physics: AlwaysScrollableScrollPhysics(),
constraints: BoxConstraints( itemCount: items.isNotEmpty ? items.length + 1 : items.length,
minHeight: viewportConstraints.maxHeight, separatorBuilder: (_, index) {
), if (widget.separatorBuilder != null) {
child: Center( return widget.separatorBuilder(context, index);
child: CircularProgressIndicator(), }
), return _separatorBuilder(context, index);
), },
); itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
}, },
); );
}
final items = snapshot.data; return LazyLoadScrollView(
onEndOfPage: () async {
if (items.isEmpty && widget.emptyBuilder != null) { return _userListController.paginateData();
return widget.emptyBuilder(context);
}
if (items.isEmpty && widget.emptyBuilder == null) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Text('There are no users currently'),
),
),
);
},
);
}
final child = _isListView
? ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
},
);
return LazyLoadScrollView(
onEndOfPage: () async {
return _listenUserPagination(usersBlocState);
},
child: child,
);
}, },
child: child,
); );
} }
@@ -481,33 +442,4 @@ class _UserListViewState extends State<UserListView>
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.greyWhisper,
); );
} }
void _listenUserPagination(UsersBlocState usersProvider) {
usersProvider.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
),
options: widget.options,
);
}
@override
void didUpdateWidget(UserListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
}
} }
@@ -43,7 +43,6 @@ class UserListCore extends StatefulWidget {
this.options, this.options,
this.sort, this.sort,
this.pagination, this.pagination,
this.pullToRefresh = true,
this.groupAlphabetically = false, this.groupAlphabetically = false,
@required this.errorBuilder, @required this.errorBuilder,
@required this.emptyBuilder, @required this.emptyBuilder,
@@ -89,9 +88,6 @@ class UserListCore extends StatefulWidget {
/// message_limit: how many messages should be included to each channel /// message_limit: how many messages should be included to each channel
final PaginationParams pagination; final PaginationParams pagination;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
/// Set it to true to group users by their first character /// Set it to true to group users by their first character
/// ///
/// defaults to false /// defaults to false
@@ -124,21 +120,7 @@ class _UserListCoreState extends State<UserListCore>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final usersBloc = UsersBloc.of(context); final usersBloc = UsersBloc.of(context);
if (!widget.pullToRefresh) { return _buildListView(usersBloc);
return _buildListView(usersBloc);
}
return RefreshIndicator(
onRefresh: () async {
return usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
options: widget.options,
pagination: widget.pagination,
);
},
child: _buildListView(usersBloc),
);
} }
bool get isListAlreadySorted => bool get isListAlreadySorted =>