Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement]Provide call rejection reason #449

Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### ✅ Added
- Support for custom participant sorting in the Call object. [#438](https://github.com/GetStream/stream-video-swift/pull/438)
- Ability to join call in advance with joinAheadTimeSeconds parameter. [#446](https://github.com/GetStream/stream-video-swift/pull/446)
- Missed calls support [#449](https://github.com/GetStream/stream-video-swift/pull/449)
- IncomingCallViewModel has been simplified and the `hideIncomingCallScreen` property as also the `stopTimer` have been removed. [#449](https://github.com/GetStream/stream-video-swift/pull/449)

# [1.0.8](https://github.com/GetStream/stream-video-swift/releases/tag/1.0.8)
_June 17, 2024_
Expand Down
8 changes: 0 additions & 8 deletions DemoApp/Sources/Examples/Examples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,6 @@ struct CustomIncomingCallView: View {
.padding()
}
.background(Color.white.edgesIgnoringSafeArea(.all))
.onChange(of: viewModel.hideIncomingCallScreen) { newValue in
if newValue {
callViewModel.rejectCall(callType: callInfo.type, callId: callInfo.id)
}
}
.onDisappear {
viewModel.stopTimer()
}
}

var callInfo: IncomingCall {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,11 @@ fileprivate func content() {

}
.background(Color.white.edgesIgnoringSafeArea(.all))
.onChange(of: viewModel.hideIncomingCallScreen) { newValue in
if newValue {
callViewModel.rejectCall(callType: callInfo.type, callId: callInfo.id)
}
}
.onDisappear {
viewModel.stopTimer()
}
}

var callInfo: IncomingCall {
viewModel.callInfo
}

}

class CustomViewFactory: ViewFactory {
Expand Down
72 changes: 59 additions & 13 deletions Sources/StreamVideo/CallKit/CallKitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,22 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
)

log.debug(
"Reporting VoIP incoming call with callUUID:\(callUUID) cid:\(cid) callerId:\(callerId) callerName:\(localizedCallerName)."
"""
Reporting VoIP incoming call with
callUUID:\(callUUID)
cid:\(cid)
callerId:\(callerId)
callerName:\(localizedCallerName)
"""
)

guard let streamVideo, let callEntry = storage[callUUID] else {
log.warning("CallKit operation:reportIncomingCall cannot be fulfilled because StreamVideo is nil.")
log.warning(
"""
CallKit operation:reportIncomingCall cannot be fulfilled because
StreamVideo is nil.
"""
)
callEnded(cid)
return
}
Expand Down Expand Up @@ -146,15 +157,23 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
} else {
log.debug(
"""
Rejecting VoIP incoming call with callUUID:\(callUUID)
cid:\(cid) callerId:\(callerId) callerName:\(localizedCallerName)
as it has been handled.
Rejecting VoIP incoming call as it has been handled.
callUUID:\(callUUID)
cid:\(cid)
callerId:\(callerId)
callerName:\(localizedCallerName)
"""
)
callEnded(cid)
}
} catch {
log.error("Failed to report incoming call with callId:\(callId) callType:\(callType)")
log.error(
"""
Failed to report incoming call with
callId:\(callId)
callType:\(callType)
"""
)
callEnded(cid)
}
}
Expand Down Expand Up @@ -313,12 +332,29 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
}

