Merge pull request #1105 from GetStream/migrate/empty_array_to_null

change(llc,core,ui,persistence): Migrate List properties in llc models to be nullable
This commit is contained in:
Salvatore Giordano
2022-04-27 11:32:03 +02:00
committed by GitHub
24 changed files with 178 additions and 154 deletions
@@ -1259,7 +1259,9 @@ class Channel {
if (preferOffline && cid != null) { if (preferOffline && cid != null) {
final updatedState = await _client.chatPersistenceClient final updatedState = await _client.chatPersistenceClient
?.getChannelStateByCid(cid!, messagePagination: messagesPagination); ?.getChannelStateByCid(cid!, messagePagination: messagesPagination);
if (updatedState != null && updatedState.messages.isNotEmpty) { if (updatedState != null &&
updatedState.messages != null &&
updatedState.messages!.isNotEmpty) {
if (this.state == null) { if (this.state == null) {
_initState(updatedState); _initState(updatedState);
} else { } else {
@@ -1570,7 +1572,7 @@ class ChannelClientState {
void _checkExpiredAttachmentMessages(ChannelState channelState) async { void _checkExpiredAttachmentMessages(ChannelState channelState) async {
final expiredAttachmentMessagesId = channelState.messages final expiredAttachmentMessagesId = channelState.messages
.where((m) => ?.where((m) =>
!_updatedMessagesIds.contains(m.id) && !_updatedMessagesIds.contains(m.id) &&
m.attachments.isNotEmpty && m.attachments.isNotEmpty &&
m.attachments.any((e) { m.attachments.any((e) {
@@ -1597,7 +1599,8 @@ class ChannelClientState {
.map((e) => e.id) .map((e) => e.id)
.toList(); .toList();
if (expiredAttachmentMessagesId.isNotEmpty) { if (expiredAttachmentMessagesId != null &&
expiredAttachmentMessagesId.isNotEmpty) {
await _channel._initializedCompleter.future; await _channel._initializedCompleter.future;
_updatedMessagesIds.addAll(expiredAttachmentMessagesId); _updatedMessagesIds.addAll(expiredAttachmentMessagesId);
_channel.getMessagesById(expiredAttachmentMessagesId); _channel.getMessagesById(expiredAttachmentMessagesId);
@@ -1607,9 +1610,10 @@ class ChannelClientState {
void _listenMemberAdded() { void _listenMemberAdded() {
_subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) { _subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) {
final member = e.member; final member = e.member;
final existingMembers = channelState.members ?? [];
updateChannelState(channelState.copyWith( updateChannelState(channelState.copyWith(
members: [ members: [
...channelState.members, ...existingMembers,
member!, member!,
], ],
)); ));
@@ -1619,11 +1623,13 @@ class ChannelClientState {
void _listenMemberRemoved() { void _listenMemberRemoved() {
_subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) { _subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) {
final user = e.user; final user = e.user;
final existingMembers = channelState.members ?? [];
final existingRead = channelState.read ?? [];
updateChannelState(channelState.copyWith( updateChannelState(channelState.copyWith(
members: channelState.members members: existingMembers
.where((m) => m.userId != user!.id) .where((m) => m.userId != user!.id)
.toList(growable: false), .toList(growable: false),
read: channelState.read read: existingRead
.where((r) => r.user.id != user!.id) .where((r) => r.user.id != user!.id)
.toList(growable: false), .toList(growable: false),
)); ));
@@ -1793,9 +1799,10 @@ class ChannelClientState {
updateMessage(message); updateMessage(message);
if (message.pinned) { if (message.pinned) {
final _existingPinnedMessages = _channelState.pinnedMessages ?? [];
_channelState = _channelState.copyWith( _channelState = _channelState.copyWith(
pinnedMessages: [ pinnedMessages: [
..._channelState.pinnedMessages, ..._existingPinnedMessages,
message, message,
], ],
); );
@@ -1931,7 +1938,7 @@ class ChannelClientState {
) )
.listen( .listen(
(event) { (event) {
final readList = List<Read>.from(_channelState.read); final readList = List<Read>.from(_channelState.read ?? []);
final userReadIndex = final userReadIndex =
read.indexWhere((r) => r.user.id == event.user!.id); read.indexWhere((r) => r.user.id == event.user!.id);
@@ -1952,31 +1959,34 @@ class ChannelClientState {
} }
/// Channel message list. /// Channel message list.
List<Message> get messages => _channelState.messages; List<Message> get messages => _channelState.messages ?? <Message>[];
/// Channel message list as a stream. /// Channel message list as a stream.
Stream<List<Message>> get messagesStream => channelStateStream Stream<List<Message>> get messagesStream => channelStateStream
.map((cs) => cs.messages) .map((cs) => cs.messages ?? <Message>[])
.distinct(const ListEquality().equals); .distinct(const ListEquality().equals);
/// Channel pinned message list. /// Channel pinned message list.
List<Message> get pinnedMessages => _channelState.pinnedMessages; List<Message> get pinnedMessages =>
_channelState.pinnedMessages ?? <Message>[];
/// Channel pinned message list as a stream. /// Channel pinned message list as a stream.
Stream<List<Message>> get pinnedMessagesStream => channelStateStream Stream<List<Message>> get pinnedMessagesStream => channelStateStream
.map((cs) => cs.pinnedMessages) .map((cs) => cs.pinnedMessages ?? <Message>[])
.distinct(const ListEquality().equals); .distinct(const ListEquality().equals);
/// Get channel last message. /// Get channel last message.
Message? get lastMessage => Message? get lastMessage =>
_channelState.messages.isNotEmpty ? _channelState.messages.last : null; _channelState.messages != null && _channelState.messages!.isNotEmpty
? _channelState.messages!.last
: null;
/// Get channel last message. /// Get channel last message.
Stream<Message?> get lastMessageStream => Stream<Message?> get lastMessageStream =>
messagesStream.map((event) => event.isNotEmpty ? event.last : null); messagesStream.map((event) => event.isNotEmpty ? event.last : null);
/// Channel members list. /// Channel members list.
List<Member> get members => _channelState.members List<Member> get members => (_channelState.members ?? <Member>[])
.map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id]))
.toList(); .toList();
@@ -1997,7 +2007,7 @@ class ChannelClientState {
channelStateStream.map((cs) => cs.watcherCount); channelStateStream.map((cs) => cs.watcherCount);
/// Channel watchers list. /// Channel watchers list.
List<User> get watchers => _channelState.watchers List<User> get watchers => (_channelState.watchers ?? <User>[])
.map((e) => _channel.client.state.users[e.id] ?? e) .map((e) => _channel.client.state.users[e.id] ?? e)
.toList(); .toList();
@@ -2018,10 +2028,11 @@ class ChannelClientState {
String? get currentUserRole => currentUserMember?.role; String? get currentUserRole => currentUserMember?.role;
/// Channel read list. /// Channel read list.
List<Read> get read => _channelState.read; List<Read> get read => _channelState.read ?? <Read>[];
/// Channel read list as a stream. /// Channel read list as a stream.
Stream<List<Read>> get readStream => channelStateStream.map((cs) => cs.read); Stream<List<Read>> get readStream =>
channelStateStream.map((cs) => cs.read ?? <Read>[]);
bool _isCurrentUserRead(Read read) => bool _isCurrentUserRead(Read read) =>
read.user.id == _channel._client.state.currentUser!.id; read.user.id == _channel._client.state.currentUser!.id;
@@ -2042,7 +2053,7 @@ class ChannelClientState {
/// Setter for unread count. /// Setter for unread count.
set unreadCount(int count) { set unreadCount(int count) {
final reads = [..._channelState.read]; final reads = [...read];
final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead); final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead);
if (currentUserReadIndex < 0) return; if (currentUserReadIndex < 0) return;
@@ -2097,31 +2108,37 @@ class ChannelClientState {
/// Update channelState with updated information. /// Update channelState with updated information.
void updateChannelState(ChannelState updatedState) { void updateChannelState(ChannelState updatedState) {
final _existingStateMessages = _channelState.messages ?? [];
final _updatedStateMessages = updatedState.messages ?? [];
final newMessages = <Message>[ final newMessages = <Message>[
...updatedState.messages, ..._updatedStateMessages,
..._channelState.messages ..._existingStateMessages
.where((m) => .where((m) =>
!updatedState.messages.any((newMessage) => newMessage.id == m.id)) !_updatedStateMessages.any((newMessage) => newMessage.id == m.id))
.toList(), .toList(),
]..sort(_sortByCreatedAt); ]..sort(_sortByCreatedAt);
final _existingStateWatchers = _channelState.watchers ?? [];
final _updatedStateWatchers = updatedState.watchers ?? [];
final newWatchers = <User>[ final newWatchers = <User>[
...updatedState.watchers, ..._updatedStateWatchers,
..._channelState.watchers ..._existingStateWatchers
.where((w) => .where((w) =>
!updatedState.watchers.any((newWatcher) => newWatcher.id == w.id)) !_updatedStateWatchers.any((newWatcher) => newWatcher.id == w.id))
.toList(), .toList(),
]; ];
final newMembers = <Member>[ final newMembers = <Member>[
...updatedState.members, ...updatedState.members ?? [],
]; ];
final _existingStateRead = _channelState.read ?? [];
final _updatedStateRead = updatedState.read ?? [];
final newReads = <Read>[ final newReads = <Read>[
...updatedState.read, ..._updatedStateRead,
..._channelState.read ..._existingStateRead
.where((r) => .where((r) =>
!updatedState.read.any((newRead) => newRead.user.id == r.user.id)) !_updatedStateRead.any((newRead) => newRead.user.id == r.user.id))
.toList(), .toList(),
]; ];
@@ -2273,9 +2290,9 @@ class ChannelClientState {
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) { _pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
final now = DateTime.now(); final now = DateTime.now();
var expiredMessages = channelState.pinnedMessages var expiredMessages = channelState.pinnedMessages
.where((m) => m.pinExpires?.isBefore(now) == true) ?.where((m) => m.pinExpires?.isBefore(now) == true)
.toList(); .toList();
if (expiredMessages.isNotEmpty) { if (expiredMessages != null && expiredMessages.isNotEmpty) {
expiredMessages = expiredMessages expiredMessages = expiredMessages
.map((m) => m.copyWith( .map((m) => m.copyWith(
pinExpires: null, pinExpires: null,
@@ -623,7 +623,7 @@ class StreamChatClient {
final channels = res.channels; final channels = res.channels;
final users = channels final users = channels
.expand((it) => it.members) .expand((it) => it.members ?? <Member>[])
.map((it) => it.user) .map((it) => it.user)
.toList(growable: false); .toList(growable: false);
@@ -124,8 +124,7 @@ class Attachment extends Equatable {
final String? assetUrl; final String? assetUrl;
/// Actions from a command /// Actions from a command
@JsonKey(defaultValue: []) final List<Action>? actions;
final List<Action> actions;
final Uri? localUri; final Uri? localUri;
@@ -26,9 +26,8 @@ Attachment _$AttachmentFromJson(Map<String, dynamic> json) => Attachment(
authorIcon: json['author_icon'] as String?, authorIcon: json['author_icon'] as String?,
assetUrl: json['asset_url'] as String?, assetUrl: json['asset_url'] as String?,
actions: (json['actions'] as List<dynamic>?) actions: (json['actions'] as List<dynamic>?)
?.map((e) => Action.fromJson(e as Map<String, dynamic>)) ?.map((e) => Action.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
[],
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {}, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
file: json['file'] == null file: json['file'] == null
? null ? null
@@ -64,7 +63,7 @@ Map<String, dynamic> _$AttachmentToJson(Attachment instance) {
writeNotNull('author_link', instance.authorLink); writeNotNull('author_link', instance.authorLink);
writeNotNull('author_icon', instance.authorIcon); writeNotNull('author_icon', instance.authorIcon);
writeNotNull('asset_url', instance.assetUrl); writeNotNull('asset_url', instance.assetUrl);
val['actions'] = instance.actions.map((e) => e.toJson()).toList(); writeNotNull('actions', instance.actions?.map((e) => e.toJson()).toList());
writeNotNull('file', instance.file?.toJson()); writeNotNull('file', instance.file?.toJson());
val['upload_state'] = instance.uploadState.toJson(); val['upload_state'] = instance.uploadState.toJson();
val['extra_data'] = instance.extraData; val['extra_data'] = instance.extraData;
@@ -7,42 +7,40 @@ import 'package:stream_chat/src/core/models/user.dart';
part 'channel_state.g.dart'; part 'channel_state.g.dart';
const _emptyPinnedMessages = <Message>[];
/// The class that contains the information about a channel /// The class that contains the information about a channel
@JsonSerializable() @JsonSerializable()
class ChannelState { class ChannelState {
/// Constructor used for json serialization /// Constructor used for json serialization
ChannelState({ ChannelState({
this.channel, this.channel,
this.messages = const [], this.messages,
this.members = const [], this.members,
this.pinnedMessages = _emptyPinnedMessages, this.pinnedMessages,
this.watcherCount, this.watcherCount,
this.watchers = const [], this.watchers,
this.read = const [], this.read,
}); });
/// The channel to which this state belongs /// The channel to which this state belongs
final ChannelModel? channel; final ChannelModel? channel;
/// A paginated list of channel messages /// A paginated list of channel messages
final List<Message> messages; final List<Message>? messages;
/// A paginated list of channel members /// A paginated list of channel members
final List<Member> members; final List<Member>? members;
/// A paginated list of pinned messages /// A paginated list of pinned messages
final List<Message> pinnedMessages; final List<Message>? pinnedMessages;
/// The count of users watching the channel /// The count of users watching the channel
final int? watcherCount; final int? watcherCount;
/// A paginated list of users watching the channel /// A paginated list of users watching the channel
final List<User> watchers; final List<User>? watchers;
/// The list of channel reads /// The list of channel reads
final List<Read> read; final List<Read>? read;
/// Create a new instance from a json /// Create a new instance from a json
static ChannelState fromJson(Map<String, dynamic> json) => static ChannelState fromJson(Map<String, dynamic> json) =>
@@ -56,7 +54,7 @@ class ChannelState {
ChannelModel? channel, ChannelModel? channel,
List<Message>? messages, List<Message>? messages,
List<Member>? members, List<Member>? members,
List<Message> pinnedMessages = _emptyPinnedMessages, List<Message>? pinnedMessages,
int? watcherCount, int? watcherCount,
List<User>? watchers, List<User>? watchers,
List<Read>? read, List<Read>? read,
@@ -65,11 +63,7 @@ class ChannelState {
channel: channel ?? this.channel, channel: channel ?? this.channel,
messages: messages ?? this.messages, messages: messages ?? this.messages,
members: members ?? this.members, members: members ?? this.members,
// Hack to avoid using the default value in case nothing is provided. pinnedMessages: pinnedMessages ?? this.pinnedMessages,
// FIXME: Use non-nullable by default instead of empty list.
pinnedMessages: pinnedMessages == _emptyPinnedMessages
? this.pinnedMessages
: pinnedMessages,
watcherCount: watcherCount ?? this.watcherCount, watcherCount: watcherCount ?? this.watcherCount,
watchers: watchers ?? this.watchers, watchers: watchers ?? this.watchers,
read: read ?? this.read, read: read ?? this.read,
@@ -11,36 +11,31 @@ ChannelState _$ChannelStateFromJson(Map<String, dynamic> json) => ChannelState(
? null ? null
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>), : ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
messages: (json['messages'] as List<dynamic>?) messages: (json['messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
const [],
members: (json['members'] as List<dynamic>?) members: (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
const [],
pinnedMessages: (json['pinned_messages'] as List<dynamic>?) pinnedMessages: (json['pinned_messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
_emptyPinnedMessages,
watcherCount: json['watcher_count'] as int?, watcherCount: json['watcher_count'] as int?,
watchers: (json['watchers'] as List<dynamic>?) watchers: (json['watchers'] as List<dynamic>?)
?.map((e) => User.fromJson(e as Map<String, dynamic>)) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
const [],
read: (json['read'] as List<dynamic>?) read: (json['read'] as List<dynamic>?)
?.map((e) => Read.fromJson(e as Map<String, dynamic>)) ?.map((e) => Read.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList(),
const [],
); );
Map<String, dynamic> _$ChannelStateToJson(ChannelState instance) => Map<String, dynamic> _$ChannelStateToJson(ChannelState instance) =>
<String, dynamic>{ <String, dynamic>{
'channel': instance.channel?.toJson(), 'channel': instance.channel?.toJson(),
'messages': instance.messages.map((e) => e.toJson()).toList(), 'messages': instance.messages?.map((e) => e.toJson()).toList(),
'members': instance.members.map((e) => e.toJson()).toList(), 'members': instance.members?.map((e) => e.toJson()).toList(),
'pinned_messages': 'pinned_messages':
instance.pinnedMessages.map((e) => e.toJson()).toList(), instance.pinnedMessages?.map((e) => e.toJson()).toList(),
'watcher_count': instance.watcherCount, 'watcher_count': instance.watcherCount,
'watchers': instance.watchers.map((e) => e.toJson()).toList(), 'watchers': instance.watchers?.map((e) => e.toJson()).toList(),
'read': instance.read.map((e) => e.toJson()).toList(), 'read': instance.read?.map((e) => e.toJson()).toList(),
}; };
@@ -49,6 +49,9 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
updatedAt: json['updated_at'] == null updatedAt: json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
user: json['user'] == null user: json['user'] == null
? null ? null
: User.fromJson(json['user'] as Map<String, dynamic>), : User.fromJson(json['user'] as Map<String, dynamic>),
@@ -63,9 +66,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
? null ? null
: User.fromJson(json['pinned_by'] as Map<String, dynamic>), : User.fromJson(json['pinned_by'] as Map<String, dynamic>),
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {}, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
i18n: (json['i18n'] as Map<String, dynamic>?)?.map( i18n: (json['i18n'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String), (k, e) => MapEntry(k, e as String),
), ),
@@ -99,6 +99,7 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
val['silent'] = instance.silent; val['silent'] = instance.silent;
writeNotNull('shadowed', readonly(instance.shadowed)); writeNotNull('shadowed', readonly(instance.shadowed));
writeNotNull('command', readonly(instance.command)); writeNotNull('command', readonly(instance.command));
writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('created_at', readonly(instance.createdAt)); writeNotNull('created_at', readonly(instance.createdAt));
writeNotNull('updated_at', readonly(instance.updatedAt)); writeNotNull('updated_at', readonly(instance.updatedAt));
writeNotNull('user', readonly(instance.user)); writeNotNull('user', readonly(instance.user));
@@ -107,7 +108,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
val['pin_expires'] = instance.pinExpires?.toIso8601String(); val['pin_expires'] = instance.pinExpires?.toIso8601String();
val['pinned_by'] = readonly(instance.pinnedBy); val['pinned_by'] = readonly(instance.pinnedBy);
val['extra_data'] = instance.extraData; val['extra_data'] = instance.extraData;
writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('i18n', instance.i18n); writeNotNull('i18n', instance.i18n);
return val; return val;
} }
@@ -44,10 +44,10 @@ abstract class ChatPersistenceClient {
Future<ChannelModel?> getChannelByCid(String cid); Future<ChannelModel?> getChannelByCid(String cid);
/// Get stored channel [Member]s by providing channel [cid] /// Get stored channel [Member]s by providing channel [cid]
Future<List<Member>> getMembersByCid(String cid); Future<List<Member>?> getMembersByCid(String cid);
/// Get stored channel [Read]s by providing channel [cid] /// Get stored channel [Read]s by providing channel [cid]
Future<List<Read>> getReadsByCid(String cid); Future<List<Read>?> getReadsByCid(String cid);
/// Get stored [Message]s by providing channel [cid] /// Get stored [Message]s by providing channel [cid]
/// ///
@@ -78,15 +78,11 @@ abstract class ChatPersistenceClient {
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination), getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
]); ]);
return ChannelState( return ChannelState(
// ignore: cast_nullable_to_non_nullable members: data[0] as List<Member>?,
members: data[0] as List<Member>, read: data[1] as List<Read>?,
// ignore: cast_nullable_to_non_nullable
read: data[1] as List<Read>,
channel: data[2] as ChannelModel?, channel: data[2] as ChannelModel?,
// ignore: cast_nullable_to_non_nullable messages: data[3] as List<Message>?,
messages: data[3] as List<Message>, pinnedMessages: data[4] as List<Message>?,
// ignore: cast_nullable_to_non_nullable
pinnedMessages: data[4] as List<Message>,
); );
} }
@@ -146,7 +142,7 @@ abstract class ChatPersistenceClient {
bulkUpdateMessages({cid: messages}); bulkUpdateMessages({cid: messages});
/// Bulk updates the message data of multiple channels. /// Bulk updates the message data of multiple channels.
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages); Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages);
/// Updates the pinned message data of a particular channel [cid] with /// Updates the pinned message data of a particular channel [cid] with
/// the new [messages] data /// the new [messages] data
@@ -154,7 +150,7 @@ abstract class ChatPersistenceClient {
bulkUpdatePinnedMessages({cid: messages}); bulkUpdatePinnedMessages({cid: messages});
/// Bulk updates the message data of multiple channels. /// Bulk updates the message data of multiple channels.
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages); Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages);
/// Returns all the threads by parent message of a particular channel by /// Returns all the threads by parent message of a particular channel by
/// providing channel [cid] /// providing channel [cid]
@@ -169,7 +165,7 @@ abstract class ChatPersistenceClient {
bulkUpdateMembers({cid: members}); bulkUpdateMembers({cid: members});
/// Bulk updates the members data of multiple channels. /// Bulk updates the members data of multiple channels.
Future<void> bulkUpdateMembers(Map<String, List<Member>> members); Future<void> bulkUpdateMembers(Map<String, List<Member>?> members);
/// Updates the read data of a particular channel [cid] with /// Updates the read data of a particular channel [cid] with
/// the new [reads] data /// the new [reads] data
@@ -177,7 +173,7 @@ abstract class ChatPersistenceClient {
bulkUpdateReads({cid: reads}); bulkUpdateReads({cid: reads});
/// Bulk updates the read data of multiple channels. /// Bulk updates the read data of multiple channels.
Future<void> bulkUpdateReads(Map<String, List<Read>> reads); Future<void> bulkUpdateReads(Map<String, List<Read>?> reads);
/// Updates the users data with the new [users] data /// Updates the users data with the new [users] data
Future<void> updateUsers(List<User> users); Future<void> updateUsers(List<User> users);
@@ -230,10 +226,10 @@ abstract class ChatPersistenceClient {
final membersToDelete = <String>[]; final membersToDelete = <String>[];
final channels = <ChannelModel>[]; final channels = <ChannelModel>[];
final channelWithMessages = <String, List<Message>>{}; final channelWithMessages = <String, List<Message>?>{};
final channelWithPinnedMessages = <String, List<Message>>{}; final channelWithPinnedMessages = <String, List<Message>?>{};
final channelWithReads = <String, List<Read>>{}; final channelWithReads = <String, List<Read>?>{};
final channelWithMembers = <String, List<Member>>{}; final channelWithMembers = <String, List<Member>?>{};
final users = <User>[]; final users = <User>[];
final reactions = <Reaction>[]; final reactions = <Reaction>[];
@@ -252,8 +248,9 @@ abstract class ChatPersistenceClient {
// Preparing deletion data // Preparing deletion data
membersToDelete.add(cid); membersToDelete.add(cid);
reactionsToDelete.addAll(state.messages.map((it) => it.id)); reactionsToDelete.addAll(state.messages?.map((it) => it.id) ?? []);
pinnedReactionsToDelete.addAll(state.pinnedMessages.map((it) => it.id)); pinnedReactionsToDelete
.addAll(state.pinnedMessages?.map((it) => it.id) ?? []);
// preparing addition data // preparing addition data
channelWithReads[cid] = reads; channelWithReads[cid] = reads;
@@ -261,14 +258,14 @@ abstract class ChatPersistenceClient {
channelWithMessages[cid] = messages; channelWithMessages[cid] = messages;
channelWithPinnedMessages[cid] = pinnedMessages; channelWithPinnedMessages[cid] = pinnedMessages;
reactions.addAll(messages.expand(_expandReactions)); reactions.addAll(messages?.expand(_expandReactions) ?? []);
pinnedReactions.addAll(pinnedMessages.expand(_expandReactions)); pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);
users.addAll([ users.addAll([
channel.createdBy, channel.createdBy,
...messages.map((it) => it.user), ...messages?.map((it) => it.user) ?? <User>[],
...reads.map((it) => it.user), ...reads?.map((it) => it.user) ?? <User>[],
...members.map((it) => it.user), ...members?.map((it) => it.user) ?? <User>[],
...reactions.map((it) => it.user), ...reactions.map((it) => it.user),
...pinnedReactions.map((it) => it.user), ...pinnedReactions.map((it) => it.user),
].withNullifyer); ].withNullifyer);
@@ -105,11 +105,11 @@ void main() {
); );
expect(res, isNotNull); expect(res, isNotNull);
expect(res.messages.length, channelState.messages.length); expect(res.messages?.length, channelState.messages?.length);
expect(res.pinnedMessages.length, channelState.pinnedMessages.length); expect(res.pinnedMessages?.length, channelState.pinnedMessages?.length);
expect(res.members.length, channelState.members.length); expect(res.members?.length, channelState.members?.length);
expect(res.read.length, channelState.read.length); expect(res.read?.length, channelState.read?.length);
expect(res.watchers.length, channelState.watchers.length); expect(res.watchers?.length, channelState.watchers?.length);
expect(res.watcherCount, channelState.watcherCount); expect(res.watcherCount, channelState.watcherCount);
verify(() => client.post(path, data: any(named: 'data'))).called(1); verify(() => client.post(path, data: any(named: 'data'))).called(1);
@@ -19,8 +19,10 @@ void main() {
attachment.thumbUrl, attachment.thumbUrl,
'https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif', 'https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif',
); );
expect(attachment.actions, isNotNull);
expect(attachment.actions, isNotEmpty);
expect(attachment.actions, hasLength(3)); expect(attachment.actions, hasLength(3));
expect(attachment.actions[0], isA<Action>()); expect(attachment.actions![0], isA<Action>());
}); });
test('should serialize to json correctly', () { test('should serialize to json correctly', () {
@@ -30,14 +30,16 @@ void main() {
channelState.channel?.extraData['image'], channelState.channel?.extraData['image'],
'https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png', 'https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png',
); );
expect(channelState.messages, isNotNull);
expect(channelState.messages, isNotEmpty);
expect(channelState.messages, hasLength(25)); expect(channelState.messages, hasLength(25));
expect(channelState.messages[0], isA<Message>()); expect(channelState.messages![0], isA<Message>());
expect(channelState.messages[0], isNotNull); expect(channelState.messages![0], isNotNull);
expect( expect(
channelState.messages[0].createdAt, channelState.messages![0].createdAt,
DateTime.parse('2020-01-29T03:23:02.843948Z'), DateTime.parse('2020-01-29T03:23:02.843948Z'),
); );
expect(channelState.messages[0].user, isA<User>()); expect(channelState.messages![0].user, isA<User>());
expect(channelState.watcherCount, 5); expect(channelState.watcherCount, 5);
}); });
@@ -117,19 +117,20 @@ class TestPersistenceClient extends ChatPersistenceClient {
Future<void> updateUsers(List<User> users) => Future.value(); Future<void> updateUsers(List<User> users) => Future.value();
@override @override
Future<void> bulkUpdateMembers(Map<String, List<Member>> members) => Future<void> bulkUpdateMembers(Map<String, List<Member>?> members) =>
Future.value(); Future.value();
@override @override
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages) => Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages) =>
Future.value(); Future.value();
@override @override
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages) => Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages) =>
Future.value(); Future.value();
@override @override
Future<void> bulkUpdateReads(Map<String, List<Read>> reads) => Future.value(); Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) =>
Future.value();
} }
void main() { void main() {
@@ -45,7 +45,7 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
if (imageUrl == null) { if (imageUrl == null) {
return const AttachmentError(); return const AttachmentError();
} }
if (attachment.actions.isNotEmpty) { if (attachment.actions != null && attachment.actions!.isNotEmpty) {
return _buildSendingAttachment(context, imageUrl); return _buildSendingAttachment(context, imageUrl);
} }
return _buildSentAttachment(context, imageUrl); return _buildSentAttachment(context, imageUrl);
@@ -286,7 +286,6 @@ class StreamMessageSearchListView extends StatelessWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
final Clip clipBehavior; final Clip clipBehavior;
@override @override
Widget build(BuildContext context) => Widget build(BuildContext context) =>
PagedValueListView<String, GetMessageResponse>( PagedValueListView<String, GetMessageResponse>(
@@ -106,7 +106,9 @@ class StreamChannelState extends State<StreamChannel> {
limit: limit, limit: limit,
preferOffline: preferOffline, preferOffline: preferOffline,
); );
if (state.messages.isEmpty || state.messages.length < limit) { if (state.messages == null ||
state.messages!.isEmpty ||
state.messages!.length < limit) {
_topPaginationEnded = true; _topPaginationEnded = true;
} }
_queryTopMessagesController.safeAdd(false); _queryTopMessagesController.safeAdd(false);
@@ -137,7 +139,9 @@ class StreamChannelState extends State<StreamChannel> {
limit: limit, limit: limit,
preferOffline: preferOffline, preferOffline: preferOffline,
); );
if (state.messages.isEmpty || state.messages.length < limit) { if (state.messages == null ||
state.messages!.isEmpty ||
state.messages!.length < limit) {
_bottomPaginationEnded = true; _bottomPaginationEnded = true;
} }
_queryBottomMessagesController.safeAdd(false); _queryBottomMessagesController.safeAdd(false);
@@ -299,7 +303,9 @@ class StreamChannelState extends State<StreamChannel> {
), ),
preferOffline: preferOffline, preferOffline: preferOffline,
); );
if (state.messages.isEmpty || state.messages.length < limit) { if (state.messages == null ||
state.messages!.isEmpty ||
state.messages!.length < limit) {
channel.state?.isUpToDate = true; channel.state?.isUpToDate = true;
} }
return state; return state;
@@ -98,8 +98,9 @@ class HomeScreen extends StatelessWidget {
AsyncSnapshot<ChannelState?> snapshot, AsyncSnapshot<ChannelState?> snapshot,
) { ) {
if (snapshot.hasData && snapshot.data != null) { if (snapshot.hasData && snapshot.data != null) {
final _messages = snapshot.data!.messages ?? [];
return MessageView( return MessageView(
messages: snapshot.data!.messages.reversed.toList(), messages: _messages.reversed.toList(),
channel: channel, channel: channel,
); );
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
@@ -34,14 +34,20 @@ class MemberDao extends DatabaseAccessor<DriftChatDatabase>
bulkUpdateMembers({cid: memberList}); bulkUpdateMembers({cid: memberList});
/// Bulk updates the members data of multiple channels /// Bulk updates the members data of multiple channels
Future<void> bulkUpdateMembers(Map<String, List<Member>> channelWithMembers) { Future<void> bulkUpdateMembers(
Map<String, List<Member>?> channelWithMembers,
) {
final entities = channelWithMembers.entries final entities = channelWithMembers.entries
.map((entry) => entry.value.map( .map((entry) =>
(entry.value?.map(
(member) => member.toEntity(cid: entry.key), (member) => member.toEntity(cid: entry.key),
)) )) ??
[])
.expand((it) => it) .expand((it) => it)
.toList(growable: false); .toList(growable: false);
return batch((batch) => batch.insertAllOnConflictUpdate(members, entities)); return batch(
(batch) => batch.insertAllOnConflictUpdate(members, entities),
);
} }
/// Deletes all the members whose [Members.channelCid] is present in [cids] /// Deletes all the members whose [Members.channelCid] is present in [cids]
@@ -183,12 +183,14 @@ class MessageDao extends DatabaseAccessor<DriftChatDatabase>
/// Bulk updates the message data of multiple channels /// Bulk updates the message data of multiple channels
Future<void> bulkUpdateMessages( Future<void> bulkUpdateMessages(
Map<String, List<Message>> channelWithMessages, Map<String, List<Message>?> channelWithMessages,
) { ) {
final entities = channelWithMessages.entries final entities = channelWithMessages.entries
.map((entry) => entry.value.map( .map((entry) =>
entry.value?.map(
(message) => message.toEntity(cid: entry.key), (message) => message.toEntity(cid: entry.key),
)) ) ??
[])
.expand((it) => it) .expand((it) => it)
.toList(growable: false); .toList(growable: false);
return batch( return batch(
@@ -183,12 +183,14 @@ class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
/// Bulk updates the message data of multiple channels /// Bulk updates the message data of multiple channels
Future<void> bulkUpdateMessages( Future<void> bulkUpdateMessages(
Map<String, List<Message>> channelWithMessages, Map<String, List<Message>?> channelWithMessages,
) { ) {
final entities = channelWithMessages.entries final entities = channelWithMessages.entries
.map((entry) => entry.value.map( .map((entry) =>
entry.value?.map(
(message) => message.toPinnedEntity(cid: entry.key), (message) => message.toPinnedEntity(cid: entry.key),
)) ) ??
[])
.expand((it) => it) .expand((it) => it)
.toList(growable: false); .toList(growable: false);
return batch( return batch(
@@ -33,11 +33,13 @@ class ReadDao extends DatabaseAccessor<DriftChatDatabase> with _$ReadDaoMixin {
bulkUpdateReads({cid: readList}); bulkUpdateReads({cid: readList});
/// Bulk updates the reads data of multiple channels /// Bulk updates the reads data of multiple channels
Future<void> bulkUpdateReads(Map<String, List<Read>> channelWithReads) { Future<void> bulkUpdateReads(Map<String, List<Read>?> channelWithReads) {
final entities = channelWithReads.entries final entities = channelWithReads.entries
.map((entry) => entry.value.map( .map((entry) =>
entry.value?.map(
(read) => read.toEntity(cid: entry.key), (read) => read.toEntity(cid: entry.key),
)) ) ??
[])
.expand((it) => it) .expand((it) => it)
.toList(growable: false); .toList(growable: false);
return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities)); return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities));
@@ -56,7 +56,7 @@ class DriftChatDatabase extends _$DriftChatDatabase {
// you should bump this number whenever you change or add a table definition. // you should bump this number whenever you change or add a table definition.
@override @override
int get schemaVersion => 6; int get schemaVersion => 7;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -295,21 +295,21 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
} }
@override @override
Future<void> bulkUpdateMembers(Map<String, List<Member>> members) { Future<void> bulkUpdateMembers(Map<String, List<Member>?> members) {
assert(_debugIsConnected, ''); assert(_debugIsConnected, '');
_logger.info('bulkUpdateMembers'); _logger.info('bulkUpdateMembers');
return _readProtected(() => db!.memberDao.bulkUpdateMembers(members)); return _readProtected(() => db!.memberDao.bulkUpdateMembers(members));
} }
@override @override
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages) { Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages) {
assert(_debugIsConnected, ''); assert(_debugIsConnected, '');
_logger.info('bulkUpdateMessages'); _logger.info('bulkUpdateMessages');
return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages)); return _readProtected(() => db!.messageDao.bulkUpdateMessages(messages));
} }
@override @override
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages) { Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages) {
assert(_debugIsConnected, ''); assert(_debugIsConnected, '');
_logger.info('bulkUpdatePinnedMessages'); _logger.info('bulkUpdatePinnedMessages');
return _readProtected( return _readProtected(
@@ -334,7 +334,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
} }
@override @override
Future<void> bulkUpdateReads(Map<String, List<Read>> reads) { Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) {
assert(_debugIsConnected, ''); assert(_debugIsConnected, '');
_logger.info('bulkUpdateReads'); _logger.info('bulkUpdateReads');
return _readProtected(() => db!.readDao.bulkUpdateReads(reads)); return _readProtected(() => db!.readDao.bulkUpdateReads(reads));
@@ -61,10 +61,10 @@ void main() {
); );
expect(channelState, isA<ChannelState>()); expect(channelState, isA<ChannelState>());
expect(channelState.members.length, members.length); expect(channelState.members?.length, members.length);
expect(channelState.read.length, reads.length); expect(channelState.read?.length, reads.length);
expect(channelState.messages.length, messages.length); expect(channelState.messages?.length, messages.length);
expect(channelState.pinnedMessages.length, messages.length); expect(channelState.pinnedMessages?.length, messages.length);
final channelModel = channelState.channel!; final channelModel = channelState.channel!;
expect(channelModel.id, entity.id); expect(channelModel.id, entity.id);
@@ -219,10 +219,10 @@ void main() {
.thenAnswer((_) async => messages); .thenAnswer((_) async => messages);
final fetchedChannelState = await client.getChannelStateByCid(cid); final fetchedChannelState = await client.getChannelStateByCid(cid);
expect(fetchedChannelState.messages.length, messages.length); expect(fetchedChannelState.messages?.length, messages.length);
expect(fetchedChannelState.pinnedMessages.length, messages.length); expect(fetchedChannelState.pinnedMessages?.length, messages.length);
expect(fetchedChannelState.members.length, members.length); expect(fetchedChannelState.members?.length, members.length);
expect(fetchedChannelState.read.length, reads.length); expect(fetchedChannelState.read?.length, reads.length);
expect(fetchedChannelState.channel!.cid, channel.cid); expect(fetchedChannelState.channel!.cid, channel.cid);
verify(() => mockDatabase.memberDao.getMembersByCid(cid)).called(1); verify(() => mockDatabase.memberDao.getMembersByCid(cid)).called(1);
@@ -277,10 +277,10 @@ void main() {
for (var i = 0; i < fetchedChannelStates.length; i++) { for (var i = 0; i < fetchedChannelStates.length; i++) {
final original = channelStates[i]; final original = channelStates[i];
final fetched = fetchedChannelStates[i]; final fetched = fetchedChannelStates[i];
expect(fetched.members.length, original.members.length); expect(fetched.members?.length, original.members?.length);
expect(fetched.messages.length, original.messages.length); expect(fetched.messages?.length, original.messages?.length);
expect(fetched.pinnedMessages.length, original.pinnedMessages.length); expect(fetched.pinnedMessages?.length, original.pinnedMessages?.length);
expect(fetched.read.length, original.read.length); expect(fetched.read?.length, original.read?.length);
expect(fetched.channel!.cid, original.channel!.cid); expect(fetched.channel!.cid, original.channel!.cid);
} }