Skip to content

Commit 70c0f7e

Browse files
authored
fix(llc): prevent sending empty messages (#2389)
1 parent e83a790 commit 70c0f7e

File tree

3 files changed

+308
-0
lines changed

3 files changed

+308
-0
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
🐞 Fixed
44

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

79
## 9.16.0

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,15 @@ class Channel {
658658
});
659659
}
660660

661+
bool _isMessageValidForUpload(Message message) {
662+
final hasText = message.text?.trim().isNotEmpty == true;
663+
final hasAttachments = message.attachments.isNotEmpty;
664+
final hasQuotedMessage = message.quotedMessageId != null;
665+
final hasPoll = message.pollId != null;
666+
667+
return hasText || hasAttachments || hasQuotedMessage || hasPoll;
668+
}
669+
661670
final _sendMessageLock = Lock();
662671

663672
/// Send a [message] to this channel.
@@ -716,6 +725,15 @@ class Channel {
716725
message = await attachmentsUploadCompleter.future;
717726
}
718727

728+
// Validate the final message before sending it to the server.
729+
if (_isMessageValidForUpload(message) == false) {
730+
client.logger.warning('Message is not valid for sending, removing it');
731+
732+
// Remove the message from state as it is invalid.
733+
state!.deleteMessage(message, hardDelete: true);
734+
throw const StreamChatError('Message is not valid for sending');
735+
}
736+
719737
// Wait for the previous sendMessage call to finish. Otherwise, the order
720738
// of messages will not be maintained.
721739
final response = await _sendMessageLock.synchronized(

packages/stream_chat/test/src/client/channel_test.dart

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ void main() {
245245
test('should work fine', () async {
246246
final message = Message(
247247
id: 'test-message-id',
248+
text: 'Hello world!',
248249
user: client.state.currentUser,
249250
);
250251

@@ -459,6 +460,293 @@ void main() {
459460
channelType,
460461
)).called(1);
461462
});
463+
464+
test('should not send if the message is invalid', () async {
465+
final message = Message(id: 'test-message-id');
466+
467+
expect(
468+
() => channel.sendMessage(message),
469+
throwsA(isA<StreamChatError>()),
470+
);
471+
472+
verifyNever(
473+
() => client.sendMessage(any(), channelId, channelType),
474+
);
475+
});
476+
477+
test(
478+
'should not send empty message when all attachments are cancelled',
479+
() async {
480+
final attachment = Attachment(
481+
id: 'test-attachment-id',
482+
type: 'image',
483+
file: AttachmentFile(size: 100, path: 'test-file-path'),
484+
);
485+
486+
final message = Message(
487+
id: 'test-message-id',
488+
attachments: [attachment],
489+
);
490+
491+
when(
492+
() => client.sendImage(
493+
any(),
494+
channelId,
495+
channelType,
496+
onSendProgress: any(named: 'onSendProgress'),
497+
cancelToken: any(named: 'cancelToken'),
498+
extraData: any(named: 'extraData'),
499+
),
500+
).thenAnswer(
501+
(_) async => throw StreamChatNetworkError.raw(
502+
code: 0,
503+
message: 'Request cancelled',
504+
isRequestCancelledError: true,
505+
),
506+
);
507+
508+
expect(
509+
() => channel.sendMessage(message),
510+
throwsA(isA<StreamChatError>()),
511+
);
512+
513+
verify(
514+
() => client.sendImage(
515+
any(),
516+
channelId,
517+
channelType,
518+
onSendProgress: any(named: 'onSendProgress'),
519+
cancelToken: any(named: 'cancelToken'),
520+
extraData: any(named: 'extraData'),
521+
),
522+
);
523+
524+
verifyNever(
525+
() => client.sendMessage(any(), channelId, channelType),
526+
);
527+
},
528+
);
529+
530+
test(
531+
'should send message when attachment is cancelled but text exists',
532+
() async {
533+
final attachment = Attachment(
534+
id: 'test-attachment-id',
535+
type: 'image',
536+
file: AttachmentFile(size: 100, path: 'test-file-path'),
537+
);
538+
539+
final message = Message(
540+
id: 'test-message-id',
541+
text: 'Hello world!',
542+
attachments: [attachment],
543+
);
544+
545+
when(
546+
() => client.sendImage(
547+
any(),
548+
channelId,
549+
channelType,
550+
onSendProgress: any(named: 'onSendProgress'),
551+
cancelToken: any(named: 'cancelToken'),
552+
extraData: any(named: 'extraData'),
553+
),
554+
).thenAnswer(
555+
(_) async => throw StreamChatNetworkError.raw(
556+
code: 0,
557+
message: 'Request cancelled',
558+
isRequestCancelledError: true,
559+
),
560+
);
561+
562+
when(
563+
() => client.sendMessage(
564+
any(that: isSameMessageAs(message)),
565+
channelId,
566+
channelType,
567+
),
568+
).thenAnswer(
569+
(_) async => SendMessageResponse()
570+
..message = message.copyWith(
571+
attachments: [],
572+
state: MessageState.sent,
573+
),
574+
);
575+
576+
final res = await channel.sendMessage(message);
577+
578+
expect(res, isNotNull);
579+
expect(res.message.text, 'Hello world!');
580+
581+
verify(
582+
() => client.sendImage(
583+
any(),
584+
channelId,
585+
channelType,
586+
onSendProgress: any(named: 'onSendProgress'),
587+
cancelToken: any(named: 'cancelToken'),
588+
extraData: any(named: 'extraData'),
589+
),
590+
);
591+
592+
verify(
593+
() => client.sendMessage(
594+
any(that: isSameMessageAs(message)),
595+
channelId,
596+
channelType,
597+
),
598+
);
599+
},
600+
);
601+
602+
test(
603+
'should send message when attachment is cancelled but quoted message exists',
604+
() async {
605+
final attachment = Attachment(
606+
id: 'test-attachment-id',
607+
type: 'image',
608+
file: AttachmentFile(size: 100, path: 'test-file-path'),
609+
);
610+
611+
final quotedMessage = Message(
612+
id: 'quoted-123',
613+
text: 'Original message',
614+
);
615+
616+
final message = Message(
617+
id: 'test-message-id',
618+
attachments: [attachment],
619+
quotedMessageId: quotedMessage.id,
620+
);
621+
622+
when(
623+
() => client.sendImage(
624+
any(),
625+
channelId,
626+
channelType,
627+
onSendProgress: any(named: 'onSendProgress'),
628+
cancelToken: any(named: 'cancelToken'),
629+
extraData: any(named: 'extraData'),
630+
),
631+
).thenAnswer(
632+
(_) async => throw StreamChatNetworkError.raw(
633+
code: 0,
634+
message: 'Request cancelled',
635+
isRequestCancelledError: true,
636+
),
637+
);
638+
639+
when(
640+
() => client.sendMessage(
641+
any(that: isSameMessageAs(message)),
642+
channelId,
643+
channelType,
644+
),
645+
).thenAnswer(
646+
(_) async => SendMessageResponse()
647+
..message = message.copyWith(
648+
attachments: [],
649+
state: MessageState.sent,
650+
),
651+
);
652+
653+
final res = await channel.sendMessage(message);
654+
655+
expect(res, isNotNull);
656+
expect(res.message.quotedMessageId, quotedMessage.id);
657+
658+
verify(
659+
() => client.sendImage(
660+
any(),
661+
channelId,
662+
channelType,
663+
onSendProgress: any(named: 'onSendProgress'),
664+
cancelToken: any(named: 'cancelToken'),
665+
extraData: any(named: 'extraData'),
666+
),
667+
);
668+
669+
verify(
670+
() => client.sendMessage(
671+
any(that: isSameMessageAs(message)),
672+
channelId,
673+
channelType,
674+
),
675+
);
676+
},
677+
);
678+
679+
test(
680+
'should send message when attachment is cancelled but poll exists',
681+
() async {
682+
final attachment = Attachment(
683+
id: 'test-attachment-id',
684+
type: 'image',
685+
file: AttachmentFile(size: 100, path: 'test-file-path'),
686+
);
687+
688+
final message = Message(
689+
id: 'test-message-id',
690+
attachments: [attachment],
691+
pollId: 'poll-123',
692+
);
693+
694+
when(
695+
() => client.sendImage(
696+
any(),
697+
channelId,
698+
channelType,
699+
onSendProgress: any(named: 'onSendProgress'),
700+
cancelToken: any(named: 'cancelToken'),
701+
extraData: any(named: 'extraData'),
702+
),
703+
).thenAnswer(
704+
(_) async => throw StreamChatNetworkError.raw(
705+
code: 0,
706+
message: 'Request cancelled',
707+
isRequestCancelledError: true,
708+
),
709+
);
710+
711+
when(
712+
() => client.sendMessage(
713+
any(that: isSameMessageAs(message)),
714+
channelId,
715+
channelType,
716+
),
717+
).thenAnswer(
718+
(_) async => SendMessageResponse()
719+
..message = message.copyWith(
720+
attachments: [],
721+
state: MessageState.sent,
722+
),
723+
);
724+
725+
final res = await channel.sendMessage(message);
726+
727+
expect(res, isNotNull);
728+
expect(res.message.pollId, 'poll-123');
729+
730+
verify(
731+
() => client.sendImage(
732+
any(),
733+
channelId,
734+
channelType,
735+
onSendProgress: any(named: 'onSendProgress'),
736+
cancelToken: any(named: 'cancelToken'),
737+
extraData: any(named: 'extraData'),
738+
),
739+
);
740+
741+
verify(
742+
() => client.sendMessage(
743+
any(that: isSameMessageAs(message)),
744+
channelId,
745+
channelType,
746+
),
747+
);
748+
},
749+
);
462750
});
463751

464752
group('`.createDraft`', () {

0 commit comments

Comments
 (0)