Skip to content

chore(llc): improve reject and leave reasons#1218

Merged
Brazol merged 5 commits intomainfrom
chore/improve-call-end-reject-reason
Apr 28, 2026
Merged

chore(llc): improve reject and leave reasons#1218
Brazol merged 5 commits intomainfrom
chore/improve-call-end-reject-reason

Conversation

@Brazol
Copy link
Copy Markdown
Contributor

@Brazol Brazol commented Apr 21, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved disconnect and rejection reason propagation (includes explicit "call ended" and "user responded elsewhere" reasons, and more specific reject reasons).
    • Suppressed duplicate ringing notifications during coordinator-driven call termination paths.
    • Reduced noisy logging when leaving or rejecting calls.
  • Documentation

    • Added "Upcoming" changelog entries noting disconnect/reject reason propagation improvements.

@Brazol Brazol requested a review from a team as a code owner April 21, 2026 08:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@Brazol has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 6 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0347ea7d-a8de-442e-88d4-01d3c14a5ea8

📥 Commits

Reviewing files that changed from the base of the PR and between 668a02f and 136739e.

📒 Files selected for processing (1)
  • packages/stream_video/lib/src/call/call.dart
📝 Walkthrough

Walkthrough

Coordinator-driven accept/reject events are routed through new async handlers that can locally reject when another device acted; new CallRejectReason factories and DisconnectReason.callEnded() were added; SFU leave-reason mapping introduced; RingingEvent suppression added in coordinator termination paths; leave/reject logging adjusted.

Changes

Cohort / File(s) Summary
Changelog Updates
packages/stream_video/CHANGELOG.md, packages/stream_video_flutter/CHANGELOG.md
Added "Upcoming" -> "🐞 Fixed" entries noting improved disconnect/reject reason propagation.
Rejection & Disconnect Models
packages/stream_video/lib/src/call/call_reject_reason.dart, packages/stream_video/lib/src/models/disconnect_reason.dart
Added CallRejectReason factories: callEnded, callEndedLocally, userRespondedElsewhere, creatorRejected, allOtherParticipantsRejected. Added DisconnectReason.callEnded() and DisconnectReasonCallEnded singleton.
Coordinator & Call Handling
packages/stream_video/lib/src/call/call.dart, packages/stream_video/lib/src/call/state/mixins/state_coordinator_mixin.dart, packages/stream_video/lib/src/stream_video.dart
Coordinator accept/reject events now go through async wrappers that may locally reject (e.g., userRespondedElsewhere) before forwarding to state manager; hardcoded reject strings replaced with new factories; SFU leave reason centralized via _sfuLeaveReason; active/incoming call-ended handling updated to use DisconnectReason.callEnded().
Session Logging & Notifications
packages/stream_video/lib/src/call/session/call_session.dart, packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart
CallSession.leave() now logs the provided reason; push-notification manager suppresses upcoming RingingEvent (RingingEventBroadcaster().suppressEvent()) in coordinator-driven termination flows to avoid ringing leakage.

Sequence Diagram(s)

sequenceDiagram
    participant Coordinator
    participant CallHandler as Call (call.dart)
    participant StateMgr as StateManager / Session
    participant SFU
    participant Notifier as RingingEventBroadcaster

    Coordinator->>CallHandler: StreamCallRejectedEvent / StreamCallAcceptedEvent
    CallHandler->>CallHandler: await _handleCoordinatorCallRejected/_handleCoordinatorCallAccepted()
    alt other-device response && CallStatusIncoming && not acceptedByMe
        CallHandler->>CallHandler: log and create CallRejectReason.userRespondedElsewhere()
        CallHandler->>StateMgr: coordinatorCallRejected(userRespondedElsewhere)
        CallHandler->>Notifier: suppressEvent()
    else normal flow
        CallHandler->>StateMgr: coordinatorCallAccepted/coordinatorCallRejected(with factory reason)
    end
    StateMgr->>SFU: leave(sessionId, reason via _sfuLeaveReason)
    SFU-->>StateMgr: left
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through events with reasons clear and bright,

