merge origin/develop into ref/segregate-api-layer

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2021-06-15 17:32:09 +05:30
62 changed files with 2287 additions and 1620 deletions
+7
View File
@@ -1,3 +1,10 @@
## 2.0.0-nullsafety.5
- Minor fixes
- Performance improvements
- Fixed `skip_push` in `client.sendMessage`
- Added partial message update method
## 2.0.0-nullsafety.2
- Added new `Filter.raw` constructor
@@ -4,15 +4,15 @@ import 'dart:math';
import 'package:collection/collection.dart'
show IterableExtension, ListEquality;
import 'package:dio/dio.dart';
import 'package:rate_limiter/rate_limiter.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/retry_queue.dart';
import 'package:stream_chat/src/core/util/utils.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:rate_limiter/rate_limiter.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/core/util/utils.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/stream_chat.dart';
/// This a the class that manages a specific channel.
@@ -70,8 +70,11 @@ class Channel {
true;
/// Returns true if the channel is muted as a stream
Stream<bool>? get isMutedStream => _client.state.userStream.map((event) =>
event!.channelMutes.any((element) => element.channel.cid == cid) == true);
Stream<bool>? get isMutedStream => _client.state.userStream
.map((event) =>
event!.channelMutes.any((element) => element.channel.cid == cid) ==
true)
.distinct();
/// True if the channel is a group
bool get isGroup => memberCount != 2;
@@ -253,7 +256,10 @@ class Channel {
String messageId,
Iterable<String> attachmentIds,
) {
final message = state!.messages.firstWhereOrNull(
final message = [
...state!.messages,
...state!.threads.values.expand((messages) => messages),
].firstWhereOrNull(
(it) => it.id == messageId,
);
@@ -351,9 +357,13 @@ class Channel {
}
/// Send a [message] to this channel.
/// If [skipPush] is true the message will not send a push notification
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually sending the message.
Future<SendMessageResponse> sendMessage(Message message) async {
Future<SendMessageResponse> sendMessage(
Message message, {
bool skipPush = false,
}) async {
_checkInitialized();
// Cancelling previous completer in case it's called again in the process
// Eg. Updating the message while the previous call is in progress.
@@ -386,7 +396,6 @@ class Channel {
_messageAttachmentsUploadCompleter[message.id] =
attachmentsUploadCompleter;
// ignore: unawaited_futures
_uploadAttachments(
message.id,
message.attachments.map((it) => it.id),
@@ -396,7 +405,12 @@ class Channel {
message = await attachmentsUploadCompleter.future;
}
final response = await _client.sendMessage(message, id!, type);
final response = await _client.sendMessage(
message,
id!,
type,
skipPush: skipPush,
);
state!.addMessage(response.message);
return response;
} catch (e) {
@@ -411,6 +425,8 @@ class Channel {
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually updating the message.
Future<UpdateMessageResponse> updateMessage(Message message) async {
final originalMessage = message;
// Cancelling previous completer in case it's called again in the process
// Eg. Updating the message while the previous call is in progress.
_messageAttachmentsUploadCompleter
@@ -432,12 +448,11 @@ class Channel {
state?.addMessage(message);
try {
if (message.attachments.any((it) => !it.uploadState.isSuccess) == true) {
if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
final attachmentsUploadCompleter = Completer<Message>();
_messageAttachmentsUploadCompleter[message.id] =
attachmentsUploadCompleter;
// ignore: unawaited_futures
_uploadAttachments(
message.id,
message.attachments.map((it) => it.id),
@@ -455,6 +470,40 @@ class Channel {
state?.addMessage(m);
return response;
} catch (e) {
if (e is StreamChatNetworkError) {
if (e.isRetriable) {
state!._retryQueue.add([message]);
} else {
state?.addMessage(originalMessage);
}
}
rethrow;
}
}
/// Partially updates the [message] in this channel.
/// Use [set] to define values to be set
/// Use [unset] to define values to be unset
Future<UpdateMessageResponse> partialUpdateMessage(
Message message, {
Map<String, Object?>? set,
List<String>? unset,
}) async {
try {
final response = await _client.partialUpdateMessage(
message.id,
set: set,
unset: unset,
);
final updatedMessage = response.message.copyWith(
ownReactions: message.ownReactions,
);
state?.addMessage(updatedMessage);
return response;
} catch (e) {
if (e is StreamChatNetworkError && e.isRetriable) {
@@ -527,17 +576,23 @@ class Channel {
Duration(seconds: timeoutOrExpirationDate.toInt()),
);
}
return updateMessage(
message.copyWith(
pinned: true,
pinExpires: pinExpires,
),
return partialUpdateMessage(
message,
set: {
'pinned': true,
'pin_expires': pinExpires?.toUtc().toIso8601String(),
},
);
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) =>
updateMessage(message.copyWith(pinned: false));
partialUpdateMessage(
message,
set: {
'pinned': false,
},
);
/// Send a file to this channel
Future<SendFileResponse> sendFile(
@@ -747,11 +802,12 @@ class Channel {
}
/// Edit the channel custom data
Future<PartialUpdateChannelResponse> updatePartial(
Map<String, dynamic> channelData,
) async {
Future<PartialUpdateChannelResponse> updatePartial({
Map<String, Object?>? set,
List<String>? unset,
}) async {
_checkInitialized();
return _client.updateChannelPartial(id!, type, channelData);
return _client.updateChannelPartial(id!, type, set: set, unset: unset);
}
/// Delete this channel. Messages are permanently removed.
@@ -856,9 +912,9 @@ class Channel {
/// particular message as read
Future<EmptyResponse> markRead({String? messageId}) async {
_checkInitialized();
client.state.totalUnreadCount = max(
0, (client.state.totalUnreadCount ?? 0) - (state!.unreadCount ?? 0));
state!._unreadCountController.add(0);
client.state.totalUnreadCount =
max(0, (client.state.totalUnreadCount) - (state!.unreadCount));
state!.unreadCount = 0;
return _client.markChannelRead(id!, type, messageId: messageId);
}
@@ -1252,7 +1308,7 @@ class ChannelClientState {
(r) => r.user.id == _channel._client.state.user?.id,
);
if (userRead != null) {
_unreadCountController.add(userRead.unreadMessages);
unreadCount = userRead.unreadMessages;
}
}
@@ -1432,7 +1488,7 @@ class ChannelClientState {
}
if (_countMessageAsUnread(message)) {
_unreadCountController.add(_unreadCountController.value + 1);
unreadCount += 1;
}
}));
}
@@ -1488,7 +1544,7 @@ class ChannelClientState {
if (userReadIndex != null && userReadIndex != -1) {
final userRead = readList.removeAt(userReadIndex);
if (userRead.user.id == _channel._client.state.user!.id) {
_unreadCountController.add(0);
unreadCount = 0;
}
readList.add(Read(
user: event.user!,
@@ -1508,7 +1564,7 @@ class ChannelClientState {
/// Channel message list as a stream
Stream<List<Message>?> get messagesStream => channelStateStream
.map((cs) => cs.messages)
.distinct((prev, next) => const ListEquality().equals(prev, next));
.distinct(const ListEquality().equals);
/// Channel pinned message list
List<Message>? get pinnedMessages => _channelState.pinnedMessages.toList();
@@ -1538,7 +1594,7 @@ class ChannelClientState {
_channel.client.state.usersStream,
(members, users) =>
members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(),
);
).distinct(const ListEquality().equals);
/// Channel watcher count
int? get watcherCount => _channelState.watcherCount;
@@ -1568,11 +1624,13 @@ class ChannelClientState {
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
set unreadCount(int value) => _unreadCountController.add(value);
/// Unread count getter as a stream
Stream<int> get unreadCountStream => _unreadCountController.stream;
Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
/// Unread count getter
int? get unreadCount => _unreadCountController.value;
int get unreadCount => _unreadCountController.value;
bool _countMessageAsUnread(Message message) {
final userId = _channel.client.state.user?.id;
@@ -1708,7 +1766,9 @@ class ChannelClientState {
List<User> get typingEvents => _typingEventsController.value;
/// Channel related typing users stream
Stream<List<User>> get typingEventsStream => _typingEventsController.stream;
Stream<List<User>> get typingEventsStream =>
_typingEventsController.stream.distinct(const ListEquality().equals);
final BehaviorSubject<List<User>> _typingEventsController =
BehaviorSubject.seeded([]);
+57 -31
View File
@@ -196,7 +196,7 @@ class StreamChatClient {
_wsConnectionStatusController.add(status);
/// The current status value of the websocket connection
ConnectionStatus? get wsConnectionStatus =>
ConnectionStatus get wsConnectionStatus =>
_wsConnectionStatusController.value;
/// This notifies the connection status of the websocket connection.
@@ -745,13 +745,15 @@ class StreamChatClient {
/// Updates the [channelId] of type [ChannelType] data with [data]
Future<PartialUpdateChannelResponse> updateChannelPartial(
String channelId,
String channelType,
Map<String, dynamic> data,
) =>
String channelType, {
Map<String, Object?>? set,
List<String>? unset,
}) =>
_chatApi.channel.updateChannelPartial(
channelId,
channelType,
data,
set: set,
unset: unset,
);
/// Add a device for Push Notifications.
@@ -1125,12 +1127,14 @@ class StreamChatClient {
Future<SendMessageResponse> sendMessage(
Message message,
String channelId,
String channelType,
) =>
String channelType, {
bool skipPush = false,
}) =>
_chatApi.message.sendMessage(
channelId,
channelType,
message,
skipPush: skipPush,
);
/// Lists all the message replies for the [parentId]
@@ -1157,6 +1161,20 @@ class StreamChatClient {
Future<UpdateMessageResponse> updateMessage(Message message) =>
_chatApi.message.updateMessage(message);
/// Partially update the given [messageId]
/// Use [set] to define values to be set
/// Use [unset] to define values to be unset
Future<UpdateMessageResponse> partialUpdateMessage(
String messageId, {
Map<String, Object?>? set,
List<String>? unset,
}) =>
_chatApi.message.partialUpdateMessage(
messageId,
set: set,
unset: unset,
);
/// Deletes the given message
Future<EmptyResponse> deleteMessage(String messageId) =>
_chatApi.message.deleteMessage(messageId);
@@ -1192,7 +1210,7 @@ class StreamChatClient {
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
/// to be added to [DateTime.now]
Future<UpdateMessageResponse> pinMessage(
Message message, {
String messageId, {
Object? /*num|DateTime*/ timeoutOrExpirationDate,
}) {
assert(() {
@@ -1212,17 +1230,23 @@ class StreamChatClient {
Duration(seconds: timeoutOrExpirationDate.toInt()),
);
}
return updateMessage(
message.copyWith(
pinned: true,
pinExpires: pinExpires,
),
return partialUpdateMessage(
messageId,
set: {
'pinned': true,
'pin_expires': pinExpires?.toUtc().toIso8601String(),
},
);
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) =>
updateMessage(message.copyWith(pinned: false));
Future<UpdateMessageResponse> unpinMessage(String messageId) =>
partialUpdateMessage(
messageId,
set: {
'pinned': false,
},
);
/// Closes the [_ws] connection and resets the [state]
/// If [flushChatPersistence] is true the client deletes all offline
@@ -1275,23 +1299,25 @@ class ClientState {
.map((e) => e.me)
.listen((user) {
_userController.add(user);
if (user?.totalUnreadCount != null) {
_totalUnreadCountController.add(user?.totalUnreadCount);
final totalUnreadCount = user?.totalUnreadCount;
if (totalUnreadCount != null) {
_totalUnreadCountController.add(totalUnreadCount);
}
if (user?.unreadChannels != null) {
_unreadChannelsController.add(user?.unreadChannels);
final unreadChannels = user?.unreadChannels;
if (unreadChannels != null) {
_unreadChannelsController.add(unreadChannels);
}
}),
_client
.on()
.where((event) => event.unreadChannels != null)
.map((e) => e.unreadChannels)
.map((event) => event.unreadChannels)
.whereType<int>()
.listen(_unreadChannelsController.add),
_client
.on()
.where((event) => event.totalUnreadCount != null)
.map((e) => e.totalUnreadCount)
.map((event) => event.totalUnreadCount)
.whereType<int>()
.listen(_totalUnreadCountController.add),
]);
@@ -1305,8 +1331,8 @@ class ClientState {
final _subscriptions = <StreamSubscription>[];
/// Used internally for optimistic update of unread count
set totalUnreadCount(int? unreadCount) {
_totalUnreadCountController.add(unreadCount ?? 0);
set totalUnreadCount(int unreadCount) {
_totalUnreadCountController.add(unreadCount);
}
void _listenChannelHidden() {
@@ -1374,16 +1400,16 @@ class ClientState {
Stream<Map<String, User>> get usersStream => _usersController.stream;
/// The current unread channels count
int? get unreadChannels => _unreadChannelsController.valueOrNull;
int get unreadChannels => _unreadChannelsController.value;
/// The current unread channels count as a stream
Stream<int?> get unreadChannelsStream => _unreadChannelsController.stream;
Stream<int> get unreadChannelsStream => _unreadChannelsController.stream;
/// The current total unread messages count
int? get totalUnreadCount => _totalUnreadCountController.valueOrNull;
int get totalUnreadCount => _totalUnreadCountController.value;
/// The current total unread messages count as a stream
Stream<int?> get totalUnreadCountStream => _totalUnreadCountController.stream;
Stream<int> get totalUnreadCountStream => _totalUnreadCountController.stream;
/// The current list of channels in memory as a stream
Stream<Map<String, Channel>> get channelsStream => _channelsController.stream;
@@ -1399,8 +1425,8 @@ class ClientState {
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
final _userController = BehaviorSubject<OwnUser?>();
final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
final _unreadChannelsController = BehaviorSubject<int?>();
final _totalUnreadCountController = BehaviorSubject<int?>();
final _unreadChannelsController = BehaviorSubject<int>.seeded(0);
final _totalUnreadCountController = BehaviorSubject<int>.seeded(0);
/// Call this method to dispose this object
void dispose() {
@@ -109,12 +109,16 @@ class ChannelApi {
/// Updates the [channelId] of type [ChannelType] data with [data]
Future<PartialUpdateChannelResponse> updateChannelPartial(
String channelId,
String channelType,
Map<String, dynamic> data,
) async {
String channelType, {
Map<String, Object?>? set,
List<String>? unset,
}) async {
final response = await _client.patch(
_getChannelUrl(channelId, channelType),
data: data,
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
},
);
return PartialUpdateChannelResponse.fromJson(response.data);
}
@@ -14,11 +14,15 @@ class MessageApi {
Future<SendMessageResponse> sendMessage(
String channelId,
String channelType,
Message message,
) async {
Message message, {
bool skipPush = false,
}) async {
final response = await _client.post(
'/channels/$channelType/$channelId/message',
data: {'message': message},
data: {
'message': message,
'skip_push': skipPush,
},
);
return SendMessageResponse.fromJson(response.data);
}
@@ -56,6 +60,24 @@ class MessageApi {
return UpdateMessageResponse.fromJson(response.data);
}
/// Partially update the given [messageId]
/// Use [set] to define values to be set
/// Use [unset] to define values to be unset
Future<UpdateMessageResponse> partialUpdateMessage(
String messageId, {
Map<String, Object?>? set,
List<String>? unset,
}) async {
final response = await _client.put(
'/messages/$messageId',
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
},
);
return UpdateMessageResponse.fromJson(response.data);
}
/// Deletes the given [messageId]
Future<EmptyResponse> deleteMessage(
String messageId,
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/user.dart';
@@ -6,7 +7,7 @@ part 'member.g.dart';
/// The class that contains the information about the user membership
/// in a channel
@JsonSerializable()
class Member {
class Member extends Equatable {
/// Constructor used for json serialization
Member({
this.user,
@@ -98,4 +99,19 @@ class Member {
/// Serialize to json
Map<String, dynamic> toJson() => _$MemberToJson(this);
@override
List<Object?> get props => [
user,
inviteAcceptedAt,
inviteRejectedAt,
invited,
role,
userId,
isModerator,
banned,
shadowBanned,
createdAt,
updatedAt,
];
}
@@ -73,7 +73,6 @@ class Message extends Equatable {
this.extraData = const {},
this.deletedAt,
this.status = MessageSendingStatus.sent,
this.skipPush = false,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(),
@@ -158,10 +157,6 @@ class Message extends Equatable {
@JsonKey(defaultValue: false)
final bool silent;
/// If true the message will not send a push notification
@JsonKey(defaultValue: false)
final bool skipPush;
/// If true the message is shadowed
@JsonKey(
includeIfNull: false,
@@ -253,7 +248,6 @@ class Message extends Equatable {
'pinned_at',
'pin_expires',
'pinned_by',
'skip_push',
];
/// Serialize to json
@@ -291,7 +285,6 @@ class Message extends Equatable {
User? pinnedBy,
Map<String, Object?>? extraData,
MessageSendingStatus? status,
bool? skipPush,
}) {
assert(() {
if (pinExpires is! DateTime &&
@@ -331,7 +324,6 @@ class Message extends Equatable {
pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires:
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
skipPush: skipPush ?? this.skipPush,
);
}
@@ -398,7 +390,6 @@ class Message extends Equatable {
pinnedBy,
extraData,
status,
skipPush,
];
}
@@ -67,7 +67,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
skipPush: json['skip_push'] as bool? ?? false,
);
}
@@ -97,7 +96,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
writeNotNull('thread_participants', readonly(instance.threadParticipants));
val['show_in_channel'] = instance.showInChannel;
val['silent'] = instance.silent;
val['skip_push'] = instance.skipPush;
writeNotNull('shadowed', readonly(instance.shadowed));
writeNotNull('command', readonly(instance.command));
writeNotNull('created_at', readonly(instance.createdAt));
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
@@ -5,7 +6,7 @@ part 'user.g.dart';
/// The class that defines the user model
@JsonSerializable()
class User {
class User extends Equatable {
/// Constructor used for json serialization
User({
required this.id,
@@ -129,4 +130,17 @@ class User {
banned: banned ?? this.banned,
teams: teams ?? this.teams,
);
@override
List<Object?> get props => [
id,
role,
teams,
createdAt,
updatedAt,
lastActive,
online,
banned,
extraData,
];
}
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '2.0.0-nullsafety.2';
const PACKAGE_VERSION = '2.0.0-nullsafety.5';
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat
homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications.
version: 2.0.0-nullsafety.2
version: 2.0.0-nullsafety.5
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -30,4 +30,4 @@ dev_dependencies:
freezed: ^0.14.1+3
json_serializable: ^4.1.0
mocktail: ^0.1.1
test: ^1.16.8
test: ^1.17.7
@@ -425,6 +425,54 @@ void main() {
});
});
test('`.partialUpdateMessage`', () async {
final message = Message(id: 'test-message-id');
const set = {'text': 'Update Message text'};
const unset = ['pinExpires'];
final updateMessageResponse = UpdateMessageResponse()
..message = message.copyWith(text: set['text'], pinExpires: null);
when(
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
).thenAnswer((_) async => updateMessageResponse);
channel.state?.messagesStream.skip(1).listen(print);
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
updateMessageResponse.message.copyWith(
status: MessageSendingStatus.sent,
),
matchText: true,
matchSendingStatus: true,
),
],
]),
);
final res = await channel.partialUpdateMessage(
message,
set: set,
unset: unset,
);
expect(res, isNotNull);
expect(res.message.id, message.id);
expect(res.message.id, message.id);
expect(res.message.text, set['text']);
expect(res.message.pinExpires, isNull);
verify(
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
).called(1);
});
group('`.deleteMessage`', () {
test('should work fine', () async {
const messageId = 'test-message-id';
@@ -493,21 +541,21 @@ void main() {
() async {
final message = Message(id: 'test-message-id');
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
.thenAnswer((invocation) async => UpdateMessageResponse()
..message = (invocation.positionalArguments.first as Message)
.copyWith(status: MessageSendingStatus.sent));
when(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
pinned: true,
pinExpires: null,
status: MessageSendingStatus.sent,
));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.updating),
matchSendingStatus: true,
),
],
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
@@ -523,8 +571,11 @@ void main() {
expect(res.message.pinned, isTrue);
expect(res.message.pinExpires, isNull);
verify(() => client.updateMessage(any(that: isSameMessageAs(message))))
.called(1);
verify(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).called(1);
});
test(
@@ -533,21 +584,23 @@ void main() {
final message = Message(id: 'test-message-id');
const timeoutOrExpirationDate = 300; // 300 seconds
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
.thenAnswer((invocation) async => UpdateMessageResponse()
..message = (invocation.positionalArguments.first as Message)
.copyWith(status: MessageSendingStatus.sent));
when(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
pinned: true,
pinExpires: DateTime.now().add(
const Duration(seconds: timeoutOrExpirationDate),
),
status: MessageSendingStatus.sent,
));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.updating),
matchSendingStatus: true,
),
],
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
@@ -566,9 +619,11 @@ void main() {
expect(res.message.pinned, isTrue);
expect(res.message.pinExpires, isNotNull);
verify(() =>
client.updateMessage(any(that: isSameMessageAs(message))))
.called(1);
verify(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).called(1);
},
);
@@ -579,21 +634,21 @@ void main() {
final timeoutOrExpirationDate =
DateTime.now().add(const Duration(days: 3)); // 3 days
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
.thenAnswer((invocation) async => UpdateMessageResponse()
..message = (invocation.positionalArguments.first as Message)
.copyWith(status: MessageSendingStatus.sent));
when(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
pinned: true,
pinExpires: timeoutOrExpirationDate,
status: MessageSendingStatus.sent,
));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.updating),
matchSendingStatus: true,
),
],
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
@@ -613,9 +668,11 @@ void main() {
expect(res.message.pinExpires, isNotNull);
expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc());
verify(
() => client.updateMessage(any(that: isSameMessageAs(message))),
).called(1);
verify(() => client.partialUpdateMessage(
message.id,
set: any(named: 'set'),
unset: any(named: 'unset'),
)).called(1);
},
);
@@ -640,21 +697,19 @@ void main() {
test('`.unpinMessage`', () async {
final message = Message(id: 'test-message-id', pinned: true);
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
.thenAnswer((invocation) async => UpdateMessageResponse()
..message = (invocation.positionalArguments.first as Message)
.copyWith(status: MessageSendingStatus.sent));
when(() => client.partialUpdateMessage(
message.id,
set: {'pinned': false},
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
pinned: false,
status: MessageSendingStatus.sent,
));
expectLater(
// skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1),
emitsInOrder([
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.updating),
matchSendingStatus: true,
),
],
[
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
@@ -669,44 +724,12 @@ void main() {
expect(res, isNotNull);
expect(res.message.pinned, isFalse);
verify(
() => client.updateMessage(any(that: isSameMessageAs(message))),
).called(1);
verify(() => client.partialUpdateMessage(
message.id,
set: {'pinned': false},
)).called(1);
});
//
// /// Send a file to this channel
// Future<SendFileResponse> sendFile(
// AttachmentFile file, {
// ProgressCallback? onSendProgress,
// CancelToken? cancelToken,
// }) {
// _checkInitialized();
// return _client.sendFile(
// file,
// id!,
// type,
// onSendProgress: onSendProgress,
// cancelToken: cancelToken,
// );
// }
//
// /// Send an image to this channel
// Future<SendImageResponse> sendImage(
// AttachmentFile file, {
// ProgressCallback? onSendProgress,
// CancelToken? cancelToken,
// }) {
// _checkInitialized();
// return _client.sendImage(
// file,
// id!,
// type,
// onSendProgress: onSendProgress,
// cancelToken: cancelToken,
// );
// }
group('`.search`', () {
final filter = Filter.in_('cid', const [channelCid]);
@@ -1123,40 +1146,44 @@ void main() {
});
test('`.updatePartial`', () async {
const channelData = {
const set = {
'name': 'Stream Team',
'profile_image': 'test-profile-image',
};
const unset = ['tag', 'last_name'];
final channelModel = ChannelModel(
cid: channelCid,
extraData: {
'coolness': 999,
...channelData,
...set,
},
);
when(() => client.updateChannelPartial(
channelId,
channelType,
channelData,
set: set,
unset: unset,
)).thenAnswer(
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
);
final res = await channel.updatePartial(channelData);
final res = await channel.updatePartial(set: set, unset: unset);
expect(res, isNotNull);
expect(res.channel.cid, channelModel.cid);
expect(
res.channel.extraData,
{'coolness': 999, ...channelData},
{'coolness': 999, ...set},
);
verify(() => client.updateChannelPartial(
channelId,
channelType,
channelData,
set: set,
unset: unset,
)).called(1);
});
@@ -1380,12 +1407,6 @@ void main() {
when(() => client.markChannelRead(channelId, channelType,
messageId: messageId)).thenAnswer((_) async => EmptyResponse());
expectLater(
// skipping first seed unread count -> 0 unread count
channel.state?.unreadCountStream.skip(1),
emitsInOrder([0]),
);
final res = await channel.markRead(messageId: messageId);
expect(res, isNotNull);
@@ -190,30 +190,39 @@ void main() {
test('updateChannelPartial', () async {
const channelId = 'test-channel-id';
const channelType = 'test-channel-type';
const data = {'name': 'test-channel-name'};
const set = {
'name': 'Stream Team',
'profile_image': 'test-profile-image',
};
const unset = ['tag', 'last_name'];
final path = _getChannelUrl(channelId, channelType);
final channelModel = ChannelModel(
id: channelId,
type: channelType,
extraData: data,
extraData: set,
);
when(() => client.patch(path, data: any(named: 'data')))
.thenAnswer((_) async => successResponse(path, data: {
'channel': channelModel.toJson(),
}));
when(
() => client.patch(path, data: {'set': set, 'unset': unset}),
).thenAnswer((_) async => successResponse(path, data: {
'channel': channelModel.toJson(),
}));
final res = await channelApi.updateChannelPartial(
channelId,
channelType,
data,
set: set,
unset: unset,
);
expect(res, isNotNull);
verify(() => client.patch(path, data: any(named: 'data'))).called(1);
verify(
() => client.patch(path, data: {'set': set, 'unset': unset}),
).called(1);
verifyNoMoreInteractions(client);
});
@@ -110,6 +110,40 @@ void main() {
verifyNoMoreInteractions(client);
});
test('partialUpdateMessage', () async {
const messageId = 'test-message-id';
const set = {'text': 'Update Message text'};
const unset = ['pinExpires'];
const path = '/messages/$messageId';
final message = Message(id: 'test-message-id', text: set['text']);
when(() => client.put(
path,
data: {'set': set, 'unset': unset},
)).thenAnswer(
(_) async => successResponse(path, data: {'message': message.toJson()}),
);
final res = await messageApi.partialUpdateMessage(
messageId,
set: set,
unset: unset,
);
expect(res, isNotNull);
expect(res.message.id, message.id);
expect(res.message.text, set['text']);
expect(res.message.pinExpires, isNull);
verify(() => client.put(
path,
data: {'set': set, 'unset': unset},
)).called(1);
verifyNoMoreInteractions(client);
});
test('deleteMessage', () async {
const messageId = 'test-message-id';
@@ -902,7 +902,6 @@ void main() {
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"skip_push": false,
"silent": false,
"pinned": false,
"pinned_at": null,
@@ -919,7 +918,6 @@ void main() {
"show_in_channel": null,
"mentioned_users": [],
"status": "SENT",
"skip_push": false,
"silent": false,
"pinned": false,
"pinned_at": null,
@@ -929,7 +927,6 @@ void main() {
{
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
"text": "test message",
"skip_push": false,
"attachments": [],
"parent_id": null,
"quoted_message": null,
@@ -952,7 +949,6 @@ void main() {
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"skip_push": false,
"status": "SENT",
"silent": false,
"pinned": false,
@@ -964,7 +960,6 @@ void main() {
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
"text": "test",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -982,7 +977,6 @@ void main() {
"text": "hi",
"attachments": [],
"parent_id": null,
"skip_push": false,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
@@ -1000,7 +994,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1023,7 +1016,6 @@ void main() {
"status": "SENT",
"silent": false,
"pinned": false,
"skip_push": false,
"pinned_at": null,
"pin_expires": null,
"pinned_by": null
@@ -1041,7 +1033,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1053,7 +1044,6 @@ void main() {
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"skip_push": false,
"mentioned_users": [],
"status": "SENT",
"silent": false,
@@ -1071,7 +1061,6 @@ void main() {
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
"skip_push": false,
"status": "SENT",
"silent": false,
"pinned": false,
@@ -1090,7 +1079,6 @@ void main() {
"mentioned_users": [],
"status": "SENT",
"silent": false,
"skip_push": false,
"pinned": false,
"pinned_at": null,
"pin_expires": null,
@@ -1100,7 +1088,6 @@ void main() {
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
"text": "what are you doing?",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1118,7 +1105,6 @@ void main() {
"text": "👍",
"attachments": [],
"parent_id": null,
"skip_push": false,
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
@@ -1134,7 +1120,6 @@ void main() {
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
"text": "sdasas",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1155,7 +1140,6 @@ void main() {
"quoted_message": null,
"quoted_message_id": null,
"show_in_channel": null,
"skip_push": false,
"mentioned_users": [],
"status": "SENT",
"silent": false,
@@ -1168,7 +1152,6 @@ void main() {
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
"text": "nhisagdhsadz",
"attachments": [],
"skip_push": false,
"parent_id": null,
"quoted_message": null,
"quoted_message_id": null,
@@ -1187,7 +1170,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1204,7 +1186,6 @@ void main() {
"attachments": [],
"parent_id": null,
"quoted_message": null,
"skip_push": false,
"quoted_message_id": null,
"show_in_channel": null,
"mentioned_users": [],
@@ -1212,7 +1193,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1229,7 +1209,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1246,7 +1225,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1263,7 +1241,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1280,7 +1257,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1297,7 +1273,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
},
@@ -1314,7 +1289,6 @@ void main() {
"silent": false,
"pinned": false,
"pinned_at": null,
"skip_push": false,
"pin_expires": null,
"pinned_by": null
}
@@ -133,7 +133,6 @@ void main() {
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
"silent": false,
"skip_push": false,
"attachments": [
{
"type": "video",
+1 -8
View File
@@ -87,15 +87,8 @@ class FakeClientState extends Fake implements ClientState {
@override
OwnUser? get user => OwnUser(id: 'test-user-id');
var _totalUnreadCount = 0;
@override
int? get totalUnreadCount => _totalUnreadCount;
@override
set totalUnreadCount(int? unreadCount) {
_totalUnreadCount += unreadCount ?? 0;
}
int totalUnreadCount = 0;
}
class FakeMessage extends Fake implements Message {}
@@ -40,11 +40,13 @@ class _IsSameEventAs extends Matcher {
Matcher isSameMessageAs(
Message targetMessage, {
bool matchText = false,
bool matchReactions = false,
bool matchSendingStatus = false,
}) =>
_IsSameMessageAs(
targetMessage: targetMessage,
matchText: matchText,
matchReactions: matchReactions,
matchSendingStatus: matchSendingStatus,
);
@@ -52,11 +54,13 @@ Matcher isSameMessageAs(
class _IsSameMessageAs extends Matcher {
const _IsSameMessageAs({
required this.targetMessage,
this.matchText = false,
this.matchReactions = false,
this.matchSendingStatus = false,
});
final Message targetMessage;
final bool matchText;
final bool matchReactions;
final bool matchSendingStatus;
@@ -67,6 +71,9 @@ class _IsSameMessageAs extends Matcher {
@override
bool matches(covariant Message message, Map matchState) {
var matches = message.id == targetMessage.id;
if (matchText) {
matches &= message.text == targetMessage.text;
}
if (matchSendingStatus) {
matches &= message.status == targetMessage.status;
}