Line data Source code
1 : import 'dart:convert';
2 :
3 : import 'package:flutter/foundation.dart';
4 : import 'package:flutter/material.dart';
5 : import 'package:stream_chat/stream_chat.dart';
6 : import 'package:stream_chat_flutter_core/src/users_bloc.dart';
7 :
8 : ///
9 : /// [UserListCore] is a simplified class that allows fetching users while
10 : /// exposing UI builders.
11 : /// A [UserListController] is used to load and paginate data.
12 : ///
13 : /// ```dart
14 : /// class UsersListPage extends StatelessWidget {
15 : /// @override
16 : /// Widget build(BuildContext context) {
17 : /// return Scaffold(
18 : /// body: UsersListCore(
19 : /// filter: {
20 : /// 'members': {
21 : /// '\$in': [StreamChat.of(context).user.id],
22 : /// }
23 : /// },
24 : /// sort: [SortOption('last_message_at')],
25 : /// pagination: PaginationParams(
26 : /// limit: 20,
27 : /// ),
28 : /// errorBuilder: (err) {
29 : /// return Center(
30 : /// child: Text('An error has occured'),
31 : /// );
32 : /// },
33 : /// emptyBuilder: (context) {
34 : /// return Center(
35 : /// child: Text('Nothing here...'),
36 : /// );
37 : /// },
38 : /// emptyBuilder: (context) {
39 : /// return Center(
40 : /// child: CircularProgressIndicator(),
41 : /// );
42 : /// },
43 : /// listBuilder: (context, list) {
44 : /// return UsersPage(list);
45 : /// }
46 : /// ),
47 : /// );
48 : /// }
49 : /// }
50 : /// ```
51 : ///
52 : /// [UsersBloc] must be the ancestor of this widget. This is necessary since
53 : /// [UserListCore] depends on functionality contained within [UsersBloc].
54 : ///
55 : /// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and
56 : /// [errorBuilder] must all be supplied and not null.
57 : class UserListCore extends StatefulWidget {
58 : /// Instantiate a new [UserListCore]
59 1 : const UserListCore({
60 : required this.errorBuilder,
61 : required this.emptyBuilder,
62 : required this.loadingBuilder,
63 : required this.listBuilder,
64 : Key? key,
65 : this.filter,
66 : this.options,
67 : this.sort,
68 : this.pagination,
69 : this.groupAlphabetically = false,
70 : this.userListController,
71 1 : }) : super(key: key);
72 :
73 : /// A [UserListController] allows reloading and pagination.
74 : /// Use [UserListController.loadData] and [UserListController.paginateData]
75 : /// respectively for reloading and pagination.
76 : final UserListController? userListController;
77 :
78 : /// The builder that will be used in case of error
79 : final Widget Function(Object error) errorBuilder;
80 :
81 : /// The builder that will be used to build the list
82 : final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
83 :
84 : /// The builder that will be used for loading
85 : final WidgetBuilder loadingBuilder;
86 :
87 : /// The builder used when the channel list is empty.
88 : final WidgetBuilder emptyBuilder;
89 :
90 : /// The query filters to use.
91 : /// You can query on any of the custom fields you've defined on the [Channel].
92 : /// You can also filter other built-in channel fields.
93 : final Filter? filter;
94 :
95 : /// Query channels options.
96 : ///
97 : /// state: if true returns the Channel state
98 : /// watch: if true listen to changes to this Channel in real time.
99 : final Map<String, dynamic>? options;
100 :
101 : /// The sorting used for the channels matching the filters.
102 : /// Sorting is based on field and direction, multiple sorting options can be
103 : /// provided. You can sort based on last_updated, last_message_at, updated_at,
104 : /// created_at or member_count. Direction can be ascending or descending.
105 : final List<SortOption>? sort;
106 :
107 : /// Pagination parameters
108 : /// limit: the number of users to return (max is 30)
109 : /// offset: the offset (max is 1000)
110 : /// message_limit: how many messages should be included to each channel
111 : final PaginationParams? pagination;
112 :
113 : /// Set it to true to group users by their first character
114 : ///
115 : /// defaults to false
116 : final bool groupAlphabetically;
117 :
118 1 : @override
119 1 : UserListCoreState createState() => UserListCoreState();
120 : }
121 :
122 : /// The current state of the [UserListCore].
123 : class UserListCoreState extends State<UserListCore>
124 : with WidgetsBindingObserver {
125 : UsersBlocState? _usersBloc;
126 :
127 1 : @override
128 : void didChangeDependencies() {
129 2 : final newUsersBloc = UsersBloc.of(context);
130 2 : if (newUsersBloc != _usersBloc) {
131 1 : _usersBloc = newUsersBloc;
132 1 : loadData();
133 2 : if (widget.userListController != null) {
134 4 : widget.userListController!.loadData = loadData;
135 4 : widget.userListController!.paginateData = paginateData;
136 : }
137 : }
138 1 : super.didChangeDependencies();
139 : }
140 :
141 1 : @override
142 1 : Widget build(BuildContext context) => _buildListView();
143 :
144 1 : bool get _isListAlreadySorted =>
145 2 : widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
146 :
147 4 : Stream<List<ListItem>> _buildUserStream() => _usersBloc!.usersStream.map(
148 1 : (users) {
149 2 : if (widget.groupAlphabetically) {
150 : var temp = users;
151 1 : if (!_isListAlreadySorted) {
152 : temp = users
153 5 : ..sort((curr, next) => curr.name.compareTo(next.name));
154 : }
155 1 : final groupedUsers = <String, List<User>>{};
156 2 : for (final e in temp) {
157 3 : final alphabet = e.name[0].toUpperCase();
158 5 : groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
159 : }
160 1 : final items = <ListItem>[];
161 2 : for (final key in groupedUsers.keys) {
162 : items
163 2 : ..add(ListHeaderItem(key))
164 5 : ..addAll(groupedUsers[key]!.map((e) => ListUserItem(e)));
165 : }
166 : return items;
167 : }
168 4 : return users.map((e) => ListUserItem(e)).toList();
169 : },
170 : );
171 :
172 2 : StreamBuilder<List<ListItem>> _buildListView() => StreamBuilder(
173 1 : stream: _buildUserStream(),
174 1 : builder: (context, snapshot) {
175 1 : if (snapshot.hasError) {
176 4 : return widget.errorBuilder(snapshot.error!);
177 : }
178 1 : if (!snapshot.hasData) {
179 3 : return widget.loadingBuilder(context);
180 : }
181 1 : final items = snapshot.data!;
182 1 : if (items.isEmpty) {
183 3 : return widget.emptyBuilder(context);
184 : }
185 3 : return widget.listBuilder(context, items);
186 : },
187 : );
188 :
189 : // ignore: public_member_api_docs
190 3 : Future<void> loadData() => _usersBloc!.queryUsers(
191 2 : filter: widget.filter,
192 2 : sort: widget.sort,
193 2 : pagination: widget.pagination,
194 2 : options: widget.options,
195 : );
196 :
197 : // ignore: public_member_api_docs
198 3 : Future<void> paginateData() => _usersBloc!.queryUsers(
199 2 : filter: widget.filter,
200 2 : sort: widget.sort,
201 3 : pagination: widget.pagination!.copyWith(
202 3 : offset: _usersBloc!.users?.length ?? 0,
203 : ),
204 2 : options: widget.options,
205 : );
206 :
207 1 : @override
208 : void didUpdateWidget(UserListCore oldWidget) {
209 1 : super.didUpdateWidget(oldWidget);
210 4 : if (widget.filter?.toString() != oldWidget.filter?.toString() ||
211 6 : jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
212 4 : widget.options?.toString() != oldWidget.options?.toString() ||
213 5 : widget.pagination?.toJson().toString() !=
214 3 : oldWidget.pagination?.toJson().toString()) {
215 1 : loadData();
216 : }
217 : }
218 : }
219 :
220 : /// Represents an item in a the user stream list.
221 : /// Header items are prefixed with the key `HEADER` While users are prefixed
222 : /// with `USER`.
223 : abstract class ListItem {
224 : /// Unique key per list item
225 1 : String? get key {
226 1 : if (this is ListHeaderItem) {
227 1 : final header = (this as ListHeaderItem).heading;
228 2 : return 'HEADER-${header.toLowerCase()}';
229 : }
230 1 : if (this is ListUserItem) {
231 1 : final user = (this as ListUserItem).user;
232 2 : return 'USER-${user.id}';
233 : }
234 : return null;
235 : }
236 :
237 : /// Helper function to build widget based on ListItem type
238 : // ignore: missing_return
239 1 : Widget when({
240 : required Widget Function(String heading) headerItem,
241 : required Widget Function(User user) userItem,
242 : }) {
243 1 : if (this is ListHeaderItem) {
244 2 : return headerItem((this as ListHeaderItem).heading);
245 : }
246 1 : if (this is ListUserItem) {
247 2 : return userItem((this as ListUserItem).user);
248 : }
249 0 : return Container();
250 : }
251 : }
252 :
253 : /// Header Item
254 : class ListHeaderItem extends ListItem {
255 : /// Constructs a new [ListHeaderItem]
256 1 : ListHeaderItem(this.heading);
257 :
258 : /// Heading used to build the item.
259 : final String heading;
260 : }
261 :
262 : /// User Item
263 : class ListUserItem extends ListItem {
264 : /// Constructs a new [ListUserItem]
265 1 : ListUserItem(this.user);
266 :
267 : /// [User] used to build the item.
268 : final User user;
269 : }
270 :
271 : /// Controller used for paginating data in [ChannelListView]
272 : class UserListController {
273 : /// Call this function to reload data
274 : AsyncCallback? loadData;
275 :
276 : /// Call this function to load further data
277 : AsyncCallback? paginateData;
278 : }
|