When someone answers elsewhere, I tidy up the night.
New names for endings, suppressed rings on cue,
Handlers, reasons, leave—my code hops through. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty, missing all required template sections including Goal, Implementation details, Testing, and Contributor Checklist. Provide a complete description following the repository template with clear explanations of the goal, implementation details, how the change was tested, and confirmation of the contributor checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'chore(llc): improve reject and leave reasons' clearly and concisely describes the main changes across the PR, which involve enhancing how disconnect/reject reasons are propagated and handled throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/improve-call-end-reject-reason

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
Copy Markdown

@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 (1)
packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart (1)

62-116: Narrow the suppression scope to reduce cross-call interference risk.

The blanket RingingEventBroadcaster().suppressEvent() calls suppress all RingingEvent types for 500 ms. While this may work for swallowing the native event immediately following endCallByCid, any unrelated event from a different call arriving on the platform channel during that window (e.g., ActionCallIncoming on another call) would also be silently dropped.

If feasible, narrow suppression to the specific event type expected after endCallByCid—ideally ActionCallEnded—to avoid cross-call interference:

RingingEventBroadcaster().suppressEvent(
  eventType: ActionCallEnded,
);

This pattern is already supported by suppressEvent and shown in test cases (see ringing_event_broadcaster_test.dart). Worth confirming which native RingingEvent(s) are actually emitted after endCallByCid on iOS and Android so the suppression can be scoped appropriately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart`
around lines 62 - 116, The current blanket
RingingEventBroadcaster().suppressEvent() calls in the
CoordinatorCallEndedEvent, CoordinatorCallSessionParticipantCountUpdatedEvent,
and CoordinatorCallRejectedEvent handlers suppress all ringing events and can
drop unrelated call events; update each suppression to narrow the scope by
passing the specific native event type expected after ending a call (e.g. call
RingingEventBroadcaster().suppressEvent(eventType: ActionCallEnded)) right
before calling endCallByCid(event.callCid.toString()), and confirm/adjust the
eventType if the platform emits a different RingingEvent for end-call on
iOS/Android.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stream_video/lib/src/call/call.dart`:
- Around line 650-688: The current early-return branches call reject() for
events originating from the same user on another device, which incorrectly sends
a coordinator reject; instead, remove the await reject(...) calls in
_handleCoordinatorCallAccepted and _handleCoordinatorCallRejected and, in those
branches, only update local state by invoking the coordinator handlers directly
(call _stateManager.coordinatorCallAccepted(event) in the accepted branch and
_stateManager.coordinatorCallRejected(event) in the rejected branch) and then
return so we clear local ringing without sending a new coordinator reject.

---

Nitpick comments:
In
`@packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart`:
- Around line 62-116: The current blanket
RingingEventBroadcaster().suppressEvent() calls in the
CoordinatorCallEndedEvent, CoordinatorCallSessionParticipantCountUpdatedEvent,
and CoordinatorCallRejectedEvent handlers suppress all ringing events and can
drop unrelated call events; update each suppression to narrow the scope by
passing the specific native event type expected after ending a call (e.g. call
RingingEventBroadcaster().suppressEvent(eventType: ActionCallEnded)) right
before calling endCallByCid(event.callCid.toString()), and confirm/adjust the
eventType if the platform emits a different RingingEvent for end-call on
iOS/Android.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c55ebc72-4e2f-4297-92c4-71cd854bbef5

📥 Commits

Reviewing files that changed from the base of the PR and between 5201b9b and a8d06b9.

📒 Files selected for processing (9)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/call.dart
  • packages/stream_video/lib/src/call/call_reject_reason.dart
  • packages/stream_video/lib/src/call/session/call_session.dart
  • packages/stream_video/lib/src/call/state/mixins/state_coordinator_mixin.dart
  • packages/stream_video/lib/src/models/disconnect_reason.dart
  • packages/stream_video/lib/src/stream_video.dart
  • packages/stream_video_flutter/CHANGELOG.md
  • packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart

Comment thread packages/stream_video/lib/src/call/call.dart
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
packages/stream_video/lib/src/call/call.dart (1)

