Element Call: Send rtc.decline event when incoming call is declined (#4499)

This commit is contained in:
Valere Fedronic
2025-09-16 10:14:04 +02:00
committed by GitHub
parent f6d3992601
commit dc1f174d40
6 changed files with 121 additions and 3 deletions

View File

@@ -8997,6 +8997,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
return elementCallWidgetDriverDeviceIDReturnValue
}
}
//MARK: - declineCall
var declineCallNotificationIDUnderlyingCallsCount = 0
var declineCallNotificationIDCallsCount: Int {
get {
if Thread.isMainThread {
return declineCallNotificationIDUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = declineCallNotificationIDUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
declineCallNotificationIDUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
declineCallNotificationIDUnderlyingCallsCount = newValue
}
}
}
}
var declineCallNotificationIDCalled: Bool {
return declineCallNotificationIDCallsCount > 0
}
var declineCallNotificationIDReceivedNotificationID: String?
var declineCallNotificationIDReceivedInvocations: [String] = []
var declineCallNotificationIDUnderlyingReturnValue: Result<Void, RoomProxyError>!
var declineCallNotificationIDReturnValue: Result<Void, RoomProxyError>! {
get {
if Thread.isMainThread {
return declineCallNotificationIDUnderlyingReturnValue
} else {
var returnValue: Result<Void, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = declineCallNotificationIDUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
declineCallNotificationIDUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
declineCallNotificationIDUnderlyingReturnValue = newValue
}
}
}
}
var declineCallNotificationIDClosure: ((String) async -> Result<Void, RoomProxyError>)?
func declineCall(notificationID: String) async -> Result<Void, RoomProxyError> {
declineCallNotificationIDCallsCount += 1
declineCallNotificationIDReceivedNotificationID = notificationID
DispatchQueue.main.async {
self.declineCallNotificationIDReceivedInvocations.append(notificationID)
}
if let declineCallNotificationIDClosure = declineCallNotificationIDClosure {
return await declineCallNotificationIDClosure(notificationID)
} else {
return declineCallNotificationIDReturnValue
}
}
//MARK: - matrixToPermalink
var matrixToPermalinkUnderlyingCallsCount = 0

View File

@@ -16,6 +16,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
private struct CallID: Equatable {
let callKitID: UUID
let roomID: String
let rtcNotificationID: String?
}
private let pushRegistry: PKPushRegistry
@@ -93,7 +94,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
let callID = if let incomingCallID, incomingCallID.roomID == roomID {
incomingCallID
} else {
CallID(callKitID: UUID(), roomID: roomID)
CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: nil)
}
incomingCallID = nil
@@ -146,12 +147,17 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return
}
guard let rtcNotificationID = payload.dictionaryPayload[ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue] as? String else {
MXLog.error("Something went wrong, missing rtc notification event identifier for incoming voip call: \(payload)")
return
}
guard ongoingCallID?.roomID != roomID else {
MXLog.warning("Call already ongoing for room \(roomID), ignoring incoming push")
return
}
let callID = CallID(callKitID: UUID(), roomID: roomID)
let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID)
incomingCallID = callID
let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
@@ -253,6 +259,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
}
if let incomingCallID {
Task {
await sendDeclineCallEvent(incomingCallID)
}
}
tearDownCallSession(sendEndCallAction: false)
action.fulfill()
@@ -274,6 +286,25 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
ongoingCallID = nil
}
private func sendDeclineCallEvent(_ incomingCallID: CallID) async {
guard let rtcNotificationID = incomingCallID.rtcNotificationID else {
MXLog.info("No rtc notification event to decline.")
return
}
guard let clientProxy else {
MXLog.warning("A ClientProxy is needed to fetch the room.")
return
}
guard case let .joined(roomProxy) = await clientProxy.roomForIdentifier(incomingCallID.roomID) else {
MXLog.warning("Failed to fetch a joined room for the incoming call.")
return
}
_ = await roomProxy.declineCall(notificationID: rtcNotificationID)
}
private func observeIncomingCallRoomInfo() async {
incomingCallRoomInfoCancellable = nil

View File

@@ -10,6 +10,9 @@ import Foundation
enum ElementCallServiceNotificationKey: String {
case roomID
case roomDisplayName
/// When an incoming call is set to ring, there will be a `m.rtc.notification`event (MSC4075).
/// Keep the notification event id as it is needed to decline calls (MSC4310).
case rtcNotifyEventID
}
let ElementCallServiceNotificationDiscardDelta = 15.0

View File

@@ -639,6 +639,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
ElementCallWidgetDriver(room: room, deviceID: deviceID)
}
func declineCall(notificationID: String) async -> Result<Void, RoomProxyError> {
do {
try await room.declineCall(rtcNotificationEventId: notificationID)
return .success(())
} catch {
MXLog.error("Failed to decline rtc notification \(notificationID) with error: \(error)")
return .failure(.sdkError(error))
}
}
// MARK: - Permalinks
func matrixToPermalink() async -> Result<URL, RoomProxyError> {

View File

@@ -169,6 +169,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
// MARK: - Element Call
func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol
func declineCall(notificationID: String) async -> Result<Void, RoomProxyError>
// MARK: - Permalinks

View File

@@ -126,6 +126,7 @@ class NotificationHandler {
return .processedShouldDiscard
case .callNotify(let notifyType):
return await handleCallNotification(notifyType: notifyType,
rtcNotifyEventID: event.eventId(),
timestamp: event.timestamp(),
roomID: itemProxy.roomID,
roomDisplayName: itemProxy.roomDisplayName)
@@ -153,6 +154,7 @@ class NotificationHandler {
/// Handle incoming call notifications.
/// - Returns: A boolean indicating whether the notification was handled and should now be discarded.
private func handleCallNotification(notifyType: NotifyType,
rtcNotifyEventID: String,
timestamp: Timestamp,
roomID: String,
roomDisplayName: String) async -> NotificationProcessingResult {
@@ -206,7 +208,8 @@ class NotificationHandler {
}
let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID,
ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName]
ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName,
ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID]
do {
try await CXProvider.reportNewIncomingVoIPPushPayload(payload)