Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com> Co-authored-by: Doug <douglase@element.io>
221 lines
8.8 KiB
Swift
221 lines
8.8 KiB
Swift
//
|
|
// Copyright 2022-2024 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
import MatrixRustSDK
|
|
import SwiftUI
|
|
|
|
struct ElementCallWidgetMessage: Codable {
|
|
enum Direction: String, Codable {
|
|
case fromWidget
|
|
case toWidget
|
|
}
|
|
|
|
enum Action: String, Codable {
|
|
case hangup = "im.vector.hangup"
|
|
case close = "io.element.close"
|
|
case mediaState = "io.element.device_mute"
|
|
}
|
|
|
|
struct Data: Codable {
|
|
var audioEnabled: Bool?
|
|
var videoEnabled: Bool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case audioEnabled = "audio_enabled"
|
|
case videoEnabled = "video_enabled"
|
|
}
|
|
}
|
|
|
|
let direction: Direction
|
|
let action: Action
|
|
var data: Data = .init()
|
|
|
|
let widgetId: String
|
|
var requestId = "widgetapi-\(UUID())"
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case direction = "api"
|
|
case action
|
|
case data
|
|
case widgetId
|
|
case requestId
|
|
}
|
|
}
|
|
|
|
class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriverProtocol {
|
|
private let room: RoomProtocol
|
|
private let deviceID: String
|
|
|
|
private var widgetDriver: WidgetDriverAndHandle?
|
|
|
|
let widgetID = UUID().uuidString
|
|
let messagePublisher = PassthroughSubject<String, Never>()
|
|
|
|
private let actionsSubject: PassthroughSubject<ElementCallWidgetDriverAction, Never> = .init()
|
|
var actions: AnyPublisher<ElementCallWidgetDriverAction, Never> {
|
|
actionsSubject.eraseToAnyPublisher()
|
|
}
|
|
|
|
init(room: RoomProtocol, deviceID: String) {
|
|
self.room = room
|
|
self.deviceID = deviceID
|
|
}
|
|
|
|
func start(baseURL: URL,
|
|
clientID: String,
|
|
colorScheme: ColorScheme,
|
|
rageshakeURL: String?,
|
|
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> {
|
|
guard let room = room as? Room else {
|
|
return .failure(.roomInvalid)
|
|
}
|
|
|
|
async let useEncryption = (try? room.latestEncryptionState() == .encrypted) ?? false
|
|
async let intent = room.joinCallIntent
|
|
|
|
let widgetSettings: WidgetSettings
|
|
do {
|
|
widgetSettings = try await newVirtualElementCallWidget(props: .init(elementCallUrl: baseURL.absoluteString,
|
|
widgetId: widgetID,
|
|
parentUrl: nil,
|
|
fontScale: nil,
|
|
font: nil,
|
|
encryption: useEncryption ? .perParticipantKeys : .unencrypted,
|
|
posthogUserId: nil,
|
|
posthogApiHost: analyticsConfiguration?.posthogAPIHost,
|
|
posthogApiKey: analyticsConfiguration?.posthogAPIKey,
|
|
rageshakeSubmitUrl: rageshakeURL,
|
|
sentryDsn: analyticsConfiguration?.sentryDSN,
|
|
sentryEnvironment: nil),
|
|
config: .init(intent: intent,
|
|
skipLobby: nil,
|
|
header: nil,
|
|
hideHeader: nil,
|
|
preload: nil,
|
|
appPrompt: nil,
|
|
confineToRoom: nil,
|
|
hideScreensharing: nil,
|
|
controlledAudioDevices: nil,
|
|
sendNotificationType: nil))
|
|
} catch {
|
|
MXLog.error("Failed to build widget settings: \(error)")
|
|
return .failure(.failedBuildingWidgetSettings)
|
|
}
|
|
|
|
let languageTag = "\(Locale.current.language.languageCode ?? "en")-\(Locale.current.language.region ?? "US")"
|
|
let theme = colorScheme == .light ? "light" : "dark"
|
|
|
|
let urlString: String
|
|
do {
|
|
urlString = try await generateWebviewUrl(widgetSettings: widgetSettings, room: room,
|
|
props: .init(clientId: clientID,
|
|
languageTag: languageTag,
|
|
theme: theme))
|
|
} catch {
|
|
MXLog.error("Failed to generate web view URL: \(error)")
|
|
return .failure(.failedBuildingCallURL)
|
|
}
|
|
|
|
guard let url = URL(string: urlString) else {
|
|
return .failure(.failedParsingCallURL)
|
|
}
|
|
|
|
let widgetDriver: WidgetDriverAndHandle
|
|
do {
|
|
widgetDriver = try makeWidgetDriver(settings: widgetSettings)
|
|
} catch {
|
|
MXLog.error("Failed to build widget driver: \(error)")
|
|
return .failure(.failedBuildingWidgetDriver)
|
|
}
|
|
|
|
self.widgetDriver = widgetDriver
|
|
|
|
Task.detached { [weak self, widgetDriver, messagePublisher] in
|
|
MXLog.debug("Started message receiving loop")
|
|
|
|
defer {
|
|
MXLog.debug("Stopped message receiving loop")
|
|
}
|
|
|
|
while true {
|
|
guard let receivedMessage = await widgetDriver.handle.recv() else {
|
|
return
|
|
}
|
|
|
|
messagePublisher.send(receivedMessage)
|
|
MXLog.debug("Received message: \(receivedMessage)")
|
|
|
|
self?.handleMessageIfNeeded(receivedMessage)
|
|
}
|
|
}
|
|
|
|
Task.detached { [widgetDriver] in
|
|
MXLog.debug("Started widget driver")
|
|
|
|
defer {
|
|
MXLog.debug("Stopped widget driver")
|
|
}
|
|
|
|
await widgetDriver.driver.run(room: room, capabilitiesProvider: self)
|
|
}
|
|
|
|
return .success(url)
|
|
}
|
|
|
|
@discardableResult
|
|
func handleMessage(_ message: String) async -> Result<Bool, ElementCallWidgetDriverError> {
|
|
guard let widgetDriver else {
|
|
return .failure(.driverNotSetup)
|
|
}
|
|
|
|
let result = await widgetDriver.handle.send(msg: message)
|
|
MXLog.debug("Sent message: \(message) with result: \(result)")
|
|
|
|
handleMessageIfNeeded(message)
|
|
|
|
return .success(result)
|
|
}
|
|
|
|
// MARK: - WidgetCapabilitiesProvider
|
|
|
|
func acquireCapabilities(capabilities: WidgetCapabilities) -> WidgetCapabilities {
|
|
getElementCallRequiredPermissions(ownUserId: room.ownUserId(), ownDeviceId: deviceID)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
func handleMessageIfNeeded(_ message: String) {
|
|
guard let data = message.data(using: .utf8) else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let widgetMessage = try JSONDecoder().decode(ElementCallWidgetMessage.self, from: data)
|
|
if widgetMessage.direction == .fromWidget {
|
|
switch widgetMessage.action {
|
|
case .hangup:
|
|
break
|
|
case .close:
|
|
actionsSubject.send(.callEnded)
|
|
case .mediaState:
|
|
guard let audioEnabled = widgetMessage.data.audioEnabled,
|
|
let videoEnabled = widgetMessage.data.videoEnabled else {
|
|
MXLog.error("Media state change messages should contain info data")
|
|
return
|
|
}
|
|
|
|
actionsSubject.send(.mediaStateChanged(audioEnabled: audioEnabled, videoEnabled: videoEnabled))
|
|
}
|
|
}
|
|
} catch {
|
|
// Not all actions are supported
|
|
MXLog.verbose("Failed processing widget message with error: \(error)")
|
|
}
|
|
}
|
|
}
|