656-665: ⚠️ Potential issue | 🟠 Major

Don't send a second coordinator reject for same-user multi-device events.

Lines 664 and 685 still call reject(), which sends _coordinatorClient.rejectCall(...). At that point the coordinator event is already authoritative, so this path should only clear the local ringing state; otherwise a same-user accept/reject on another device can be followed by a contradictory extra reject from this device.

Suggested fix
     if (event.acceptedByUserId == currentUserId &&
         status is CallStatusIncoming &&
         !status.acceptedByMe) {
       _logger.i(
         () =>
             '[onCoordinatorEvent] call accepted on another device, '
             'rejecting locally with userRespondedElsewhere',
       );
-      await reject(reason: CallRejectReason.userRespondedElsewhere());
+      _stateManager.coordinatorCallAccepted(event);
       return;
     }
@@
     if (event.rejectedByUserId == currentUserId &&
         status is CallStatusIncoming &&
         !status.acceptedByMe) {
       _logger.i(
         () =>
             '[onCoordinatorEvent] call rejected on another device, '
             'rejecting locally with userRespondedElsewhere',
       );
-      await reject(reason: CallRejectReason.userRespondedElsewhere());
+      _stateManager.coordinatorCallRejected(event);
       return;
     }

Also applies to: 677-686

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stream_video/lib/src/call/call.dart` around lines 656 - 665, The
handler is sending a coordinator reject after the coordinator already signaled
acceptance on another device; instead of calling reject() (which triggers
_coordinatorClient.rejectCall), simply clear the local ringing/incoming state
and return. Locate the branches that check event.acceptedByUserId ==
currentUserId and status is CallStatusIncoming && !status.acceptedByMe (and the
similar branch for rejected-by-same-user) and remove the reject(reason:
CallRejectReason.userRespondedElsewhere()) / reject(...) calls; replace them
with code that updates local state only (e.g., mark the incoming/ringing state
as cleared or update the CallStatus locally) and keep the existing _logger.i
call and return. Ensure no coordinator RPC is invoked from these branches by
referencing the reject() method and CallRejectReason.* usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/stream_video/lib/src/call/call.dart`:
- Around line 656-665: The handler is sending a coordinator reject after the
coordinator already signaled acceptance on another device; instead of calling
reject() (which triggers _coordinatorClient.rejectCall), simply clear the local
ringing/incoming state and return. Locate the branches that check
event.acceptedByUserId == currentUserId and status is CallStatusIncoming &&
!status.acceptedByMe (and the similar branch for rejected-by-same-user) and
remove the reject(reason: CallRejectReason.userRespondedElsewhere()) /
reject(...) calls; replace them with code that updates local state only (e.g.,
mark the incoming/ringing state as cleared or update the CallStatus locally) and
keep the existing _logger.i call and return. Ensure no coordinator RPC is
invoked from these branches by referencing the reject() method and
CallRejectReason.* usages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5def9871-6300-4780-a76b-c6dbc1e44a24

📥 Commits

Reviewing files that changed from the base of the PR and between a8d06b9 and 82b0d2b.

