Add custom sort support for offline channels

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2021-02-26 18:30:22 +05:30
parent aeecc71154
commit 623d48e32b
13 changed files with 394 additions and 442 deletions
@@ -1245,7 +1245,9 @@ class ChannelClientState {
_channel._client.chatPersistenceClient
?.getChannelStateByCid(_channel.cid)
?.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();
});
});
+11 -3
View File
@@ -4,7 +4,7 @@ part 'requests.g.dart';
/// Sorting options
@JsonSerializable(createFactory: false)
class SortOption {
class SortOption<T> {
/// Ascending order
static const ASC = 1;
@@ -17,6 +17,10 @@ class SortOption {
/// A sorting direction
final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
/// Creates a new SortOption instance
///
/// For example:
@@ -24,7 +28,11 @@ class SortOption {
/// // Sort channels by the last message date:
/// 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
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
@@ -67,7 +75,7 @@ class PaginationParams {
/// ```
const PaginationParams({
this.limit = 10,
this.offset,
this.offset = 0,
this.greaterThan,
this.greaterThanOrEqual,
this.lessThan,
+113 -140
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/event_type.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/platform_detector/platform_detector.dart';
import 'package:stream_chat/version.dart';
@@ -22,6 +23,7 @@ import 'api/responses.dart';
import 'api/websocket.dart';
import 'db/chat_persistence_client.dart';
import 'exceptions.dart';
import 'models/channel_state.dart';
import 'models/event.dart';
import 'models/message.dart';
import 'models/user.dart';
@@ -489,7 +491,7 @@ class StreamChatClient {
if (status == ConnectionStatus.connected &&
state.channels?.isNotEmpty == true) {
unawaited(queryChannels(filter: {
unawaited(_queryChannels(filter: {
'cid': {
'\$in': state.channels.keys.toList(),
},
@@ -578,15 +580,15 @@ class StreamChatClient {
final _queryChannelsStreams = <String, Future<List<Channel>>>{};
/// Requests channels with a given query.
Future<List<Channel>> queryChannels({
Stream<List<Channel>> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sort,
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
PaginationParams paginationParams = const PaginationParams(limit: 10),
int messageLimit,
bool onlyOffline = false,
bool preferOffline = true,
bool waitForConnect = true,
}) async {
}) async* {
if (waitForConnect) {
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
logger.info('awaiting connection completer');
@@ -598,7 +600,7 @@ class StreamChatClient {
if (persistenceEnabled) {
logger.warning(
'$errorMessage\nTrying to retrieve channels from the offline storage.');
onlyOffline = true;
preferOffline = true;
} else {
throw Exception(errorMessage);
}
@@ -606,34 +608,41 @@ class StreamChatClient {
}
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$onlyOffline'));
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$preferOffline',
));
if (_queryChannelsStreams.containsKey(hash)) {
return _queryChannelsStreams[hash];
yield await _queryChannelsStreams[hash];
} else {
if (true) {
final channels = await _queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
if (channels.isNotEmpty) yield channels;
}
final newQueryChannelsFuture = _queryChannels(
filter: filter,
sort: sort,
options: options,
paginationParams: paginationParams,
messageLimit: messageLimit,
);
_queryChannelsStreams[hash] = newQueryChannelsFuture;
yield await newQueryChannelsFuture;
}
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({
Future<List<Channel>> _queryChannels({
@required Map<String, dynamic> filter,
@required List<SortOption> sort,
@required Map<String, dynamic> options,
@required int messageLimit,
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
bool onlyOffline = false,
}) async {
logger.info('Query channel start');
final defaultOptions = {
@@ -661,86 +670,85 @@ class StreamChatClient {
payload.addAll(paginationParams.toJson());
}
if (onlyOffline) {
return _queryChannelsOffline(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
}
final response = await get(
'/channels',
queryParameters: {
'payload': jsonEncode(payload),
},
);
try {
final response = await get(
'/channels',
queryParameters: {
'payload': jsonEncode(payload),
},
);
final res = decode<QueryChannelsResponse>(
response.data,
QueryChannelsResponse.fromJson,
);
final res = decode<QueryChannelsResponse>(
response.data,
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.
if (res.channels?.isEmpty == true && (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
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''');
}
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,
);
return <Channel>[];
}
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) {
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) {
final apiError =
ApiError(error.response?.data, error.response?.statusCode);
@@ -751,39 +759,6 @@ class StreamChatClient {
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.
Future<Response<String>> get(
String path, {
@@ -1448,18 +1423,16 @@ class ClientState {
_userController.add(user);
}
void _updateUsers(List<User> users) {
users?.forEach(_updateUser);
}
void _updateUser(User user) {
void _updateUsers(List<User> userList) {
final newUsers = {
...users ?? {},
user.id: user,
for (var user in userList) user.id: user,
};
_usersController.add(newUsers);
}
void _updateUser(User user) => _updateUsers([user]);
/// The current user
OwnUser get user => _userController.value;
@@ -68,23 +68,19 @@ abstract class ChatPersistenceClient {
PaginationParams messagePagination,
PaginationParams pinnedMessagePagination,
}) async {
final members = await getMembersByCid(cid);
final reads = await getReadsByCid(cid);
final channel = await getChannelByCid(cid);
final messages = await getMessagesByCid(
cid,
messagePagination: messagePagination,
);
final pinnedMessages = await getPinnedMessagesByCid(
cid,
messagePagination: pinnedMessagePagination,
);
final data = await Future.wait([
getMembersByCid(cid),
getReadsByCid(cid),
getChannelByCid(cid),
getMessagesByCid(cid, messagePagination: messagePagination),
getPinnedMessagesByCid(cid,messagePagination: pinnedMessagePagination),
]);
return ChannelState(
members: members,
read: reads,
messages: messages,
pinnedMessages: pinnedMessages,
channel: channel,
members: data[0],
read: data[1],
channel: data[2],
messages: data[3],
pinnedMessages: data[4],
);
}
@@ -94,7 +90,7 @@ abstract class ChatPersistenceClient {
/// for filtering out states.
Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter,
List<SortOption> sort = const [],
List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams,
});
@@ -8,7 +8,7 @@ import 'message.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()
class ChannelState {
/// The channel to which this state belongs
+110 -10
View File
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:rxdart/rxdart.dart';
import 'package:test/test.dart';
import 'package:stream_chat/version.dart';
@@ -10,14 +11,113 @@ void prepareTest() {
}
}
void main() {
prepareTest();
test('stream chat version matches pubspec', () {
final String pubspecPath = '${Directory.current.path}/pubspec.yaml';
final String pubspec = File(pubspecPath).readAsStringSync();
final RegExp regex = RegExp('version:\s*(.*)');
final RegExpMatch match = regex.firstMatch(pubspec);
expect(match, isNotNull);
expect(PACKAGE_VERSION, match.group(1).trim());
});
// void main() {
// prepareTest();
// test('stream chat version matches pubspec', () {
// final String pubspecPath = '${Directory.current.path}/pubspec.yaml';
// final String pubspec = File(pubspecPath).readAsStringSync();
// final RegExp regex = RegExp('version:\s*(.*)');
// final RegExpMatch match = regex.firstMatch(pubspec);
// expect(match, isNotNull);
// expect(PACKAGE_VERSION, match.group(1).trim());
// });
// }
void main() async {
var items = [
ABC('Sahil', 22, 62.0),
ABC('Devraj', 23, 76.0),
ABC('Harsh', 18, 48.0),
ABC('Harsh', 17, 88.0),
ABC('Harsh', 17, 48.0),
ABC('Devraj', 23, 74.0, {
'Test': 'Sahil',
}),
ABC('Devraj', 12, 76.0, {
'Test': 'Avni',
}),
];
var comparators = [
(ABC a, ABC b) {
// if (a.extraData == null) return -1;
// if (b.extraData == null) return 1;
// if (a.extraData == null && b.extraData == null) return 0;
var aa = (a.extraData ?? {})['Test'] as String;
var bb = (b.extraData ?? {})['Test'] as String;
return aa.compareTo(bb);
},
// (ABC a, ABC b) => a.name.compareTo(b.name),
// (ABC a, ABC b) => a.age.compareTo(b.age),
// (ABC a, ABC b) => a.weight.compareTo(b.weight),
];
// for (var comp in comparators.reversed) {
// items.sort(comp);
// }
// items.sort(comparators[last])
Stream<String> getLaugh2() {
if (true) {
return Stream.value('HEHO');
} else {
return getLaugh2();
}
}
Stream<String> getLaugh() async* {
yield 'HAHA';
await Future.delayed(const Duration(seconds: 3));
yield 'HOHO';
await Future.delayed(const Duration(seconds: 3));
yield 'HEHE';
}
// final stream = BehaviorSubject.seeded('HAHA');
//
// stream.('HOHO');
await for (var value in getLaugh2()) {
print(value);
}
// stream.add('HEHE');
// compare(ABC a, ABC b) {
// int result;
// for (final comparator in comparators) {
// try {
// result = comparator(a, b);
// } catch (e) {
// result = 0;
// }
// if (result != 0) return result;
// }
// return 0;
// }
//
// items.sort(compare);
//
// print(items);
}
class ABC {
final String name;
final int age;
final double weight;
final Map<String, Object> extraData;
const ABC(this.name, this.age, this.weight, [this.extraData]);
@override
String toString() {
return '''
\n
Name : $name,
Age : $age,
Weight : $weight,
ExtraData : $extraData,
''';
}
}
@@ -49,8 +49,11 @@ class ChannelInfo extends StatelessWidget {
var alternativeWidget;
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(
'${channel.memberCount} Members, ${channel.state.watcherCount} Online',
text,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
@@ -99,7 +99,7 @@ class ChannelListView extends StatefulWidget {
/// 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.
/// Direction can be ascending or descending.
final List<SortOption> sort;
final List<SortOption<ChannelModel>> sort;
/// Pagination parameters
/// limit: the number of channels to return (max is 30)
@@ -159,54 +159,42 @@ class ChannelListView extends StatefulWidget {
_ChannelListViewState createState() => _ChannelListViewState();
}
class _ChannelListViewState extends State<ChannelListView>
with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController();
final ChannelListController _channelListController = ChannelListController();
class _ChannelListViewState extends State<ChannelListView> {
final _slideController = SlidableController();
final _channelListController = ChannelListController();
@override
Widget build(BuildContext context) {
var 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();
},
Widget child = ChannelListCore(
pagination: widget.pagination,
options: widget.options,
sort: widget.sort,
filter: widget.filter,
channelListController: _channelListController,
listBuilder: widget.listBuilder ?? _buildListView,
emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget,
errorBuilder: widget.errorBuilder ?? _buildErrorWidget,
loadingBuilder: widget.loadingBuilder ?? _buildLoadingWidget,
);
if (!widget.pullToRefresh) {
return child;
} else {
return RefreshIndicator(
onRefresh: () async {
_channelListController.loadData();
},
if (widget.pullToRefresh) {
child = RefreshIndicator(
onRefresh: () async => _channelListController.loadData(),
child: child,
);
}
return LazyLoadScrollView(
onEndOfPage: () async {
_channelListController.paginateData();
},
child: child,
);
}
Widget _buildListView(
List<Channel> channels,
) {
var child;
Widget _buildListView(BuildContext context, List<Channel> channels) {
Widget child;
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
@@ -216,7 +204,6 @@ class _ChannelListViewState extends State<ChannelListView>
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
},
@@ -236,18 +223,17 @@ class _ChannelListViewState extends State<ChannelListView>
itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels);
},
controller: _scrollController,
);
}
}
return AnimatedSwitcher(
child: child,
duration: Duration(milliseconds: 500),
duration: const Duration(milliseconds: 500),
);
}
Widget _buildEmptyWidget() {
Widget _buildEmptyWidget(BuildContext context) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
@@ -326,7 +312,7 @@ class _ChannelListViewState extends State<ChannelListView>
);
}
Widget _buildLoadingWidget() {
Widget _buildLoadingWidget(BuildContext context) {
return ListView(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
@@ -341,13 +327,13 @@ class _ChannelListViewState extends State<ChannelListView>
return _separatorBuilder(context, i);
}
}
return _buildLoadingItem();
return _buildLoadingItem(context);
},
),
);
}
Shimmer _buildLoadingItem() {
Shimmer _buildLoadingItem(BuildContext context) {
if (widget.crossAxisCount > 1) {
return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
@@ -443,9 +429,7 @@ class _ChannelListViewState extends State<ChannelListView>
}
}
Widget _buildErrorWidget(
BuildContext context,
) {
Widget _buildErrorWidget(BuildContext context, Object error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -572,23 +556,13 @@ class _ChannelListViewState extends State<ChannelListView>
],
child: Container(
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: widget.channelPreviewBuilder != null
? widget.channelPreviewBuilder(
context,
channel,
)
: ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: widget.onImageTap != null
? () {
widget.onImageTap(channel);
}
: null,
onTap: (channel) {
onTap(channel, widget.channelWidget);
},
),
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
),
),
);
},
@@ -618,9 +592,7 @@ class _ChannelListViewState extends State<ChannelListView>
width: 64,
height: 64,
),
onTap: () {
widget.onChannelTap(channel, null);
},
onTap: () => widget.onChannelTap(channel, null),
),
SizedBox(height: 7),
Padding(
@@ -680,70 +652,4 @@ class _ChannelListViewState extends State<ChannelListView>
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.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sort;
final List<SortOption<ChannelModel>> sort;
/// Pagination parameters
/// limit: the number of channels to return (max is 30)
@@ -118,8 +118,7 @@ class ChannelListCore extends StatefulWidget {
_ChannelListCoreState createState() => _ChannelListCoreState();
}
class _ChannelListCoreState extends State<ChannelListCore>
with WidgetsBindingObserver {
class _ChannelListCoreState extends State<ChannelListCore> {
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -133,34 +132,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
var child;
if (snapshot.hasError) {
child = _buildErrorWidget(
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);
}
return _buildErrorWidget(snapshot, context, channelsBlocState);
}
return child;
if (!snapshot.hasData) {
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(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
@@ -197,39 +183,21 @@ class _ChannelListCoreState extends State<ChannelListCore>
);
}
StreamSubscription _subscription;
StreamSubscription<Event> _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,
);
loadData();
final client = StreamChatCore.of(context).client;
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) {
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
});
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) => loadData());
if (widget.channelListController != null) {
widget.channelListController.loadData = loadData;
@@ -246,20 +214,13 @@ class _ChannelListCoreState extends State<ChannelListCore>
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
loadData();
}
}
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
@@ -84,7 +84,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
@@ -102,21 +102,21 @@ class ChannelsBlocState extends State<ChannelsBloc>
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []);
final _channels = await client.queryChannels(
await for (final channels in client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
onlyOffline: onlyOffline,
);
if (clear) {
_channelsController.add(_channels);
} else {
final l = oldChannels + _channels;
_channelsController.add(l);
preferOffline: onlyOffline,
)) {
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
}
_queryChannelsLoadingController.sink.add(false);
}
_queryChannelsLoadingController.sink.add(false);
} catch (err, stackTrace) {
print(err);
print(stackTrace);
@@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.0
stream_chat:
stream_chat:
path: ../../stream_chat
stream_chat_persistence:
path: ../
@@ -15,9 +15,7 @@ part 'channel_query_dao.g.dart';
class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
with _$ChannelQueryDaoMixin {
/// Creates a new channel query dao instance
ChannelQueryDao(this._db) : super(_db);
final MoorChatDatabase _db;
ChannelQueryDao(MoorChatDatabase db) : super(db);
String _computeHash(Map<String, dynamic> filter) {
if (filter == null) {
@@ -37,19 +35,19 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
) async {
final hash = _computeHash(filter);
if (clearQueryCache) {
await (delete(channelQueries)
..where((query) => query.queryHash.equals(hash)))
.go();
await batch((it) {
it.deleteWhere<ChannelQueries, ChannelQueryEntity>(
channelQueries,
(c) => c.queryHash.equals(hash),
);
});
}
return batch((batch) {
batch.insertAll(
return batch((it) {
it.insertAll(
channelQueries,
cids.map((cid) {
return ChannelQueryEntity(
queryHash: hash,
channelCid: cid,
);
return ChannelQueryEntity(queryHash: hash, channelCid: cid);
}).toList(),
mode: InsertMode.insertOrReplace,
);
@@ -57,70 +55,74 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
}
/// Get list of channels by filter, sort and paginationParams
Future<List<ChannelState>> getChannelStates({
Future<List<ChannelModel>> getChannels({
Map<String, dynamic> filter,
List<SortOption> sort = const [],
List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams,
}) 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 cachedChannels = await Future.wait(await (select(channelQueries)
final cachedChannelCids = await (select(channelQueries)
..where((c) => c.queryHash.equals(hash)))
.get()
.then((channelQueries) {
final cids = channelQueries.map((c) => c.channelCid).toList();
final query = select(channels)..where((c) => c.cid.isIn(cids));
.map((c) => c.channelCid)
.get();
sort = sort
?.where((s) => ChannelModel.topLevelFields.contains(s.field))
?.toList();
final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids));
if (sort != null && sort.isNotEmpty) {
query.orderBy(sort.map((s) {
final orderExpression = CustomExpression('channels.${s.field}');
return (c) => OrderingTerm(
expression: orderExpression,
mode: s.direction == 1 ? OrderingMode.asc : OrderingMode.desc,
);
}).toList());
}
final cachedChannels = await (query.join([
leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
]).map((row) {
final createdByEntity = row.readTable(users);
final channelEntity = row.readTable(channels);
return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser());
})).get();
if (paginationParams != null) {
query.limit(
paginationParams.limit ?? 10,
offset: paginationParams.offset,
);
}
final possibleSortingFields = cachedChannels.fold<List<String>>(
ChannelModel.topLevelFields, (previousValue, element) {
return {...previousValue, ...element.extraData.keys}.toList();
});
return query.join([
leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
]).map((row) async {
final userEntity = row.readTable(users);
final channelEntity = row.readTable(channels);
sort = sort
?.where((s) => possibleSortingFields.contains(s.field))
?.toList(growable: false);
final cid = channelEntity.cid;
final members = await _db.memberDao.getMembersByCid(cid);
final reads = await _db.readDao.getReadsByCid(cid);
final messages = await _db.messageDao.getMessagesByCid(cid);
final pinnedMessages = await _db.pinnedMessageDao.getMessagesByCid(cid);
Comparator<ChannelModel> chainedComparator = (a, b) {
final dateA = a.lastMessageAt ?? a.createdAt;
final dateB = b.lastMessageAt ?? b.createdAt;
return dateB.compareTo(dateA);
};
return channelEntity.toChannelState(
createdBy: userEntity?.toUser(),
members: members,
reads: reads,
messages: messages,
pinnedMessages: pinnedMessages,
);
}).get();
}));
if (sort != null && sort.isNotEmpty) {
chainedComparator = (a, b) {
int result;
for (final comparator in sort.map((it) => it.comparator)) {
try {
result = comparator(a, b);
} catch (e) {
result = 0;
}
if (result != 0) return result;
}
return 0;
};
}
if (sort?.isEmpty != false && cachedChannels?.isNotEmpty == true) {
cachedChannels
.sort((a, b) => b.channel.updatedAt.compareTo(a.channel.updatedAt));
cachedChannels.sort((a, b) {
final dateA = a.channel.lastMessageAt ?? a.channel.createdAt;
final dateB = b.channel.lastMessageAt ?? b.channel.createdAt;
return dateB.compareTo(dateA);
});
cachedChannels.sort(chainedComparator);
if (paginationParams?.offset != null) {
cachedChannels.removeRange(0, paginationParams.offset);
}
if (paginationParams?.limit != null) {
return cachedChannels.take(paginationParams.limit).toList();
}
return cachedChannels;
@@ -161,14 +161,15 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
@override
Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter,
List<SortOption> sort = const [],
List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams,
}) {
return _db.channelQueryDao.getChannelStates(
}) async {
final channels = await _db.channelQueryDao.getChannels(
filter: filter,
sort: sort,
paginationParams: paginationParams,
);
return Future.wait(channels.map((e) => getChannelStateByCid(e.cid)));
}
@override