Merge branch 'develop' into release/1.3.0-beta

This commit is contained in:
Salvatore Giordano
2021-02-26 16:20:03 +01:00
15 changed files with 315 additions and 466 deletions
@@ -1245,7 +1245,9 @@ class ChannelClientState {
_channel._client.chatPersistenceClient _channel._client.chatPersistenceClient
?.getChannelStateByCid(_channel.cid) ?.getChannelStateByCid(_channel.cid)
?.then((state) { ?.then((state) {
updateChannelState(state); // Replacing the persistence state members with the latest `channelState.members`
// as they may have changes over the time.
updateChannelState(state.copyWith(members: channelState.members));
retryFailedMessages(); retryFailedMessages();
}); });
}); });
+11 -3
View File
@@ -4,7 +4,7 @@ part 'requests.g.dart';
/// Sorting options /// Sorting options
@JsonSerializable(createFactory: false) @JsonSerializable(createFactory: false)
class SortOption { class SortOption<T> {
/// Ascending order /// Ascending order
static const ASC = 1; static const ASC = 1;
@@ -17,6 +17,10 @@ class SortOption {
/// A sorting direction /// A sorting direction
final int direction; final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
/// Creates a new SortOption instance /// Creates a new SortOption instance
/// ///
/// For example: /// For example:
@@ -24,7 +28,11 @@ class SortOption {
/// // Sort channels by the last message date: /// // Sort channels by the last message date:
/// final sorting = SortOption("last_message_at") /// final sorting = SortOption("last_message_at")
/// ``` /// ```
const SortOption(this.field, {this.direction = DESC}); const SortOption(
this.field, {
this.direction = DESC,
this.comparator,
});
/// Serialize model to json /// Serialize model to json
Map<String, dynamic> toJson() => _$SortOptionToJson(this); Map<String, dynamic> toJson() => _$SortOptionToJson(this);
@@ -67,7 +75,7 @@ class PaginationParams {
/// ``` /// ```
const PaginationParams({ const PaginationParams({
this.limit = 10, this.limit = 10,
this.offset, this.offset = 0,
this.greaterThan, this.greaterThan,
this.greaterThanOrEqual, this.greaterThanOrEqual,
this.lessThan, this.lessThan,
+126 -155
View File
@@ -9,6 +9,7 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/retry_policy.dart'; import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/models/attachment_file.dart'; import 'package:stream_chat/src/models/attachment_file.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/own_user.dart'; import 'package:stream_chat/src/models/own_user.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/platform_detector/platform_detector.dart';
import 'package:stream_chat/version.dart'; import 'package:stream_chat/version.dart';
@@ -22,6 +23,7 @@ import 'api/responses.dart';
import 'api/websocket.dart'; import 'api/websocket.dart';
import 'db/chat_persistence_client.dart'; import 'db/chat_persistence_client.dart';
import 'exceptions.dart'; import 'exceptions.dart';
import 'models/channel_state.dart';
import 'models/event.dart'; import 'models/event.dart';
import 'models/message.dart'; import 'models/message.dart';
import 'models/user.dart'; import 'models/user.dart';
@@ -489,7 +491,7 @@ class StreamChatClient {
if (status == ConnectionStatus.connected && if (status == ConnectionStatus.connected &&
state.channels?.isNotEmpty == true) { state.channels?.isNotEmpty == true) {
unawaited(queryChannels(filter: { unawaited(queryChannelsOnline(filter: {
'cid': { 'cid': {
'\$in': state.channels.keys.toList(), '\$in': state.channels.keys.toList(),
}, },
@@ -578,13 +580,52 @@ class StreamChatClient {
final _queryChannelsStreams = <String, Future<List<Channel>>>{}; final _queryChannelsStreams = <String, Future<List<Channel>>>{};
/// Requests channels with a given query. /// Requests channels with a given query.
Future<List<Channel>> queryChannels({ Stream<List<Channel>> queryChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort, List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options, Map<String, dynamic> options,
PaginationParams paginationParams = const PaginationParams(limit: 10), PaginationParams paginationParams = const PaginationParams(limit: 10),
int messageLimit, int messageLimit,
bool onlyOffline = false, bool preferOffline = false,
bool waitForConnect = true,
}) async* {
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$preferOffline',
));
if (_queryChannelsStreams.containsKey(hash)) {
yield await _queryChannelsStreams[hash];
} else {
if (preferOffline) {
final channels = await queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
if (channels.isNotEmpty) yield channels;
}
final newQueryChannelsFuture = queryChannelsOnline(
filter: filter,
sort: sort,
options: options,
paginationParams: paginationParams,
messageLimit: messageLimit,
);
_queryChannelsStreams[hash] = newQueryChannelsFuture;
yield await newQueryChannelsFuture;
}
}
/// Requests channels with a given query from the API.
Future<List<Channel>> queryChannelsOnline({
@required Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
bool waitForConnect = true, bool waitForConnect = true,
}) async { }) async {
if (waitForConnect) { if (waitForConnect) {
@@ -593,48 +634,13 @@ class StreamChatClient {
await _connectCompleter.future; await _connectCompleter.future;
} }
if (wsConnectionStatus != ConnectionStatus.connected) { if (wsConnectionStatus != ConnectionStatus.connected) {
final errorMessage = throw Exception(
'You cannot use queryChannels without an active connection. Please call `connectUser` to connect the client.'; 'You cannot use queryChannels without an active connection.'
if (persistenceEnabled) { ' Please call `connectUser` to connect the client.',
logger.warning( );
'$errorMessage\nTrying to retrieve channels from the offline storage.');
onlyOffline = true;
} else {
throw Exception(errorMessage);
}
} }
} }
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$onlyOffline'));
if (_queryChannelsStreams.containsKey(hash)) {
return _queryChannelsStreams[hash];
}
final newQueryChannelsStream = _doQueryChannels(
filter: filter,
sort: sort,
options: options,
paginationParams: paginationParams,
messageLimit: messageLimit,
onlyOffline: onlyOffline,
).whenComplete(() {
_queryChannelsStreams.remove(hash);
});
_queryChannelsStreams[hash] = newQueryChannelsStream;
return newQueryChannelsStream;
}
Future<List<Channel>> _doQueryChannels({
@required Map<String, dynamic> filter,
@required List<SortOption> sort,
@required Map<String, dynamic> options,
@required int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
bool onlyOffline = false,
}) async {
logger.info('Query channel start'); logger.info('Query channel start');
final defaultOptions = { final defaultOptions = {
'state': true, 'state': true,
@@ -661,86 +667,86 @@ class StreamChatClient {
payload.addAll(paginationParams.toJson()); payload.addAll(paginationParams.toJson());
} }
if (onlyOffline) { final response = await get(
return _queryChannelsOffline( '/channels',
filter: filter, queryParameters: {
sort: sort, 'payload': jsonEncode(payload),
paginationParams: paginationParams, },
); );
}
try { final res = decode<QueryChannelsResponse>(
final response = await get( response.data,
'/channels', QueryChannelsResponse.fromJson,
queryParameters: { );
'payload': jsonEncode(payload),
},
);
final res = decode<QueryChannelsResponse>( if ((res.channels ?? []).isEmpty && (paginationParams?.offset ?? 0) == 0) {
response.data, logger.warning('''We could not find any channel for this query.
QueryChannelsResponse.fromJson,
);
final users = res.channels
?.expand((channel) => channel.members.map((member) => member.user))
?.toList();
if (users != null) {
state._updateUsers(users);
}
logger.info('Got ${res.channels?.length} channels from api');
if (res.channels?.isEmpty != false &&
(paginationParams?.offset ?? 0) == 0) {
logger.warning('''We could not find any channel for this query.
Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart'''); If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''');
} return <Channel>[];
final newChannels = Map<String, Channel>.from(state.channels ?? {});
final channels = <Channel>[];
if (res.channels != null) {
for (final channelState in res.channels) {
final channel = newChannels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
channels.add(channel);
} else {
final newChannel = Channel.fromState(this, channelState);
await chatPersistenceClient
?.updateChannelState(newChannel.state.channelState);
newChannel.state?.updateChannelState(channelState);
newChannels[newChannel.cid] = newChannel;
channels.add(newChannel);
}
}
}
state.channels = newChannels;
await chatPersistenceClient?.updateChannelQueries(
filter,
res.channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
);
return channels;
} catch (e) {
if (!persistenceEnabled) {
rethrow;
}
return _queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
} }
final channels = res.channels;
final users = channels
.expand((it) => it.members)
.map((it) => it.user)
.toList(growable: false);
state._updateUsers(users);
logger.info('Got ${res.channels?.length} channels from api');
final updateData = _mapChannelStateToChannel(channels);
await chatPersistenceClient?.updateChannelQueries(
filter,
channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0,
);
state.channels = updateData.key;
return updateData.value;
} }
dynamic _parseError(DioError error) { /// Requests channels with a given query from the Persistence client.
Future<List<Channel>> queryChannelsOffline({
@required Map<String, dynamic> filter,
@required List<SortOption<ChannelModel>> sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
}) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
final updatedData = _mapChannelStateToChannel(offlineChannels);
state.channels = updatedData.key;
return updatedData.value;
}
MapEntry<Map<String, Channel>, List<Channel>> _mapChannelStateToChannel(
List<ChannelState> channelStates,
) {
final channels = {...state.channels ?? {}};
final newChannels = <Channel>[];
if (channelStates != null) {
for (final channelState in channelStates) {
final channel = channels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
newChannels.add(channel);
} else {
final newChannel = Channel.fromState(this, channelState);
channels[newChannel.cid] = newChannel;
newChannels.add(newChannel);
}
}
}
return MapEntry(channels, newChannels);
}
Object _parseError(DioError error) {
if (error.type == DioErrorType.RESPONSE) { if (error.type == DioErrorType.RESPONSE) {
final apiError = final apiError =
ApiError(error.response?.data, error.response?.statusCode); ApiError(error.response?.data, error.response?.statusCode);
@@ -751,39 +757,6 @@ class StreamChatClient {
return error; return error;
} }
Future<List<Channel>> _queryChannelsOffline({
@required Map<String, dynamic> filter,
@required List<SortOption> sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
}) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
sort: sort,
paginationParams: paginationParams,
) ??
[];
final newChannels = Map<String, Channel>.from(state.channels ?? {});
logger.info('Got ${offlineChannels.length} channels from storage');
final channels = offlineChannels.map((channelState) {
final channel = newChannels[channelState.channel.cid];
if (channel != null) {
channel.state?.updateChannelState(channelState);
return channel;
} else {
final newChannel = Channel.fromState(this, channelState);
chatPersistenceClient
?.updateChannelState(newChannel.state.channelState);
newChannels[newChannel.cid] = newChannel;
return newChannel;
}
}).toList();
if (channels.isNotEmpty) {
state.channels = newChannels;
}
return channels;
}
/// Handy method to make http GET request with error parsing. /// Handy method to make http GET request with error parsing.
Future<Response<String>> get( Future<Response<String>> get(
String path, { String path, {
@@ -1448,18 +1421,16 @@ class ClientState {
_userController.add(user); _userController.add(user);
} }
void _updateUsers(List<User> users) { void _updateUsers(List<User> userList) {
users?.forEach(_updateUser);
}
void _updateUser(User user) {
final newUsers = { final newUsers = {
...users ?? {}, ...users ?? {},
user.id: user, for (var user in userList) user.id: user,
}; };
_usersController.add(newUsers); _usersController.add(newUsers);
} }
void _updateUser(User user) => _updateUsers([user]);
/// The current user /// The current user
OwnUser get user => _userController.value; OwnUser get user => _userController.value;
@@ -68,23 +68,19 @@ abstract class ChatPersistenceClient {
PaginationParams messagePagination, PaginationParams messagePagination,
PaginationParams pinnedMessagePagination, PaginationParams pinnedMessagePagination,
}) async { }) async {
final members = await getMembersByCid(cid); final data = await Future.wait([
final reads = await getReadsByCid(cid); getMembersByCid(cid),
final channel = await getChannelByCid(cid); getReadsByCid(cid),
final messages = await getMessagesByCid( getChannelByCid(cid),
cid, getMessagesByCid(cid, messagePagination: messagePagination),
messagePagination: messagePagination, getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
); ]);
final pinnedMessages = await getPinnedMessagesByCid(
cid,
messagePagination: pinnedMessagePagination,
);
return ChannelState( return ChannelState(
members: members, members: data[0],
read: reads, read: data[1],
messages: messages, channel: data[2],
pinnedMessages: pinnedMessages, messages: data[3],
channel: channel, pinnedMessages: data[4],
); );
} }
@@ -94,7 +90,7 @@ abstract class ChatPersistenceClient {
/// for filtering out states. /// for filtering out states.
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}); });
@@ -8,7 +8,7 @@ import 'message.dart';
part 'channel_state.g.dart'; part 'channel_state.g.dart';
/// The class that contains the information about a command /// The class that contains the information about a channel
@JsonSerializable() @JsonSerializable()
class ChannelState { class ChannelState {
/// The channel to which this state belongs /// The channel to which this state belongs
@@ -12,7 +12,7 @@ void main() {
test('PaginationParams', () { test('PaginationParams', () {
final option = PaginationParams(); final option = PaginationParams();
final j = option.toJson(); final j = option.toJson();
expect(j, {'limit': 10}); expect(j, {'limit': 10, 'offset': 0});
}); });
}); });
} }
+11 -7
View File
@@ -10,6 +10,7 @@ import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/exceptions.dart'; import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/message.dart'; import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/user.dart'; import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class MockDio extends Mock implements DioForNative {} class MockDio extends Mock implements DioForNative {}
@@ -86,7 +87,7 @@ void main() {
}); });
}); });
group('queryChannels', () { group('queryChannelsOnline', () {
test('should pass right default parameters', () async { test('should pass right default parameters', () async {
final mockDio = MockDio(); final mockDio = MockDio();
@@ -106,13 +107,14 @@ void main() {
"watch": true, "watch": true,
"presence": false, "presence": false,
"limit": 10, "limit": 10,
"offset": 0,
}), }),
}; };
when(mockDio.get<String>('/channels', queryParameters: queryParams)) when(mockDio.get<String>('/channels', queryParameters: queryParams))
.thenAnswer((_) async => Response(data: '{}', statusCode: 200)); .thenAnswer((_) async => Response(data: '{}', statusCode: 200));
await client.queryChannels(waitForConnect: false); await client.queryChannelsOnline(filter: null, waitForConnect: false);
verify(mockDio.get<String>('/channels', queryParameters: queryParams)) verify(mockDio.get<String>('/channels', queryParameters: queryParams))
.called(1); .called(1);
@@ -134,7 +136,7 @@ void main() {
"\$in": ["test"], "\$in": ["test"],
}, },
}; };
final sortOptions = <SortOption>[]; final sortOptions = <SortOption<ChannelModel>>[];
final options = {"state": false, "watch": false, "presence": true}; final options = {"state": false, "watch": false, "presence": true};
final paginationParams = PaginationParams( final paginationParams = PaginationParams(
limit: 10, limit: 10,
@@ -152,10 +154,10 @@ void main() {
when(mockDio.get<String>('/channels', queryParameters: queryParams)) when(mockDio.get<String>('/channels', queryParameters: queryParams))
.thenAnswer((_) async { .thenAnswer((_) async {
return Response(data: '{}', statusCode: 200); return Response(data: '{"channels":[]}', statusCode: 200);
}); });
await client.queryChannels( await client.queryChannelsOnline(
filter: queryFilter, filter: queryFilter,
sort: sortOptions, sort: sortOptions,
options: options, options: options,
@@ -229,6 +231,7 @@ void main() {
'query': query, 'query': query,
'sort': sortOptions, 'sort': sortOptions,
'limit': 10, 'limit': 10,
'offset': 0,
}), }),
}; };
@@ -346,7 +349,8 @@ void main() {
}; };
when(mockDio.get<String>('/users', queryParameters: queryParams)) when(mockDio.get<String>('/users', queryParameters: queryParams))
.thenAnswer((_) async => Response(data: '{}', statusCode: 200)); .thenAnswer(
(_) async => Response(data: '{"users":[]}', statusCode: 200));
await client.queryUsers(); await client.queryUsers();
@@ -382,7 +386,7 @@ void main() {
when(mockDio.get<String>('/users', queryParameters: queryParams)) when(mockDio.get<String>('/users', queryParameters: queryParams))
.thenAnswer((_) async { .thenAnswer((_) async {
return Response(data: '{}', statusCode: 200); return Response(data: '{"users":[]}', statusCode: 200);
}); });
await client.queryUsers( await client.queryUsers(
@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:stream_chat/version.dart'; import 'package:stream_chat/version.dart';
@@ -49,8 +49,11 @@ class ChannelInfo extends StatelessWidget {
var alternativeWidget; var alternativeWidget;
if (channel.memberCount != null && channel.memberCount > 2) { if (channel.memberCount != null && channel.memberCount > 2) {
var text = '${channel.memberCount} Members';
final watcherCount = channel.state.watcherCount ?? 0;
if (watcherCount > 0) text += ' $watcherCount Online';
alternativeWidget = Text( alternativeWidget = Text(
'${channel.memberCount} Members, ${channel.state.watcherCount} Online', text,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.channelTheme .channelTheme
.channelHeaderTheme .channelHeaderTheme
@@ -99,7 +99,7 @@ class ChannelListView extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sort; final List<SortOption<ChannelModel>> sort;
/// Pagination parameters /// Pagination parameters
/// limit: the number of channels to return (max is 30) /// limit: the number of channels to return (max is 30)
@@ -159,54 +159,40 @@ class ChannelListView extends StatefulWidget {
_ChannelListViewState createState() => _ChannelListViewState(); _ChannelListViewState createState() => _ChannelListViewState();
} }
class _ChannelListViewState extends State<ChannelListView> class _ChannelListViewState extends State<ChannelListView> {
with WidgetsBindingObserver { final _slideController = SlidableController();
final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController(); final _channelListController = ChannelListController();
final ChannelListController _channelListController = ChannelListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var child = ChannelListCore( Widget child = ChannelListCore(
channelListController: _channelListController,
listBuilder: widget.listBuilder ??
(context, list) {
return _buildListView(list);
},
emptyBuilder: widget.emptyBuilder ??
(BuildContext context) {
return _buildEmptyWidget();
},
errorBuilder: widget.errorBuilder ??
(BuildContext context, dynamic error) {
return _buildErrorWidget(context);
},
loadingBuilder: widget.loadingBuilder ??
(BuildContext context) {
return _buildLoadingWidget();
},
pagination: widget.pagination, pagination: widget.pagination,
options: widget.options, options: widget.options,
sort: widget.sort, sort: widget.sort,
filter: widget.filter, filter: widget.filter,
channelListController: _channelListController,
listBuilder: widget.listBuilder ?? _buildListView,
emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget,
errorBuilder: widget.errorBuilder ?? _buildErrorWidget,
loadingBuilder: widget.loadingBuilder ?? _buildLoadingWidget,
); );
if (!widget.pullToRefresh) { if (widget.pullToRefresh) {
return child; child = RefreshIndicator(
} else { onRefresh: () => _channelListController.loadData(),
return RefreshIndicator(
onRefresh: () async {
_channelListController.loadData();
},
child: child, child: child,
); );
} }
return LazyLoadScrollView(
onEndOfPage: () => _channelListController.paginateData(),
child: child,
);
} }
Widget _buildListView( Widget _buildListView(BuildContext context, List<Channel> channels) {
List<Channel> channels, Widget child;
) {
var child;
if (channels.isNotEmpty) { if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
@@ -216,7 +202,6 @@ class _ChannelListViewState extends State<ChannelListView>
crossAxisCount: widget.crossAxisCount), crossAxisCount: widget.crossAxisCount),
itemCount: channels.length, itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels); return _gridItemBuilder(context, index, channels);
}, },
@@ -236,18 +221,17 @@ class _ChannelListViewState extends State<ChannelListView>
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels); return _listItemBuilder(context, index, channels);
}, },
controller: _scrollController,
); );
} }
} }
return AnimatedSwitcher( return AnimatedSwitcher(
child: child, child: child,
duration: Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
} }
Widget _buildEmptyWidget() { Widget _buildEmptyWidget(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, viewportConstraints) { builder: (context, viewportConstraints) {
return SingleChildScrollView( return SingleChildScrollView(
@@ -326,7 +310,7 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} }
Widget _buildLoadingWidget() { Widget _buildLoadingWidget(BuildContext context) {
return ListView( return ListView(
padding: widget.padding, padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
@@ -341,13 +325,13 @@ class _ChannelListViewState extends State<ChannelListView>
return _separatorBuilder(context, i); return _separatorBuilder(context, i);
} }
} }
return _buildLoadingItem(); return _buildLoadingItem(context);
}, },
), ),
); );
} }
Shimmer _buildLoadingItem() { Shimmer _buildLoadingItem(BuildContext context) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro, baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
@@ -443,9 +427,7 @@ class _ChannelListViewState extends State<ChannelListView>
} }
} }
Widget _buildErrorWidget( Widget _buildErrorWidget(BuildContext context, Object error) {
BuildContext context,
) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -467,9 +449,7 @@ class _ChannelListViewState extends State<ChannelListView>
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
), ),
FlatButton( FlatButton(
onPressed: () { onPressed: () => _channelListController.loadData(),
_channelListController.loadData();
},
child: Text('Retry'), child: Text('Retry'),
), ),
], ],
@@ -572,23 +552,13 @@ class _ChannelListViewState extends State<ChannelListView>
], ],
child: Container( child: Container(
color: StreamChatTheme.of(context).colorTheme.whiteSnow, color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: widget.channelPreviewBuilder != null child: widget.channelPreviewBuilder?.call(context, channel) ??
? widget.channelPreviewBuilder( ChannelPreview(
context, onLongPress: widget.onChannelLongPress,
channel, channel: channel,
) onImageTap: widget.onImageTap?.call(channel),
: ChannelPreview( onTap: (channel) => onTap(channel, widget.channelWidget),
onLongPress: widget.onChannelLongPress, ),
channel: channel,
onImageTap: widget.onImageTap != null
? () {
widget.onImageTap(channel);
}
: null,
onTap: (channel) {
onTap(channel, widget.channelWidget);
},
),
), ),
); );
}, },
@@ -618,9 +588,7 @@ class _ChannelListViewState extends State<ChannelListView>
width: 64, width: 64,
height: 64, height: 64,
), ),
onTap: () { onTap: () => widget.onChannelTap(channel, null),
widget.onChannelTap(channel, null);
},
), ),
SizedBox(height: 7), SizedBox(height: 7),
Padding( Padding(
@@ -680,70 +648,4 @@ class _ChannelListViewState extends State<ChannelListView>
color: effect.color.withOpacity(effect.alpha ?? 1.0), color: effect.color.withOpacity(effect.alpha ?? 1.0),
); );
} }
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset &&
_scrollController.offset != 0) {
_channelListController.paginateData();
}
}
StreamSubscription _subscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_scrollController.addListener(() {
channelsBloc.queryChannelsLoading.first.then((loading) {
if (!loading) {
_listenChannelPagination(channelsBloc);
}
});
});
final client = StreamChat.of(context).client;
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) {
_channelListController.loadData();
});
}
@override
void didUpdateWidget(ChannelListView 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()) {
_channelListController.loadData();
}
}
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
} }
@@ -106,7 +106,7 @@ class ChannelListCore extends StatefulWidget {
/// Sorting is based on field and direction, multiple sorting options can be provided. /// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. /// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption> sort; final List<SortOption<ChannelModel>> sort;
/// Pagination parameters /// Pagination parameters
/// limit: the number of channels to return (max is 30) /// limit: the number of channels to return (max is 30)
@@ -118,8 +118,7 @@ class ChannelListCore extends StatefulWidget {
_ChannelListCoreState createState() => _ChannelListCoreState(); _ChannelListCoreState createState() => _ChannelListCoreState();
} }
class _ChannelListCoreState extends State<ChannelListCore> class _ChannelListCoreState extends State<ChannelListCore> {
with WidgetsBindingObserver {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
@@ -133,34 +132,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
return StreamBuilder<List<Channel>>( return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream, stream: channelsBlocState.channelsStream,
builder: (context, snapshot) { builder: (context, snapshot) {
var child;
if (snapshot.hasError) { if (snapshot.hasError) {
child = _buildErrorWidget( return _buildErrorWidget(snapshot, context, channelsBlocState);
snapshot,
context,
channelsBlocState,
);
} else if (!snapshot.hasData) {
child = _buildLoadingWidget();
} else {
final channels = snapshot.data;
child = widget.emptyBuilder(context);
if (channels.isNotEmpty) {
return widget.listBuilder(context, channels);
}
} }
if (!snapshot.hasData) {
return child; return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
}, },
); );
} }
Widget _buildLoadingWidget() {
return widget.loadingBuilder(context);
}
Widget _buildErrorWidget( Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot, AsyncSnapshot<List<Channel>> snapshot,
BuildContext context, BuildContext context,
@@ -173,10 +159,9 @@ class _ChannelListCoreState extends State<ChannelListCore>
return widget.errorBuilder(context, snapshot.error); return widget.errorBuilder(context, snapshot.error);
} }
void loadData() { Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
channelsBloc.queryChannels(
filter: widget.filter, filter: widget.filter,
sortOptions: widget.sort, sortOptions: widget.sort,
paginationParams: widget.pagination, paginationParams: widget.pagination,
@@ -184,10 +169,9 @@ class _ChannelListCoreState extends State<ChannelListCore>
); );
} }
void paginateData() { Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
channelsBloc.queryChannels(
filter: widget.filter, filter: widget.filter,
sortOptions: widget.sort, sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith( paginationParams: widget.pagination.copyWith(
@@ -197,39 +181,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
); );
} }
StreamSubscription _subscription; StreamSubscription<Event> _subscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadData();
WidgetsBinding.instance.addObserver(this);
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
final client = StreamChatCore.of(context).client; final client = StreamChatCore.of(context).client;
_subscription = client _subscription = client
.on( .on(
EventType.connectionRecovered, EventType.connectionRecovered,
EventType.notificationAddedToChannel, EventType.notificationAddedToChannel,
EventType.notificationMessageNew, EventType.notificationMessageNew,
EventType.channelVisible, EventType.channelVisible,
) )
.listen((event) { .listen((event) => loadData());
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
});
if (widget.channelListController != null) { if (widget.channelListController != null) {
widget.channelListController.loadData = loadData; widget.channelListController.loadData = loadData;
@@ -246,20 +212,13 @@ class _ChannelListCoreState extends State<ChannelListCore>
widget.pagination?.toJson()?.toString() != widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() || oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) { widget.options?.toString() != oldWidget.options?.toString()) {
final channelsBloc = ChannelsBloc.of(context); loadData();
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
} }
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel(); _subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
} }
@@ -268,10 +227,10 @@ class _ChannelListCoreState extends State<ChannelListCore>
class ChannelListController { class ChannelListController {
/// This function calls Stream's servers to load a list of channels. If there is existing data, /// This function calls Stream's servers to load a list of channels. If there is existing data,
/// calling this function causes a reload. /// calling this function causes a reload.
VoidCallback loadData; AsyncCallback loadData;
/// This function is used to load another page of data. Note, [loadData] should be /// This function is used to load another page of data. Note, [loadData] should be
/// used to populate the initial page of data. Calling [paginateData] performs a query /// used to populate the initial page of data. Calling [paginateData] performs a query
/// to load subsequent pages. /// to load subsequent pages.
VoidCallback paginateData; AsyncCallback paginateData;
} }
@@ -84,7 +84,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream /// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({ Future<void> queryChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sortOptions, List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams, PaginationParams paginationParams,
Map<String, dynamic> options, Map<String, dynamic> options,
bool onlyOffline = false, bool onlyOffline = false,
@@ -102,21 +102,21 @@ class ChannelsBlocState extends State<ChannelsBloc>
paginationParams.offset == null || paginationParams.offset == null ||
paginationParams.offset == 0; paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []); final oldChannels = List<Channel>.from(channels ?? []);
final _channels = await client.queryChannels( await for (final channels in client.queryChannels(
filter: filter, filter: filter,
sort: sortOptions, sort: sortOptions,
options: options, options: options,
paginationParams: paginationParams, paginationParams: paginationParams,
onlyOffline: onlyOffline, preferOffline: onlyOffline,
); )) {
if (clear) {
if (clear) { _channelsController.add(channels);
_channelsController.add(_channels); } else {
} else { final l = oldChannels + channels;
final l = oldChannels + _channels; _channelsController.add(l);
_channelsController.add(l); }
_queryChannelsLoadingController.sink.add(false);
} }
_queryChannelsLoadingController.sink.add(false);
} catch (err, stackTrace) { } catch (err, stackTrace) {
print(err); print(err);
print(stackTrace); print(stackTrace);
@@ -15,9 +15,7 @@ part 'channel_query_dao.g.dart';
class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase> class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
with _$ChannelQueryDaoMixin { with _$ChannelQueryDaoMixin {
/// Creates a new channel query dao instance /// Creates a new channel query dao instance
ChannelQueryDao(this._db) : super(_db); ChannelQueryDao(MoorChatDatabase db) : super(db);
final MoorChatDatabase _db;
String _computeHash(Map<String, dynamic> filter) { String _computeHash(Map<String, dynamic> filter) {
if (filter == null) { if (filter == null) {
@@ -37,19 +35,19 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
) async { ) async {
final hash = _computeHash(filter); final hash = _computeHash(filter);
if (clearQueryCache) { if (clearQueryCache) {
await (delete(channelQueries) await batch((it) {
..where((query) => query.queryHash.equals(hash))) it.deleteWhere<ChannelQueries, ChannelQueryEntity>(
.go(); channelQueries,
(c) => c.queryHash.equals(hash),
);
});
} }
return batch((batch) { return batch((it) {
batch.insertAll( it.insertAll(
channelQueries, channelQueries,
cids.map((cid) { cids.map((cid) {
return ChannelQueryEntity( return ChannelQueryEntity(queryHash: hash, channelCid: cid);
queryHash: hash,
channelCid: cid,
);
}).toList(), }).toList(),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
@@ -57,70 +55,74 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
} }
/// Get list of channels by filter, sort and paginationParams /// Get list of channels by filter, sort and paginationParams
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelModel>> getChannels({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}) async { }) async {
assert(() {
if (sort != null && sort.any((it) => it.comparator == null)) {
throw ArgumentError(
'SortOption requires a comparator in order to sort',
);
}
return true;
}());
final hash = _computeHash(filter); final hash = _computeHash(filter);
final cachedChannels = await Future.wait(await (select(channelQueries) final cachedChannelCids = await (select(channelQueries)
..where((c) => c.queryHash.equals(hash))) ..where((c) => c.queryHash.equals(hash)))
.get() .map((c) => c.channelCid)
.then((channelQueries) { .get();
final cids = channelQueries.map((c) => c.channelCid).toList();
final query = select(channels)..where((c) => c.cid.isIn(cids));
sort = sort final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids));
?.where((s) => ChannelModel.topLevelFields.contains(s.field))
?.toList();
if (sort != null && sort.isNotEmpty) { final cachedChannels = await (query.join([
query.orderBy(sort.map((s) { leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
final orderExpression = CustomExpression('channels.${s.field}'); ]).map((row) {
return (c) => OrderingTerm( final createdByEntity = row.readTable(users);
expression: orderExpression, final channelEntity = row.readTable(channels);
mode: s.direction == 1 ? OrderingMode.asc : OrderingMode.desc, return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser());
); })).get();
}).toList());
}
if (paginationParams != null) { final possibleSortingFields = cachedChannels.fold<List<String>>(
query.limit( ChannelModel.topLevelFields, (previousValue, element) {
paginationParams.limit ?? 10, return {...previousValue, ...element.extraData.keys}.toList();
offset: paginationParams.offset, });
);
}
return query.join([ sort = sort
leftOuterJoin(users, channels.createdById.equalsExp(users.id)), ?.where((s) => possibleSortingFields.contains(s.field))
]).map((row) async { ?.toList(growable: false);
final userEntity = row.readTable(users);
final channelEntity = row.readTable(channels);
final cid = channelEntity.cid; Comparator<ChannelModel> chainedComparator = (a, b) {
final members = await _db.memberDao.getMembersByCid(cid); final dateA = a.lastMessageAt ?? a.createdAt;
final reads = await _db.readDao.getReadsByCid(cid); final dateB = b.lastMessageAt ?? b.createdAt;
final messages = await _db.messageDao.getMessagesByCid(cid); return dateB.compareTo(dateA);
final pinnedMessages = await _db.pinnedMessageDao.getMessagesByCid(cid); };
return channelEntity.toChannelState( if (sort != null && sort.isNotEmpty) {
createdBy: userEntity?.toUser(), chainedComparator = (a, b) {
members: members, int result;
reads: reads, for (final comparator in sort.map((it) => it.comparator)) {
messages: messages, try {
pinnedMessages: pinnedMessages, result = comparator(a, b);
); } catch (e) {
}).get(); result = 0;
})); }
if (result != 0) return result;
}
return 0;
};
}
if (sort?.isEmpty != false && cachedChannels?.isNotEmpty == true) { cachedChannels.sort(chainedComparator);
cachedChannels
.sort((a, b) => b.channel.updatedAt.compareTo(a.channel.updatedAt)); if (paginationParams?.offset != null) {
cachedChannels.sort((a, b) { cachedChannels.removeRange(0, paginationParams.offset);
final dateA = a.channel.lastMessageAt ?? a.channel.createdAt; }
final dateB = b.channel.lastMessageAt ?? b.channel.createdAt;
return dateB.compareTo(dateA); if (paginationParams?.limit != null) {
}); return cachedChannels.take(paginationParams.limit).toList();
} }
return cachedChannels; return cachedChannels;
@@ -161,14 +161,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override @override
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}) { }) async {
return _db.channelQueryDao.getChannelStates( final channels = await _db.channelQueryDao.getChannels(
filter: filter, filter: filter,
sort: sort, sort: sort,
paginationParams: paginationParams, paginationParams: paginationParams,
); );
return Future.wait(channels.map((e) => getChannelStateByCid(e.cid)));
} }
@override @override