mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Fixes #2878 - Retain cycles on the ElementCall webView and correctly tear down the call on dismissal.
This commit is contained in:
committed by
Stefan Ceriu
parent
8bfd802793
commit
f8d094516d
@@ -4313,6 +4313,11 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
|
||||
}
|
||||
}
|
||||
class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol {
|
||||
var widgetID: String {
|
||||
get { return underlyingWidgetID }
|
||||
set(value) { underlyingWidgetID = value }
|
||||
}
|
||||
var underlyingWidgetID: String!
|
||||
var messagePublisher: PassthroughSubject<String, Never> {
|
||||
get { return underlyingMessagePublisher }
|
||||
set(value) { underlyingMessagePublisher = value }
|
||||
|
||||
@@ -55,6 +55,10 @@ final class CallScreenCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
viewModel.stop()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(CallScreen(context: viewModel.context))
|
||||
|
||||
@@ -31,10 +31,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
deinit {
|
||||
elementCallService.tearDownCallSession()
|
||||
}
|
||||
|
||||
/// Designated initialiser
|
||||
/// - Parameters:
|
||||
/// - elementCallService: service responsible for setting up CallKit
|
||||
@@ -120,7 +116,28 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
Task {
|
||||
await hangUp()
|
||||
}
|
||||
|
||||
elementCallService.tearDownCallSession()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func hangUp() async {
|
||||
let hangUpMessage = """
|
||||
"api":"toWidget",
|
||||
"widgetId":"\(widgetDriver.widgetID)",
|
||||
"requestId":"widgetapi-\(UUID())",
|
||||
"action":"im.vector.hangup",
|
||||
"data":{}
|
||||
"""
|
||||
|
||||
let result = await widgetDriver.sendMessage(hangUpMessage)
|
||||
MXLog.error("Result yo: \(result)")
|
||||
}
|
||||
|
||||
private static let eventHandlerName = "elementx"
|
||||
|
||||
|
||||
@@ -20,4 +20,6 @@ import Combine
|
||||
protocol CallScreenViewModelProtocol {
|
||||
var actions: AnyPublisher<CallScreenViewModelAction, Never> { get }
|
||||
var context: CallScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ private struct WebView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
|
||||
private let viewModelContext: CallScreenViewModel.Context
|
||||
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
|
||||
private weak var viewModelContext: CallScreenViewModel.Context?
|
||||
|
||||
private(set) var webView: WKWebView!
|
||||
|
||||
@@ -73,7 +73,7 @@ private struct WebView: UIViewRepresentable {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
let userContentController = WKUserContentController()
|
||||
userContentController.add(self, name: viewModelContext.viewState.messageHandler)
|
||||
userContentController.add(WKScriptMessageHandlerWrapper(self), name: viewModelContext.viewState.messageHandler)
|
||||
|
||||
configuration.userContentController = userContentController
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
@@ -98,8 +98,8 @@ private struct WebView: UIViewRepresentable {
|
||||
// After testing different scenarios it seems that when using async/await version of these
|
||||
// methods wkwebView expects JavaScript to return with a value (something other than Void),
|
||||
// if there is no value returning from the JavaScript that you evaluate you will have a crash.
|
||||
try await withCheckedThrowingContinuation { continuaton in
|
||||
webView.evaluateJavaScript(script) { result, error in
|
||||
try await withCheckedThrowingContinuation { [weak self] continuaton in
|
||||
self?.webView.evaluateJavaScript(script) { result, error in
|
||||
if let error {
|
||||
continuaton.resume(throwing: error)
|
||||
} else {
|
||||
@@ -109,12 +109,10 @@ private struct WebView: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - WKScriptMessageHandler
|
||||
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
Task { @MainActor in
|
||||
viewModelContext.javaScriptMessageHandler?(message.body)
|
||||
Task { @MainActor [weak self] in
|
||||
self?.viewModelContext?.javaScriptMessageHandler?(message.body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +146,26 @@ private struct WebView: UIViewRepresentable {
|
||||
|
||||
nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
Task { @MainActor in
|
||||
viewModelContext.send(viewAction: .urlChanged(webView.url))
|
||||
viewModelContext?.send(viewAction: .urlChanged(webView.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Avoids retain loops between the configuration and webView coordinator
|
||||
private class WKScriptMessageHandlerWrapper: NSObject, WKScriptMessageHandler {
|
||||
private weak var coordinator: Coordinator?
|
||||
|
||||
init(_ coordinator: Coordinator) {
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
// MARK: - WKScriptMessageHandler
|
||||
|
||||
nonisolated func userContentController(_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage) {
|
||||
coordinator?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -41,6 +41,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv
|
||||
private let room: RoomProtocol
|
||||
private var widgetDriver: WidgetDriverAndHandle?
|
||||
|
||||
let widgetID = UUID().uuidString
|
||||
let messagePublisher = PassthroughSubject<String, Never>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ElementCallWidgetDriverAction, Never> = .init()
|
||||
@@ -60,7 +61,7 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv
|
||||
let useEncryption = (try? room.isEncrypted()) ?? false
|
||||
|
||||
guard let widgetSettings = try? newVirtualElementCallWidget(props: .init(elementCallUrl: baseURL.absoluteString,
|
||||
widgetId: UUID().uuidString,
|
||||
widgetId: widgetID,
|
||||
parentUrl: nil,
|
||||
hideHeader: nil,
|
||||
preload: nil,
|
||||
|
||||
@@ -32,6 +32,8 @@ enum ElementCallWidgetDriverAction {
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol ElementCallWidgetDriverProtocol {
|
||||
var widgetID: String { get }
|
||||
|
||||
var messagePublisher: PassthroughSubject<String, Never> { get }
|
||||
var actions: AnyPublisher<ElementCallWidgetDriverAction, Never> { get }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user