Task {
log
.debug(
"Ending VoIP call with callId:\(stackEntry.call.callId) callType:\(stackEntry.call.callType) callerId:\(String(describing: stackEntry.createdBy?.id))."
)
log.debug(
"""
Ending VoIP call with
callId:\(stackEntry.call.callId)
callType:\(stackEntry.call.callType)
callerId:\(String(describing: stackEntry.createdBy?.id))
"""
)
do {
try await stackEntry.call.reject()
let rejectionReason = streamVideo?
.rejectionReasonProvider
.reason(
for: stackEntry.call.cId,
ringTimeout: false
)
log.debug(
"""
Rejecting with reason: \(rejectionReason ?? "nil")
call:\(stackEntry.call.callId)
callType: \(stackEntry.call.callType)
"""
)
try await stackEntry.call.reject(reason: rejectionReason)
} catch {
log.error(error)
}
Expand Down Expand Up @@ -396,7 +432,12 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
callEventsSubscription = nil

guard let streamVideo else {
log.warning("CallKit operation:\(#function) cannot be fulfilled because StreamVideo is nil.")
log.warning(
"""
CallKit operation:\(#function) cannot be fulfilled because
StreamVideo is nil.
"""
)
return
}

Expand Down Expand Up @@ -454,7 +495,12 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
let update = CXCallUpdate()
let idComponents = cid.components(separatedBy: ":")
let uuid = uuidFactory.get()
if idComponents.count >= 2, let call = streamVideo?.call(callType: idComponents[0], callId: idComponents[1]) {
if
idComponents.count >= 2,
let call = streamVideo?.call(
callType: idComponents[0],
callId: idComponents[1]
) {
storage[uuid] = .init(call: call, callUUID: uuid)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// An extension of `RejectCallRequest` that defines various reasons for rejecting a call.
extension RejectCallRequest {

/// Enum containing possible reasons for rejecting a call.
public enum Reason {

/// Indicates that the callee is busy and cannot accept the call.
public static let busy = "busy"

/// Indicates that the callee intentionally declines the call.
public static let decline = "decline"

/// Indicates that the caller cancels the call.
public static let cancel = "cancel"

/// Indicates that the callee didn't answer the call in a given time amount.
public static let timeout = "timeout"
}
}
11 changes: 9 additions & 2 deletions Sources/StreamVideo/StreamVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
/// Provides information regarding hardware-acceleration capabilities (neuralEngine) on device.
public var isHardwareAccelerationAvailable: Bool { neuralEngineExists }

/// A protocol that provides a method to determine the rejection reason for a call.
public lazy var rejectionReasonProvider: RejectionReasonProviding = StreamRejectionReasonProvider(self)

var token: UserToken

private var tokenProvider: UserTokenProvider
Expand Down Expand Up @@ -187,8 +190,12 @@ public class StreamVideo: ObservableObject, @unchecked Sendable {
log.error("Error connecting as guest", error: error)
}
} else {
try Task.checkCancellation()
try await self.connectUser(isInitial: true)
do {
try Task.checkCancellation()
try await self.connectUser(isInitial: true)
} catch {
log.error(error)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Combine
import Foundation

/// A protocol that provides a method to determine the rejection reason for a call.
public protocol RejectionReasonProviding {

/// Determines the rejection reason for a call with the specified call ID.
///
/// - Parameters:
/// - callCid: The call ID to evaluate.
/// - ringTimeout: Informs the provider that the rejection is because of the ringing call timeout.
///
/// - Returns: A string representing the rejection reason, or `nil` if there is no reason to reject
/// the call.
///
/// - Note: ``ringTimeout`` being true, has an effect **only** when it's set from the side of
/// the caller when the callee doesn't reply the ringing call in the amount of time set on the dashboard.
func reason(for callCid: String, ringTimeout: Bool) -> String?
}

/// A provider that determines the rejection reason for a call based on its state.
final class StreamRejectionReasonProvider: RejectionReasonProviding {

/// The stream video associated with this provider.
private weak var streamVideo: StreamVideo?

/// A container for managing cancellable subscriptions.
private let cancellables: DisposableBag = .init()

init(_ streamVideo: StreamVideo) {
self.streamVideo = streamVideo
}

// MARK: - RejectionReasonProviding

@MainActor
func reason(
for callCid: String,
ringTimeout: Bool
) -> String? {
let activeCall = streamVideo?.state.activeCall

guard
let rejectingCall = streamVideo?.state.ringingCall,
rejectingCall.cId == callCid
else {
return nil
}

let isUserBusy = activeCall != nil
let isUserRejectingOutgoingCall = rejectingCall.state.createdBy?.id == streamVideo?.user.id

if isUserBusy {
return RejectCallRequest.Reason.busy
} else if isUserRejectingOutgoingCall {
return ringTimeout
? RejectCallRequest.Reason.timeout
: RejectCallRequest.Reason.cancel
} else {
return RejectCallRequest.Reason.decline
}
}
}
66 changes: 52 additions & 14 deletions Sources/StreamVideoSwiftUI/CallViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,10 +430,24 @@ open class CallViewModel: ObservableObject {
/// - Parameters:
/// - callType: the type of the call.
/// - callId: the id of the call.
public func rejectCall(callType: String, callId: String) {
public func rejectCall(
callType: String,
callId: String
) {
Task {
let call = streamVideo.call(callType: callType, callId: callId)
_ = try? await call.reject()
let rejectionReason = streamVideo
.rejectionReasonProvider
.reason(for: call.cId, ringTimeout: false)
log.debug(
"""
Rejecting with reason: \(rejectionReason ?? "nil")
call:\(call.callId)
callType: \(call.callType)
ringTimeout: \(false)
"""
)
_ = try? await call.reject(reason: rejectionReason)
self.callingState = .idle
}
}
Expand Down Expand Up @@ -481,14 +495,7 @@ open class CallViewModel: ObservableObject {

/// Hangs up from the active call.
public func hangUp() {
if callingState == .outgoing {
Task {
_ = try? await call?.reject()
leaveCall()
}
} else {
leaveCall()
}
handleCallHangUp(ringTimeout: false)
}

/// Sets a video filter for the current call.
Expand Down Expand Up @@ -618,15 +625,46 @@ open class CallViewModel: ObservableObject {
withTimeInterval: timeout,
repeats: false,
block: { [weak self] _ in
guard let self = self else { return }
log.debug("Detected ringing timeout, hanging up...")
Task {
await self.hangUp()
Task { @MainActor [weak self] in
guard let self = self else { return }
log.debug("Detected ringing timeout, hanging up...")
handleCallHangUp(ringTimeout: true)
}
}
)
}

private func handleCallHangUp(ringTimeout: Bool = false) {
guard
let call,
callingState == .outgoing
else {
leaveCall()
return
}

Task {
do {
let rejectionReason = streamVideo
.rejectionReasonProvider
.reason(for: call.cId, ringTimeout: ringTimeout)
log.debug(
"""
Rejecting with reason: \(rejectionReason ?? "nil")
call:\(call.callId)
callType: \(call.callType)
ringTimeout: \(ringTimeout)
"""
)
try await call.reject(reason: rejectionReason)
} catch {
log.error(error)
}

leaveCall()
}
}

private func subscribeToCallEvents() {
callEventsSubscriptionTask = Task {
for await event in streamVideo.subscribe() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ public struct IncomingCallView: View {
onCallAccepted: onCallAccepted,
onCallRejected: onCallRejected
)
.onChange(of: viewModel.hideIncomingCallScreen) { newValue in
if newValue {
onCallRejected(viewModel.callInfo.id)
}
}
.onDisappear {
viewModel.stopTimer()
}
}
}

Expand Down
Loading
Loading