Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 7 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Upcoming

🐞 Fixed

- Fixed `Channel.sendMessage` to prevent sending empty messages when all attachments are cancelled
during upload.

## 9.16.0

🐞 Fixed
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
244 changes: 244 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,249 @@ 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>()),
);
Comment thread
xsahil03x marked this conversation as resolved.

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>()),
);

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.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.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.sendMessage(
any(that: isSameMessageAs(message)),
channelId,
channelType,
),
);
},
);
});

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