Skip to content

Conversation

xsahil03x
Copy link
Member

@xsahil03x xsahil03x commented Sep 22, 2025

Submit a pull request

Fixes: FLU-263
Fixes: #2391

Description of the pull request

This commit prevents the sending of messages that are considered empty by the backend.

A new private method _isMessageValidForUpload is added to the Channel class. This method checks if a message has:

  • Non-empty text
  • Attachments
  • A quoted message ID
  • A poll ID

If none of these conditions are met, the message is considered invalid.

The sendMessage method now calls _isMessageValidForUpload before attempting to send the message to the server. If the message is invalid, it is removed from the local state, a warning is logged, and a StreamChatError is thrown, preventing the message from being sent.

Summary by CodeRabbit

  • Bug Fixes
    • Prevents sending empty messages when all attachments are canceled; such messages are removed locally and not sent to the server, preserving message order.
  • Documentation
    • Changelog updated with an upcoming entry describing the fix.
  • Tests
    • Added tests covering message validity, attachment-cancel scenarios, and correct send/cleanup behavior.

This commit prevents the sending of messages that are considered empty by the backend.

A new private method `_isMessageValidForUpload` is added to the `Channel` class. This method checks if a message has:
- Non-empty text
- Attachments
- A quoted message ID
- A poll ID

If none of these conditions are met, the message is considered invalid.

The `sendMessage` method now calls `_isMessageValidForUpload` before attempting to send the message to the server. If the message is invalid, it is removed from the local state, a warning is logged, and a `StreamChatError` is thrown, preventing the message from being sent.
This commit adds tests to ensure correct behavior when sending messages with attachments that are subsequently cancelled.

The new tests cover the following scenarios:

- **All attachments cancelled, no other content:** The message should not be sent.
- **Attachment cancelled, text exists:** The message should be sent without the attachment.
- **Attachment cancelled, quoted message exists:** The message should be sent without the attachment.
- **Attachment cancelled, poll exists:** The message should be sent without the attachment.
Copy link
Contributor

coderabbitai bot commented Sep 22, 2025

Walkthrough

Adds validation to channel send flow to prevent sending empty messages when all attachments are cancelled. Implements a private helper to assess message validity, short-circuits send after attachments complete, removes the local message on invalid attempts, logs a warning, throws StreamChatError, and adds tests and changelog entry.

Changes

Cohort / File(s) Summary
Changelog Update
packages/stream_chat/CHANGELOG.md
Adds upcoming fixed notes: prevent empty message send when attachments are cancelled; ensure drafts include only successfully uploaded attachments.
Channel Send Validation
packages/stream_chat/lib/src/client/channel.dart
Adds private _isMessageValidForUpload(Message) and validates message after attachment uploads. If invalid: logs a warning, hard-deletes the local message, throws StreamChatError, and avoids calling the server. Awaits uploads before validation to preserve ordering.
Tests for Send Logic
packages/stream_chat/test/src/client/channel_test.dart
Adds tests covering: sending blocked for invalid (empty) messages, blocked when all attachments cancelled, allowed sends when text/quotedMessage/poll exist despite cancelled attachments; verifies upload/send calls and state transitions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI
  participant Channel
  participant Uploader
  participant ClientAPI

  User->>UI: tap send
  UI->>Channel: sendMessage(message)
  Channel->>Uploader: uploadAttachments(message.attachments)
  Uploader-->>Channel: uploadsComplete (finished / cancelled)

  rect rgb(230,245,255)
    note over Channel: validate after uploads
    Channel->>Channel: _isMessageValidForUpload(message)?
  end

  alt valid
    Channel->>ClientAPI: sendMessage(message)
    ClientAPI-->>Channel: server response
    Channel-->>UI: update state -> sent
  else invalid
    Channel->>Channel: log warning
    Channel->>Channel: remove local message (hard delete)
    Channel-->>UI: throw StreamChatError
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • renefloor
  • Brazol

Poem