📒 Files selected for processing (1)
  • packages/stream_video/lib/src/call/call.dart

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 43.10345% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 5.83%. Comparing base (339f044) to head (136739e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/stream_video/lib/src/call/call.dart 51.28% 19 Missing ⚠️
...stream_video/lib/src/models/disconnect_reason.dart 20.00% 4 Missing ⚠️
.../stream_video/lib/src/call/call_reject_reason.dart 40.00% 3 Missing ⚠️
packages/stream_video/lib/src/stream_video.dart 0.00% 3 Missing ⚠️
...cation/lib/src/stream_video_push_notification.dart 0.00% 3 Missing ⚠️
...tream_video/lib/src/call/session/call_session.dart 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##            main   #1218      +/-   ##
========================================
+ Coverage   5.79%   5.83%   +0.03%     
========================================
  Files        676     676              
  Lines      49345   49394      +49     
========================================
+ Hits        2862    2880      +18     
- Misses     46483   46514      +31     

☔ 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.

Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
packages/stream_video/lib/src/call/call.dart (1)

656-665: ⚠️ Potential issue | 🟠 Major

Do not echo a coordinator reject after another device already acted.

Both branches still call reject(), and reject() sends _coordinatorClient.rejectCall(...). When the coordinator event is already the authoritative accept/reject from the same user on another device, this issues a second, contradictory reject instead of only clearing local state.

🐛 Proposed fix
     if (event.acceptedByUserId == currentUserId &&
         status is CallStatusIncoming &&
         !status.acceptedByMe) {
@@
-      await reject(reason: CallRejectReason.userRespondedElsewhere());
+      await leave(
+        reason: DisconnectReason.rejected(
+          byUserId: currentUserId,
+          reason: CallRejectReason.userRespondedElsewhere(),
+        ),
+      );
       return;
     }
@@
     if (event.rejectedByUserId == currentUserId &&
         status is CallStatusIncoming &&
         !status.acceptedByMe) {
@@
-      await reject(reason: CallRejectReason.userRespondedElsewhere());
+      await leave(
+        reason: DisconnectReason.rejected(
+          byUserId: currentUserId,
+          reason: CallRejectReason.userRespondedElsewhere(),
+        ),
+      );
       return;
     }

Also applies to: 677-686

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stream_video/lib/src/call/call.dart` around lines 656 - 665, When
handling coordinator events where the authoritative accept/reject already came
from the same user on another device (the branches checking
event.acceptedByUserId == currentUserId with status is CallStatusIncoming &&
!status.acceptedByMe, and the analogous reject branch), do NOT call reject()
(which invokes _coordinatorClient.rejectCall(...)); instead only update/clear
local state and emit the local status change (e.g., mark the incoming status as
handled or set the local accepted/rejected flag) and return. Locate the logic in
onCoordinatorEvent around the checks referencing currentUserId,
CallStatusIncoming, and CallRejectReason.userRespondedElsewhere() and remove the
secondary call to reject(); replace it with local-only cleanup/update code so no
duplicate coordinator RPC is sent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stream_video/lib/src/call/call.dart`:
- Around line 2081-2102: The _sfuLeaveReason function currently falls back to a
generic message for some DisconnectReason variants; explicitly handle
DisconnectReasonBlocked, DisconnectReasonCancelled, and
DisconnectReasonManuallyClosed (or the enum/constructors named blocked,
cancelled, manuallyClosed) inside _sfuLeaveReason to return specific messages
(e.g., "blocked", "cancelled", "manually closed") instead of the generic 'user
is leaving the call', ensuring those reasons are preserved when passed into
leave().

---

Duplicate comments:
In `@packages/stream_video/lib/src/call/call.dart`:
- Around line 656-665: When handling coordinator events where the authoritative
accept/reject already came from the same user on another device (the branches
checking event.acceptedByUserId == currentUserId with status is
CallStatusIncoming && !status.acceptedByMe, and the analogous reject branch), do
NOT call reject() (which invokes _coordinatorClient.rejectCall(...)); instead
only update/clear local state and emit the local status change (e.g., mark the
incoming status as handled or set the local accepted/rejected flag) and return.
Locate the logic in onCoordinatorEvent around the checks referencing
currentUserId, CallStatusIncoming, and CallRejectReason.userRespondedElsewhere()
and remove the secondary call to reject(); replace it with local-only
cleanup/update code so no duplicate coordinator RPC is sent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f90eb69-9e51-49e9-a593-47f7fd41129c

📥 Commits

Reviewing files that changed from the base of the PR and between 82b0d2b and 668a02f.

📒 Files selected for processing (3)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/call.dart
  • packages/stream_video/lib/src/call/session/call_session.dart
✅ Files skipped from review due to trivial changes (2)
  • packages/stream_video/CHANGELOG.md
  • packages/stream_video/lib/src/call/session/call_session.dart

Comment thread packages/stream_video/lib/src/call/call.dart Outdated
@Brazol Brazol merged commit 7b13a90 into main Apr 28, 2026
14 checks passed
@Brazol Brazol deleted the chore/improve-call-end-reject-reason branch April 28, 2026 09:46
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.

2 participants