Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

🐞 Fixed

- Fixed `Channel.sendMessage` to prevent sending empty messages when all attachments are cancelled
during upload.
- Fixed `toDraftMessage` to only include successfully uploaded attachments in draft messages.

## 9.16.0
Expand Down
18 changes: 18 additions & 0 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,15 @@ class Channel {
});
}

bool _isMessageValidForUpload(Message message) {
final hasText = message.text?.trim().isNotEmpty == true;
final hasAttachments = message.attachments.isNotEmpty;
final hasQuotedMessage = message.quotedMessageId != null;
final hasPoll = message.pollId != null;

return hasText || hasAttachments || hasQuotedMessage || hasPoll;
}

final _sendMessageLock = Lock();

/// Send a [message] to this channel.
Expand Down Expand Up @@ -716,6 +725,15 @@ class Channel {
message = await attachmentsUploadCompleter.future;
}

// Validate the final message before sending it to the server.
if (_isMessageValidForUpload(message) == false) {
client.logger.warning('Message is not valid for sending, removing it');

// Remove the message from state as it is invalid.
state!.deleteMessage(message, hardDelete: true);
throw const StreamChatError('Message is not valid for sending');
}

// Wait for the previous sendMessage call to finish. Otherwise, the order
// of messages will not be maintained.
final response = await _sendMessageLock.synchronized(
Expand Down
288 changes: 288 additions & 0 deletions packages/stream_chat/test/src/client/channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ void main() {
test('should work fine', () async {
final message = Message(
id: 'test-message-id',
text: 'Hello world!',
user: client.state.currentUser,
);

Expand Down Expand Up @@ -459,6 +460,293 @@ void main() {
channelType,
)).called(1);
});

test('should not send if the message is invalid', () async {
final message = Message(id: 'test-message-id');

expect(
() => channel.sendMessage(message),
throwsA(isA<StreamChatError>()),
);

verifyNever(
() => client.sendMessage(any(), channelId, channelType),
);
});

test(
'should not send empty message when all attachments are cancelled',
() async {
final attachment = Attachment(
id: 'test-attachment-id',
type: 'image',
file: AttachmentFile(size: 100, path: 'test-file-path'),
);

final message = Message(
id: 'test-message-id',
attachments: [attachment],
);

when(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
).thenAnswer(
(_) async => throw StreamChatNetworkError.raw(
code: 0,
message: 'Request cancelled',
isRequestCancelledError: true,
),
);

expect(
() => channel.sendMessage(message),
throwsA(isA<StreamChatError>()),
);

verify(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
);

verifyNever(
() => client.sendMessage(any(), channelId, channelType),
);
},
);

test(
'should send message when attachment is cancelled but text exists',
() async {
final attachment = Attachment(
id: 'test-attachment-id',
type: 'image',
file: AttachmentFile(size: 100, path: 'test-file-path'),
);

final message = Message(
id: 'test-message-id',
text: 'Hello world!',
attachments: [attachment],
);

when(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
).thenAnswer(
(_) async => throw StreamChatNetworkError.raw(
code: 0,
message: 'Request cancelled',
isRequestCancelledError: true,
),
);

when(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
).thenAnswer(
(_) async => SendMessageResponse()
..message = message.copyWith(
attachments: [],
state: MessageState.sent,
),
);

final res = await channel.sendMessage(message);

expect(res, isNotNull);
expect(res.message.text, 'Hello world!');

verify(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
);

verify(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
);
},
);

test(
'should send message when attachment is cancelled but quoted message exists',
() async {
final attachment = Attachment(
id: 'test-attachment-id',
type: 'image',
file: AttachmentFile(size: 100, path: 'test-file-path'),
);

final quotedMessage = Message(
id: 'quoted-123',
text: 'Original message',
);

final message = Message(
id: 'test-message-id',
attachments: [attachment],
quotedMessageId: quotedMessage.id,
);

when(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
).thenAnswer(
(_) async => throw StreamChatNetworkError.raw(
code: 0,
message: 'Request cancelled',
isRequestCancelledError: true,
),
);

when(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
).thenAnswer(
(_) async => SendMessageResponse()
..message = message.copyWith(
attachments: [],
state: MessageState.sent,
),
);

final res = await channel.sendMessage(message);

expect(res, isNotNull);
expect(res.message.quotedMessageId, quotedMessage.id);

verify(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
);

verify(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
);
},
);

test(
'should send message when attachment is cancelled but poll exists',
() async {
final attachment = Attachment(
id: 'test-attachment-id',
type: 'image',
file: AttachmentFile(size: 100, path: 'test-file-path'),
);

final message = Message(
id: 'test-message-id',
attachments: [attachment],
pollId: 'poll-123',
);

when(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
).thenAnswer(
(_) async => throw StreamChatNetworkError.raw(
code: 0,
message: 'Request cancelled',
isRequestCancelledError: true,
),
);

when(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
).thenAnswer(
(_) async => SendMessageResponse()
..message = message.copyWith(
attachments: [],
state: MessageState.sent,
),
);

final res = await channel.sendMessage(message);

expect(res, isNotNull);
expect(res.message.pollId, 'poll-123');

verify(
() => client.sendImage(
any(),
channelId,
channelType,
onSendProgress: any(named: 'onSendProgress'),
cancelToken: any(named: 'cancelToken'),
extraData: any(named: 'extraData'),
),
);

verify(
() => client.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
);
},
);
});

group('`.createDraft`', () {
Expand Down