I queued a note with files to send—
some uploads hopped to a cancel end.
No phantom pings or empty post,
the channel cleans what matters most.
Hopping through tests, all checks align—
valid messages cross the line. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "fix(llc): prevent sending empty messages" concisely and accurately summarizes the primary change in the PR, which is to prevent sending empty messages when uploads are cancelled; it is specific, short, and directly related to the changeset and objectives. This makes it clear to reviewers and teammates what the main fix achieves.
Linked Issues Check ✅ Passed The PR implements Channel._isMessageValidForUpload and enforces validation in sendMessage to remove invalid messages from local state and throw a StreamChatError so no server call is made, and the added tests assert sendMessage is not invoked when uploads are cancelled; this directly prevents creation/display of empty messages and avoids sending them to the server. By preventing the aborted empty message from being created/sent, the change also prevents push notifications for aborted uploads and removes the root cause of incorrect unread counts described in FLU-263 and #2391. Tests additionally confirm intended behavior when attachments are cancelled but text, quotedMessageId, or pollId exist, preserving valid sends.
Out of Scope Changes Check ✅ Passed The modifications are confined to the Channel send flow (private helper and validation), unit tests, and the changelog; there are no public API or signature changes and no edits to unrelated modules. All changes are aligned with the linked issues' goals and testing focuses on relevant behaviors, so no out-of-scope code was introduced.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/send-empty-attachment-message

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/stream_chat/CHANGELOG.md (1)

1-7: Clarify scope and cross‑link PR for traceability.

Call out that this also prevents ghost/empty local messages and unintended push notifications, and add a PR link like other entries.

Apply this diff:

 ## Upcoming

 🐞 Fixed

-- Fixed `Channel.sendMessage` to prevent sending empty messages when all attachments are cancelled
-  during upload.
+- Fixed `Channel.sendMessage` to prevent sending empty messages when all attachments are cancelled
+  during upload, avoiding ghost messages in the timeline and unintended push notifications.
+  [[#2389]](https://github.com/GetStream/stream-chat-flutter/pull/2389)
packages/stream_chat/lib/src/client/channel.dart (2)

661-669: Validation logic LGTM; add doc and optionally normalize zero‑width whitespace.

The criteria match the intent. Optional hardening: strip zero‑width characters so messages with only ZWSPs don’t slip through.

Apply this diff to document and harden text detection:

-bool _isMessageValidForUpload(Message message) {
-  final hasText = message.text?.trim().isNotEmpty == true;
+/// Returns true if the message has uploadable content.
+/// Keep this in sync with backend "empty message" validation semantics.
+bool _isMessageValidForUpload(Message message) {
+  // Treat only-whitespace or only zero-width characters as empty.
+  final normalizedText = message.text
+      ?.replaceAll(RegExp(r'[\s\u200B\u200C\u200D\u2060\uFEFF]+'), '');
+  final hasText = normalizedText?.isNotEmpty == true;
   final hasAttachments = message.attachments.isNotEmpty;
   final hasQuotedMessage = message.quotedMessageId != null;
   final hasPoll = message.pollId != null;

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

728-736: Right place to gate send; improve warning context.

Early bail after uploads prevents empty sends and push. Log the message id and reason for easier debugging.

Please confirm tests include: (1) all‑attachments‑cancelled with only whitespace/ZWSP text, and (2) quoted‑only and poll‑only messages.

Apply this diff:

-// 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');
+// Validate the final message before sending it to the server.
+if (_isMessageValidForUpload(message) == false) {
+  client.logger.warning(
+    'Message(${message.id}) invalid (no text, attachments, quote, or poll). '
+    'Removing locally and aborting send.',
+  );
 
   // Remove the message from state as it is invalid.
   state!.deleteMessage(message, hardDelete: true);
   throw const StreamChatError('Message is not valid for sending');
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ee78069 and 8940e53.

📒 Files selected for processing (3)
  • packages/stream_chat/CHANGELOG.md (1 hunks)
  • packages/stream_chat/lib/src/client/channel.dart (2 hunks)
  • packages/stream_chat/test/src/client/channel_test.dart (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: format
  • GitHub Check: build (android)
  • GitHub Check: test
  • GitHub Check: analyze
  • GitHub Check: stream_chat_localizations
  • GitHub Check: stream_chat_flutter_core
  • GitHub Check: stream_chat_flutter
  • GitHub Check: stream_chat
  • GitHub Check: stream_chat_persistence
🔇 Additional comments (7)
packages/stream_chat/test/src/client/channel_test.dart (7)

246-250: LGTM on the text addition.

The existing test case correctly adds a text field to a Message for the basic sendMessage functionality test, which aligns with the updated validation logic.


464-475: Comprehensive test for invalid message validation.

The test correctly verifies that sending an empty message (without text, attachments, quoted message ID, or poll ID) throws a StreamChatError and doesn't call client.sendMessage. This properly tests the new validation logic.


477-517: Excellent test for cancelled attachment scenario.

This test perfectly covers the core issue from the PR: when all attachments are cancelled (simulated by throwing StreamChatNetworkError with isRequestCancelledError: true), the message becomes invalid and should not be sent. The test correctly verifies that client.sendMessage is never called and a StreamChatError is thrown.


519-578: Good test for text presence overriding attachment cancellation.

This test validates that when an attachment upload is cancelled but the message still contains text, the message should still be sent successfully. This covers an important edge case where the validation should pass despite attachment failures.


580-644: Proper test for quoted message scenario.

This test verifies that a message with a quoted message ID should be sent even if attachment uploads are cancelled, since the quoted message ID makes it a valid message. The test correctly checks that client.sendMessage is called and the response contains the quoted message ID.


646-705: Solid test for poll ID scenario.

This test covers the case where a message with a poll ID should be sent even when attachments are cancelled, since the poll ID makes it valid. The verification correctly checks that the message is sent and the poll ID is preserved in the response.


464-705: Validation logic aligns with tests
_isMessageValidForUpload returns true if the message has text, attachments, a quoted message, or a poll—exactly matching each of the test scenarios.

Copy link

codecov bot commented Sep 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.81%. Comparing base (e83a790) to head (56e6e8f).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2389      +/-   ##
==========================================
+ Coverage   63.77%   63.81%   +0.03%     
==========================================
  Files         413      413              
  Lines       25829    25837       +8     
==========================================
+ Hits        16473    16488      +15     
+ Misses       9356     9349       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

renefloor
renefloor previously approved these changes Sep 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/stream_chat/test/src/client/channel_test.dart (5)

514-522: Assert exact invocation count for clarity

Verify the cancellation path triggers a single upload attempt.

Apply this diff:

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

581-590: Tighten verifications to single calls

Make intent explicit and catch accidental retries.

Apply these diffs:

           verify(
             () => client.sendImage(
               any(),
               channelId,
               channelType,
               onSendProgress: any(named: 'onSendProgress'),
               cancelToken: any(named: 'cancelToken'),
               extraData: any(named: 'extraData'),
-            ),
-          );
+            ),
+          ).called(1);
           verify(
             () => client.sendMessage(
               any(that: isSameMessageAs(message)),
               channelId,
               channelType,
-            ),
-          );
+            ),
+          ).called(1);

Also applies to: 592-599


659-667: Also assert exact call counts in quoted message flow

Consistent and precise expectations help avoid flakiness.

Apply these diffs:

           verify(
             () => client.sendImage(
               any(),
               channelId,
               channelType,
               onSendProgress: any(named: 'onSendProgress'),
               cancelToken: any(named: 'cancelToken'),
               extraData: any(named: 'extraData'),
-            ),
-          );
+            ),
+          ).called(1);
           verify(
             () => client.sendMessage(
               any(that: isSameMessageAs(message)),
               channelId,
               channelType,
-            ),
-          );
+            ),
+          ).called(1);

