EC Timeout if it doesn't respond after 30 seconds (#4237)

* basic implementation

* using a decodable struct to interpret the widget message

* small code refactor/improvement
This commit is contained in:
Mauro
2025-06-20 18:21:31 +02:00
committed by GitHub
parent cbf405a67e
commit 0c8a401b4d
5 changed files with 48 additions and 1 deletions
@@ -6357,6 +6357,7 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S
}
var handleMessageClosure: ((String) async -> Result<Bool, ElementCallWidgetDriverError>)?
@discardableResult
func handleMessage(_ message: String) async -> Result<Bool, ElementCallWidgetDriverError> {
handleMessageCallsCount += 1
handleMessageReceivedMessage = message
@@ -94,3 +94,23 @@ enum CallScreenJavaScriptMessageName: String, CaseIterable {
allCases.map(\.postMessageScript).joined(separator: "\n")
}
}
struct DecodedWidgetMessage: Decodable {
private static let decoder = JSONDecoder()
private static let joinAction = "io.element.join"
private static let fromWidget = "fromWidget"
let action: String?
let api: String?
static func decode(message: String) throws -> DecodedWidgetMessage? {
guard let data = message.data(using: .utf8) else {
return nil
}
return try decoder.decode(DecodedWidgetMessage.self, from: data)
}
var hasJoined: Bool {
action == Self.joinAction && api == Self.fromWidget
}
}
@@ -26,6 +26,9 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
actionsSubject.eraseToAnyPublisher()
}
@CancellableTask
private var timeoutTask: Task<Void, Never>?
/// Designated initialiser
/// - Parameters:
/// - elementCallService: service responsible for setting up CallKit
@@ -109,6 +112,17 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
.store(in: &cancellables)
setupCall()
timeoutTask = Task { [weak self] in
try? await Task.sleep(for: .seconds(30))
guard !Task.isCancelled, let self else { return }
MXLog.error("Failed to join Element Call: Timeout")
state.bindings.alertInfo = .init(id: UUID(),
title: L10n.commonError,
message: L10n.errorUnknown,
primaryButton: .init(title: L10n.actionDismiss) { [weak self] in self?.actionsSubject.send(.dismiss) })
timeoutTask = nil
}
}
override func process(viewAction: CallScreenViewAction) {
@@ -129,7 +143,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
case .outputDeviceSelected(deviceID: let deviceID):
handleOutputDeviceSelected(deviceID: deviceID)
case .widgetAction(let message):
Task { await widgetDriver.handleMessage(message) }
Task { await handleWidgetAction(message: message) }
}
}
@@ -143,6 +157,16 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
}
// MARK: - Private
private func handleWidgetAction(message: String) async {
if timeoutTask != nil,
let decodedMessage = try? DecodedWidgetMessage.decode(message: message),
decodedMessage.hasJoined {
// This means that the call room was joined succesfully, we can stop the timeout task
timeoutTask = nil
}
await widgetDriver.handleMessage(message)
}
private func setupCall() {
switch configuration.kind {
@@ -163,6 +163,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv
return .success(url)
}
@discardableResult
func handleMessage(_ message: String) async -> Result<Bool, ElementCallWidgetDriverError> {
guard let widgetDriver else {
return .failure(.driverNotSetup)
@@ -36,6 +36,7 @@ protocol ElementCallWidgetDriverProtocol {
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError>
/// Passes a message from the Widget to the SDK to handle, returning a Bool that represents whether or not the widget driver is still running.
@discardableResult
func handleMessage(_ message: String) async -> Result<Bool, ElementCallWidgetDriverError>
}