Merge branch 'message-input-controller' into feat/capabilities

This commit is contained in:
Salvatore Giordano
2022-01-05 10:26:57 +01:00
28 changed files with 2321 additions and 855 deletions
@@ -1,18 +1,13 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
/// The retry queue associated to a channel.
class RetryQueue {
/// Instantiate a new RetryQueue object
/// Instantiate a new RetryQueue object.
RetryQueue({
required this.channel,
this.logger,
@@ -22,13 +17,13 @@ class RetryQueue {
_listenFailedEvents();
}
/// The channel of this queue
/// The channel of this queue.
final Channel channel;
/// The client associated with this [channel]
/// The client associated with this [channel].
final StreamChatClient client;
/// The logger associated to this queue
/// The logger associated to this queue.
final Logger? logger;
late final RetryPolicy _retryPolicy;
@@ -68,7 +63,7 @@ class RetryQueue {
}).addTo(_compositeSubscription);
}
/// Add a list of messages
/// Add a list of messages.
void add(List<Message> messages) {
if (messages.isEmpty) return;
if (!_messageQueue.containsAllMessage(messages)) {
@@ -118,6 +113,7 @@ class RetryQueue {
} catch (e) {
if (e is! StreamChatNetworkError || !e.isRetriable) {
_messageQueue.removeMessage(message);
_sendFailedEvent(message);
return true;
}
// retry logic
@@ -179,10 +175,10 @@ class RetryQueue {
}
}
/// Whether our [_messageQueue] has messages or not
/// Whether our [_messageQueue] has messages or not.
bool get hasMessages => _messageQueue.isNotEmpty;
/// Call this method to dispose this object
/// Call this method to dispose this object.
void dispose() {
_messageQueue.clear();
_compositeSubscription.dispose();
@@ -8,13 +8,13 @@ import 'package:uuid/uuid.dart';
part 'message.g.dart';
class _PinExpires {
const _PinExpires();
class _NullConst {
const _NullConst();
}
const _pinExpires = _PinExpires();
const _nullConst = _NullConst();
/// Enum defining the status of a sending message
/// Enum defining the status of a sending message.
enum MessageSendingStatus {
/// Message is being sent
sending,
@@ -40,10 +40,10 @@ enum MessageSendingStatus {
sent,
}
/// The class that contains the information about a message
/// The class that contains the information about a message.
@JsonSerializable()
class Message extends Equatable {
/// Constructor used for json serialization
/// Constructor used for json serialization.
Message({
String? id,
this.text,
@@ -58,44 +58,47 @@ class Message extends Equatable {
this.ownReactions,
this.parentId,
this.quotedMessage,
this.quotedMessageId,
String? quotedMessageId,
this.replyCount = 0,
this.threadParticipants,
this.showInChannel,
this.command,
DateTime? createdAt,
DateTime? updatedAt,
this.deletedAt,
this.user,
this.pinned = false,
this.pinnedAt,
DateTime? pinExpires,
this.pinnedBy,
this.extraData = const {},
this.deletedAt,
this.status = MessageSendingStatus.sent,
this.status = MessageSendingStatus.sending,
this.i18n,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
_createdAt = createdAt,
_updatedAt = updatedAt,
_quotedMessageId = quotedMessageId;
/// Create a new instance from a json
/// Create a new instance from JSON.
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
).copyWith(
status: MessageSendingStatus.sent,
);
/// The message ID. This is either created by Stream or set client side when
/// the message is added.
final String id;
/// The text of this message
/// The text of this message.
final String? text;
/// The status of a sending message
/// The status of a sending message.
@JsonKey(ignore: true)
final MessageSendingStatus status;
/// The message type
/// The message type.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
@@ -107,15 +110,15 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false)
final List<Attachment> attachments;
/// The list of user mentioned in the message
/// The list of user mentioned in the message.
@JsonKey(toJson: User.toIds)
final List<User> mentionedUsers;
/// A map describing the count of number of every reaction
/// A map describing the count of number of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionCounts;
/// A map describing the count of score of every reaction
/// A map describing the count of score of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionScores;
@@ -130,12 +133,14 @@ class Message extends Equatable {
/// The ID of the parent message, if the message is a thread reply.
final String? parentId;
/// A quoted reply message
/// A quoted reply message.
@JsonKey(toJson: Serializer.readOnly)
final Message? quotedMessage;
final String? _quotedMessageId;
/// The ID of the quoted message, if the message is a quoted reply.
final String? quotedMessageId;
String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id;
/// Reserved field indicating the number of replies for this message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@@ -148,10 +153,10 @@ class Message extends Equatable {
/// Check if this message needs to show in the channel.
final bool? showInChannel;
/// If true the message is silent
/// If true the message is silent.
final bool silent;
/// If true the message is shadowed
/// If true the message is shadowed.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
@@ -162,56 +167,61 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final String? command;
/// Reserved field indicating when the message was created.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime createdAt;
/// Reserved field indicating when the message was updated last time.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime updatedAt;
/// User who sent the message
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final User? user;
/// If true the message is pinned
final bool pinned;
/// Reserved field indicating when the message was pinned
@JsonKey(toJson: Serializer.readOnly)
final DateTime? pinnedAt;
/// Reserved field indicating when the message will expire
///
/// if `null` message has no expiry
final DateTime? pinExpires;
/// Reserved field indicating who pinned the message
@JsonKey(toJson: Serializer.readOnly)
final User? pinnedBy;
/// Message custom extraData
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// True if the message is a system info
bool get isSystem => type == 'system';
/// True if the message has been deleted
bool get isDeleted => type == 'deleted';
/// True if the message is ephemeral
bool get isEphemeral => type == 'ephemeral';
final DateTime? _createdAt;
/// Reserved field indicating when the message was deleted.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime? deletedAt;
/// Reserved field indicating when the message was created.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
DateTime get createdAt => _createdAt ?? DateTime.now();
final DateTime? _updatedAt;
/// Reserved field indicating when the message was updated last time.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
DateTime get updatedAt => _updatedAt ?? DateTime.now();
/// User who sent the message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final User? user;
/// If true the message is pinned.
final bool pinned;
/// Reserved field indicating when the message was pinned.
@JsonKey(toJson: Serializer.readOnly)
final DateTime? pinnedAt;
/// Reserved field indicating when the message will expire.
///
/// If `null` message has no expiry.
final DateTime? pinExpires;
/// Reserved field indicating who pinned the message.
@JsonKey(toJson: Serializer.readOnly)
final User? pinnedBy;
/// Message custom extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// True if the message is a system info.
bool get isSystem => type == 'system';
/// True if the message has been deleted.
bool get isDeleted => type == 'deleted';
/// True if the message is ephemeral.
bool get isEphemeral => type == 'ephemeral';
/// A Map of translations.
@JsonKey(includeIfNull: false)
final Map<String, String>? i18n;
/// Known top level fields.
///
/// Useful for [Serializer] methods.
static const topLevelFields = [
'id',
@@ -244,7 +254,7 @@ class Message extends Equatable {
'i18n',
];
/// Serialize to json
/// Serialize to json.
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
_$MessageToJson(this),
);
@@ -256,18 +266,18 @@ class Message extends Equatable {
String? type,
List<Attachment>? attachments,
List<User>? mentionedUsers,
bool? silent,
bool? shadowed,
Map<String, int>? reactionCounts,
Map<String, int>? reactionScores,
List<Reaction>? latestReactions,
List<Reaction>? ownReactions,
String? parentId,
Message? quotedMessage,
String? quotedMessageId,
Object? quotedMessage = _nullConst,
Object? quotedMessageId = _nullConst,
int? replyCount,
List<User>? threadParticipants,
bool? showInChannel,
bool? shadowed,
bool? silent,
String? command,
DateTime? createdAt,
DateTime? updatedAt,
@@ -275,7 +285,7 @@ class Message extends Equatable {
User? user,
bool? pinned,
DateTime? pinnedAt,
Object? pinExpires = _pinExpires,
Object? pinExpires = _nullConst,
User? pinnedBy,
Map<String, Object?>? extraData,
MessageSendingStatus? status,
@@ -284,41 +294,68 @@ class Message extends Equatable {
assert(() {
if (pinExpires is! DateTime &&
pinExpires != null &&
pinExpires is! _PinExpires) {
pinExpires is! _NullConst) {
throw ArgumentError('`pinExpires` can only be set as DateTime or null');
}
return true;
}(), 'Validate type for pinExpires');
assert(() {
if (quotedMessage is! Message &&
quotedMessage != null &&
quotedMessage is! _NullConst) {
throw ArgumentError(
'`quotedMessage` can only be set as Message or null',
);
}
return true;
}(), 'Validate type for quotedMessage');
assert(() {
if (quotedMessageId is! String &&
quotedMessageId != null &&
quotedMessageId is! _NullConst) {
throw ArgumentError(
'`quotedMessage` can only be set as String or null',
);
}
return true;
}(), 'Validate type for quotedMessage');
return Message(
id: id ?? this.id,
text: text ?? this.text,
type: type ?? this.type,
attachments: attachments ?? this.attachments,
mentionedUsers: mentionedUsers ?? this.mentionedUsers,
silent: silent ?? this.silent,
shadowed: shadowed ?? this.shadowed,
reactionCounts: reactionCounts ?? this.reactionCounts,
reactionScores: reactionScores ?? this.reactionScores,
latestReactions: latestReactions ?? this.latestReactions,
ownReactions: ownReactions ?? this.ownReactions,
parentId: parentId ?? this.parentId,
quotedMessage: quotedMessage ?? this.quotedMessage,
quotedMessageId: quotedMessageId ?? this.quotedMessageId,
quotedMessage: quotedMessage == _nullConst
? this.quotedMessage
: quotedMessage as Message?,
quotedMessageId: quotedMessageId == _nullConst
? _quotedMessageId
: quotedMessageId as String?,
replyCount: replyCount ?? this.replyCount,
threadParticipants: threadParticipants ?? this.threadParticipants,
showInChannel: showInChannel ?? this.showInChannel,
command: command ?? this.command,
createdAt: createdAt ?? this.createdAt,
silent: silent ?? this.silent,
extraData: extraData ?? this.extraData,
user: user ?? this.user,
shadowed: shadowed ?? this.shadowed,
updatedAt: updatedAt ?? this.updatedAt,
createdAt: createdAt ?? _createdAt,
updatedAt: updatedAt ?? _updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
status: status ?? this.status,
user: user ?? this.user,
pinned: pinned ?? this.pinned,
pinnedAt: pinnedAt ?? this.pinnedAt,
pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires:
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?,
pinnedBy: pinnedBy ?? this.pinnedBy,
extraData: extraData ?? this.extraData,
status: status ?? this.status,
i18n: i18n ?? this.i18n,
);
}
@@ -331,6 +368,8 @@ class Message extends Equatable {
type: other.type,
attachments: other.attachments,
mentionedUsers: other.mentionedUsers,
silent: other.silent,
shadowed: other.shadowed,
reactionCounts: other.reactionCounts,
reactionScores: other.reactionScores,
latestReactions: other.latestReactions,
@@ -343,17 +382,15 @@ class Message extends Equatable {
showInChannel: other.showInChannel,
command: other.command,
createdAt: other.createdAt,
silent: other.silent,
extraData: other.extraData,
user: other.user,
shadowed: other.shadowed,
updatedAt: other.updatedAt,
deletedAt: other.deletedAt,
status: other.status,
user: other.user,
pinned: other.pinned,
pinnedAt: other.pinnedAt,
pinExpires: other.pinExpires,
pinnedBy: other.pinnedBy,
extraData: other.extraData,
status: other.status,
i18n: other.i18n,
);
@@ -377,8 +414,8 @@ class Message extends Equatable {
shadowed,
silent,
command,
createdAt,
updatedAt,
_createdAt,
_updatedAt,
deletedAt,
user,
pinned,
@@ -7,6 +7,7 @@ export 'package:dio/src/options.dart';
export 'package:dio/src/options.dart' show ProgressCallback;
export 'package:logging/logging.dart' show Logger, Level;
export 'package:rate_limiter/rate_limiter.dart';
export 'package:uuid/uuid.dart';
export './src/core/api/attachment_file_uploader.dart'
show AttachmentFileUploader;
@@ -244,9 +244,13 @@ void main() {
group('`.sendMessage`', () {
test('should work fine', () async {
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
user: client.state.currentUser,
);
final sendMessageResponse = SendMessageResponse()..message = message;
final sendMessageResponse = SendMessageResponse()
..message = message.copyWith(status: MessageSendingStatus.sent);
when(() => client.sendMessage(
any(that: isSameMessageAs(message)),
@@ -329,6 +333,7 @@ void main() {
.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
.toList(growable: false),
status: MessageSendingStatus.sent,
));
expectLater(
@@ -455,7 +460,10 @@ void main() {
group('`.updateMessage`', () {
test('should work fine', () async {
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final updateMessageResponse = UpdateMessageResponse()
..message = message;
@@ -530,6 +538,7 @@ void main() {
any(that: isSameMessageAs(message)),
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
status: MessageSendingStatus.sent,
attachments: attachments
.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
@@ -678,7 +687,7 @@ void main() {
[
isSameMessageAs(
updateMessageResponse.message.copyWith(
status: MessageSendingStatus.sent,
status: MessageSendingStatus.sending,
),
matchText: true,
matchSendingStatus: true,
@@ -707,7 +716,10 @@ void main() {
group('`.deleteMessage`', () {
test('should work fine', () async {
const messageId = 'test-message-id';
final message = Message(id: messageId);
final message = Message(
id: messageId,
status: MessageSendingStatus.sent,
);
when(() => client.deleteMessage(messageId))
.thenAnswer((_) async => EmptyResponse());
@@ -744,7 +756,6 @@ void main() {
const messageId = 'test-message-id';
final message = Message(
id: messageId,
status: MessageSendingStatus.sending,
);
expectLater(
@@ -1077,7 +1088,10 @@ void main() {
group('`.sendReaction`', () {
test('should work fine', () async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1120,7 +1134,10 @@ void main() {
'should restore previous message if `client.sendReaction` throws',
() async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1181,6 +1198,7 @@ void main() {
latestReactions: [prevReaction],
reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1},
status: MessageSendingStatus.sent,
);
const type = 'test-reaction-type-2';
@@ -1212,7 +1230,7 @@ void main() {
emitsInOrder([
[
isSameMessageAs(
newMessage.copyWith(status: MessageSendingStatus.sent),
newMessage,
matchReactions: true,
matchSendingStatus: true,
),
@@ -1255,6 +1273,7 @@ void main() {
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))
@@ -1302,6 +1321,7 @@ void main() {
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))