Also applies to: 669-676


731-739: Likewise for the poll flow: enforce single invocations

Keeps checks strict and consistent.

Apply these diffs:

           verify(
             () => client.sendImage(
               any(),
               channelId,
               channelType,
               onSendProgress: any(named: 'onSendProgress'),
               cancelToken: any(named: 'cancelToken'),
               extraData: any(named: 'extraData'),
-            ),
-          );
+            ),
+          ).called(1);
           verify(
             () => client.sendMessage(
               any(that: isSameMessageAs(message)),
               channelId,
               channelType,
-            ),
-          );
+            ),
+          ).called(1);

Also applies to: 741-748


464-475: Optional: Assert local-state cleanup for invalid message

To fully cover FLU-263/2391, consider asserting the message isn’t retained in channel.state after the failure.

Example:

await expectLater(
  channel.sendMessage(message),
  throwsA(isA<StreamChatError>()),
);
expect(channel.state!.messages.any((m) => m.id == message.id), isFalse);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8940e53 and 56e6e8f.

📒 Files selected for processing (2)
  • packages/stream_chat/CHANGELOG.md (1 hunks)
  • packages/stream_chat/test/src/client/channel_test.dart (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat/CHANGELOG.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: stream_chat_localizations
  • GitHub Check: stream_chat_flutter
  • GitHub Check: stream_chat
  • GitHub Check: stream_chat_flutter_core
  • GitHub Check: stream_chat_persistence
  • GitHub Check: build (android)
  • GitHub Check: analyze_legacy_versions
  • GitHub Check: analyze
  • GitHub Check: test
🔇 Additional comments (1)
packages/stream_chat/test/src/client/channel_test.dart (1)

248-248: Good fix to keep the baseline sendMessage test valid under new rules

Adding non-empty text ensures the message passes the new validation.

@xsahil03x xsahil03x merged commit 70c0f7e into master Sep 23, 2025
19 checks passed
@xsahil03x xsahil03x deleted the fix/send-empty-attachment-message branch September 23, 2025 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Wrong unread messages count in channels
2 participants