From 0c8a401b4dd3f1a9b5b96b1f8200a5c92a6c53d9 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:21:31 +0200 Subject: [PATCH] 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 --- .../Mocks/Generated/GeneratedMocks.swift | 1 + .../Screens/CallScreen/CallScreenModels.swift | 20 ++++++++++++++ .../CallScreen/CallScreenViewModel.swift | 26 ++++++++++++++++++- .../ElementCall/ElementCallWidgetDriver.swift | 1 + .../ElementCallWidgetDriverProtocol.swift | 1 + 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 48c17dd06..60eae73cd 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -6357,6 +6357,7 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S } var handleMessageClosure: ((String) async -> Result)? + @discardableResult func handleMessage(_ message: String) async -> Result { handleMessageCallsCount += 1 handleMessageReceivedMessage = message diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift b/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift index bee14e9ff..fcd7fcbad 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenModels.swift @@ -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 + } +} diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index 507168657..24167e60d 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -26,6 +26,9 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol actionsSubject.eraseToAnyPublisher() } + @CancellableTask + private var timeoutTask: Task? + /// 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 { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift index 0c5c0f85b..116cfbac4 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriver.swift @@ -163,6 +163,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv return .success(url) } + @discardableResult func handleMessage(_ message: String) async -> Result { guard let widgetDriver else { return .failure(.driverNotSetup) diff --git a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift index f082ddef4..b03433f80 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallWidgetDriverProtocol.swift @@ -36,6 +36,7 @@ protocol ElementCallWidgetDriverProtocol { analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result /// 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 }