Remove support for handling SPA calls within the app. (#5515)

* Remove support for handling SPA call links.

They were a stop-gap solution whilst we were building support for embedded room calling.

* Simplify ElementCallConfiguration now that there is only 1 type of call to handle.

* Remove the unused overlayModule from NavigationRoomCoordinator.
This commit is contained in:
Doug
2026-04-29 19:32:38 +01:00
committed by GitHub
parent dc26e9eb5c
commit 63592e48c1
19 changed files with 65 additions and 410 deletions
-4
View File
@@ -578,7 +578,6 @@
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; }; 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; };
633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA12A7F5EF5C6D0B992869ED /* LiveLocationShareProxy.swift */; }; 633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA12A7F5EF5C6D0B992869ED /* LiveLocationShareProxy.swift */; };
633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */; }; 633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */; };
63780F9DA06573E38A471ECA /* GenericCallLinkWidgetDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */; };
6386EA3C898AD1A4BC1DC8A5 /* TimelineMediaPreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD40B92FCF20165658296AD /* TimelineMediaPreviewModifier.swift */; }; 6386EA3C898AD1A4BC1DC8A5 /* TimelineMediaPreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD40B92FCF20165658296AD /* TimelineMediaPreviewModifier.swift */; };
639A0A27383EC655B0E81E95 /* SpaceScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */; }; 639A0A27383EC655B0E81E95 /* SpaceScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459A3921046977CBF4F3C359 /* SpaceScreenViewModelProtocol.swift */; };
63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; }; 63CDC201A5980F304F6D0A1C /* WaveformInteractionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */; };
@@ -1859,7 +1858,6 @@
284FEEB0789B8894E52A7F34 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; 284FEEB0789B8894E52A7F34 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; }; 287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; };
28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItemContent.swift; sourceTree = "<group>"; }; 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItemContent.swift; sourceTree = "<group>"; };
28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkWidgetDriver.swift; sourceTree = "<group>"; };
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; }; 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = "<group>"; }; 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -5531,7 +5529,6 @@
6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */, 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */,
309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */, 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */,
A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */, A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */,
28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */,
); );
path = ElementCall; path = ElementCall;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -8352,7 +8349,6 @@
5705511EBE083295EF98F998 /* FrequentlyUsedEmoji.swift in Sources */, 5705511EBE083295EF98F998 /* FrequentlyUsedEmoji.swift in Sources */,
46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */, 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */,
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */, F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */,
63780F9DA06573E38A471ECA /* GenericCallLinkWidgetDriver.swift in Sources */,
4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */,
F0DACC95F24128A54CD537E4 /* GlobalSearchScreen.swift in Sources */, F0DACC95F24128A54CD537E4 /* GlobalSearchScreen.swift in Sources */,
9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */, 9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */,
@@ -274,12 +274,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
case .accountProvisioningLink: case .accountProvisioningLink:
handleAppRoute(route, handleAppRoute(route,
windowType: windowType) windowType: windowType)
case .genericCallLink(let url):
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
} else {
presentCallScreen(genericCallLink: url)
}
case .userProfile(let userID): case .userProfile(let userID):
if isExternalURL { if isExternalURL {
handleAppRoute(route, handleAppRoute(route,
@@ -878,35 +872,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
elementCallService.setClientProxy(userSession.clientProxy) elementCallService.setClientProxy(userSession.clientProxy)
} }
private func presentCallScreen(genericCallLink url: URL) {
let configuration = ElementCallConfiguration(genericCallLink: url)
let callScreenCoordinator = CallScreenCoordinator(parameters: .init(elementCallService: elementCallService,
configuration: configuration,
allowPictureInPicture: false,
appSettings: appSettings,
appHooks: appHooks,
analytics: ServiceLocator.shared.analytics))
callScreenCoordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .pictureInPictureIsAvailable:
break
case .pictureInPictureStarted, .pictureInPictureStopped:
// Don't allow PiP when signed out - the user could login at which point we'd
// need to hand over the call from here to the user session flow coordinator.
MXLog.error("Picture in Picture not supported before login.")
case .dismiss:
navigationRootCoordinator.setOverlayCoordinator(nil)
}
}
.store(in: &cancellables)
navigationRootCoordinator.setOverlayCoordinator(callScreenCoordinator, animated: false)
}
private func configureNotificationManager() { private func configureNotificationManager() {
notificationManager.setUserSession(userSession) notificationManager.setUserSession(userSession)
@@ -41,8 +41,6 @@ enum AppRoute: Hashable {
case userProfile(userID: String) case userProfile(userID: String)
/// An Element Call running in a particular room /// An Element Call running in a particular room
case call(roomID: String, isVoiceCall: Bool) case call(roomID: String, isVoiceCall: Bool)
/// An Element Call link generated outside of a chat room.
case genericCallLink(url: URL)
/// The settings screen. /// The settings screen.
case settings case settings
/// The setting screen for key backup. /// The setting screen for key backup.
@@ -84,8 +82,7 @@ struct AppRouteURLParser {
AppGroupURLParser(), AppGroupURLParser(),
MatrixPermalinkParser(), MatrixPermalinkParser(),
ElementWebURLParser(domains: appSettings.elementWebHosts), ElementWebURLParser(domains: appSettings.elementWebHosts),
AccountProvisioningURLParser(domain: appSettings.accountProvisioningHost), AccountProvisioningURLParser(domain: appSettings.accountProvisioningHost)
ElementCallURLParser()
] ]
} }
@@ -135,45 +132,6 @@ private struct AppGroupURLParser: URLParser {
} }
} }
/// The parser for Element Call links. This always returns a `.genericCallLink`.
private struct ElementCallURLParser: URLParser {
private let knownHosts = ["call.element.io"]
private let customSchemeURLQueryParameterName = "url"
func route(from url: URL) -> AppRoute? {
// Element Call not supported, WebRTC not available
// https://github.com/element-hq/element-x-ios/issues/1794
if ProcessInfo.processInfo.isiOSAppOnMac {
return nil
}
// First try processing URLs with custom schemes
if let scheme = url.scheme,
scheme == InfoPlistReader.app.elementCallScheme {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return nil
}
guard let encodedURLString = components.queryItems?.first(where: { $0.name == customSchemeURLQueryParameterName })?.value,
let callURL = URL(string: encodedURLString),
callURL.scheme == "https" // Don't allow URLs from potentially unsafe domains
else {
MXLog.error("Invalid custom scheme call parameters: \(url)")
return nil
}
return .genericCallLink(url: callURL)
}
// Otherwise try to interpret it as an universal link
guard let host = url.host, knownHosts.contains(host) else {
return nil
}
return .genericCallLink(url: url)
}
}
private struct MatrixPermalinkParser: URLParser { private struct MatrixPermalinkParser: URLParser {
func route(from url: URL) -> AppRoute? { func route(from url: URL) -> AppRoute? {
guard let entity = parseMatrixEntityFrom(uri: url.absoluteString) else { return nil } guard let entity = parseMatrixEntityFrom(uri: url.absoluteString) else { return nil }
@@ -48,25 +48,6 @@ import SwiftUI
sheetModule?.coordinator sheetModule?.coordinator
} }
fileprivate var overlayModule: NavigationModule? {
didSet {
if let oldValue {
logPresentationChange("Remove overlay", oldValue)
oldValue.tearDown()
}
if let overlayModule {
logPresentationChange("Set overlay", overlayModule)
overlayModule.coordinator?.start()
}
}
}
/// The currently displayed overlay coordinator
var overlayCoordinator: (any CoordinatorProtocol)? {
overlayModule?.coordinator
}
/// The lowest-level `AlertInfo`, directly available to the root of the app. /// The lowest-level `AlertInfo`, directly available to the root of the app.
var alertInfo: AlertInfo<UUID>? var alertInfo: AlertInfo<UUID>?
@@ -104,31 +85,6 @@ import SwiftUI
sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
} }
} }
/// Present an overlay on top of the split view
/// - Parameters:
/// - coordinator: the coordinator to display
/// - animated: whether the transition should be animated
/// - dismissalCallback: called when the overlay has been dismissed, programatically or otherwise
func setOverlayCoordinator(_ coordinator: (any CoordinatorProtocol)?,
animated: Bool = true,
dismissalCallback: (() -> Void)? = nil) {
guard let coordinator else {
overlayModule = nil
return
}
if overlayModule?.coordinator === coordinator {
fatalError("Cannot use the same coordinator more than once")
}
var transaction = Transaction()
transaction.disablesAnimations = !animated
withTransaction(transaction) {
overlayModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback)
}
}
// MARK: - CoordinatorProtocol // MARK: - CoordinatorProtocol
@@ -167,15 +123,5 @@ private struct NavigationRootCoordinatorView: View {
.sheet(item: $rootCoordinator.sheetModule) { module in .sheet(item: $rootCoordinator.sheetModule) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
} }
.accessibilityHidden(rootCoordinator.overlayModule?.coordinator != nil)
.overlay {
Group {
if let coordinator = rootCoordinator.overlayModule?.coordinator {
coordinator.toPresentable()
.transition(.opacity)
}
}
.animation(.elementDefault, value: rootCoordinator.overlayModule)
}
} }
} }
@@ -172,7 +172,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
} }
case .globalSearch: case .globalSearch:
presentGlobalSearch() presentGlobalSearch()
case .accountProvisioningLink, .settings, .chatBackupSettings, .call, .genericCallLink: case .accountProvisioningLink, .settings, .chatBackupSettings, .call:
break // These routes cannot be handled. break // These routes cannot be handled.
} }
} }
@@ -83,7 +83,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol {
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
.roomDetails, .roomMemberDetails, .userProfile, .thread, .roomDetails, .roomMemberDetails, .userProfile, .thread,
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
.call, .genericCallLink, .settings, .share, .transferOwnership, .call, .settings, .share, .transferOwnership,
.globalSearch: .globalSearch:
// These routes aren't in this flow so clear the entire stack. // These routes aren't in this flow so clear the entire stack.
clearRoute(animated: animated) clearRoute(animated: animated)
@@ -199,7 +199,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
break // These are converted to a room ID route one level above. break // These are converted to a room ID route one level above.
case .accountProvisioningLink, .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings, .globalSearch: case .accountProvisioningLink, .roomList, .userProfile, .call, .settings, .chatBackupSettings, .globalSearch:
break // These routes can't be handled. break // These routes can't be handled.
case .transferOwnership(let roomID): case .transferOwnership(let roomID):
guard self.roomID == roomID else { fatalError("Navigation route doesn't belong to this room flow.") } guard self.roomID == roomID else { fatalError("Navigation route doesn't belong to this room flow.") }
@@ -119,7 +119,7 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol {
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
break // These are converted to a room ID route one level above. break // These are converted to a room ID route one level above.
case .accountProvisioningLink, .roomList, .room, .roomDetails, .event, case .accountProvisioningLink, .roomList, .room, .roomDetails, .event,
.userProfile, .call, .genericCallLink, .settings, .chatBackupSettings, .userProfile, .call, .settings, .chatBackupSettings,
.share, .transferOwnership, .thread, .globalSearch: .share, .transferOwnership, .thread, .globalSearch:
break break
} }
@@ -132,8 +132,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
case .call(let roomID, let isVoiceCall): case .call(let roomID, let isVoiceCall):
Task { await presentCallScreen(roomID: roomID, isVoiceCall: isVoiceCall) } Task { await presentCallScreen(roomID: roomID, isVoiceCall: isVoiceCall) }
case .genericCallLink(let url):
presentCallScreen(genericCallLink: url)
case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias,
.roomDetails, .roomMemberDetails, .userProfile, .roomDetails, .roomMemberDetails, .userProfile,
.event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias,
@@ -401,10 +399,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Calls // MARK: - Calls
private func presentCallScreen(genericCallLink url: URL) {
presentCallScreen(configuration: .init(genericCallLink: url))
}
private func presentCallScreen(roomID: String, isVoiceCall: Bool) async { private func presentCallScreen(roomID: String, isVoiceCall: Bool) async {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else {
return return
@@ -19,7 +19,6 @@ enum CallScreenViewModelAction {
struct CallScreenViewState: BindableState { struct CallScreenViewState: BindableState {
let script: String? let script: String?
var url: URL? var url: URL?
let isGenericCallLink: Bool
let certificateValidator: CertificateValidatorHookProtocol let certificateValidator: CertificateValidatorHookProtocol
@@ -48,18 +48,10 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
self.analyticsService = analyticsService self.analyticsService = analyticsService
isPictureInPictureAllowed = allowPictureInPicture isPictureInPictureAllowed = allowPictureInPicture
var isGenericCallLink = false guard let deviceID = configuration.clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
switch configuration.kind { widgetDriver = configuration.roomProxy.elementCallWidgetDriver(deviceID: deviceID)
case .genericCallLink(let url):
widgetDriver = GenericCallLinkWidgetDriver(url: url)
isGenericCallLink = true
case .roomCall(let roomProxy, let clientProxy, _, _, _, _, _):
guard let deviceID = clientProxy.deviceID else { fatalError("Missing device ID for the call.") }
widgetDriver = roomProxy.elementCallWidgetDriver(deviceID: deviceID)
}
super.init(initialViewState: CallScreenViewState(script: CallScreenJavaScriptMessageName.allCasesInjectionScript, super.init(initialViewState: CallScreenViewState(script: CallScreenJavaScriptMessageName.allCasesInjectionScript,
isGenericCallLink: isGenericCallLink,
certificateValidator: appHooks.certificateValidatorHook)) certificateValidator: appHooks.certificateValidatorHook))
elementCallService.actions elementCallService.actions
@@ -162,67 +154,60 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
} }
private func setupCall() { private func setupCall() {
switch configuration.kind { Task { [weak self] in
case .genericCallLink(let url): guard let self else { return }
state.url = url
// We need widget messaging to work before enabling CallKit, otherwise mute, hangup etc do nothing.
case .roomCall(let roomProxy, _, let clientID, let voiceOnly, let elementCallBaseURL, let elementCallBaseURLOverride, let colorScheme): let baseURL = if let baseURLOverride = configuration.elementCallBaseURLOverride {
Task { [weak self] in baseURLOverride
guard let self else { return } } else {
configuration.elementCallBaseURL
let baseURL = if let elementCallBaseURLOverride {
elementCallBaseURLOverride
} else {
elementCallBaseURL
}
// We only set the analytics configuration if analytics are enabled
let analyticsConfiguration: ElementCallAnalyticsConfiguration? = if analyticsService.isEnabled {
.init(posthogAPIHost: appSettings.elementCallPosthogAPIHost,
posthogAPIKey: appSettings.elementCallPosthogAPIKey,
sentryDSN: appSettings.elementCallPosthogSentryDSN)
} else {
nil
}
let rageshakeURL: String? = if case let .url(baseURL) = appSettings.bugReportRageshakeURL.publisher.value {
baseURL.absoluteString
} else {
nil
}
switch await widgetDriver.start(baseURL: baseURL,
clientID: clientID,
colorScheme: colorScheme,
voiceOnly: voiceOnly,
rageshakeURL: rageshakeURL,
analyticsConfiguration: analyticsConfiguration) {
case .success(let url):
state.url = url
case .failure(let error):
MXLog.error("Failed starting ElementCall Widget Driver with error: \(error)")
state.bindings.alertInfo = .init(id: UUID(),
title: L10n.errorUnknown,
primaryButton: .init(title: L10n.actionOk) {
self.actionsSubject.send(.dismiss)
})
return
}
await elementCallService.setupCallSession(roomID: roomProxy.id,
roomDisplayName: roomProxy.infoPublisher.value.displayName ?? roomProxy.id)
} }
timeoutTask = Task { [weak self] in // We only set the analytics configuration if analytics are enabled
try? await Task.sleep(for: .seconds(10)) let analyticsConfiguration: ElementCallAnalyticsConfiguration? = if analyticsService.isEnabled {
guard !Task.isCancelled, let self else { return } .init(posthogAPIHost: appSettings.elementCallPosthogAPIHost,
MXLog.error("Failed to join Element Call: Timeout") posthogAPIKey: appSettings.elementCallPosthogAPIKey,
sentryDSN: appSettings.elementCallPosthogSentryDSN)
} else {
nil
}
let rageshakeURL: String? = if case let .url(baseURL) = appSettings.bugReportRageshakeURL.publisher.value {
baseURL.absoluteString
} else {
nil
}
switch await widgetDriver.start(baseURL: baseURL,
clientID: configuration.clientID,
colorScheme: configuration.colorScheme,
voiceOnly: configuration.voiceOnly,
rageshakeURL: rageshakeURL,
analyticsConfiguration: analyticsConfiguration) {
case .success(let url):
state.url = url
case .failure(let error):
MXLog.error("Failed starting ElementCall Widget Driver with error: \(error)")
state.bindings.alertInfo = .init(id: UUID(), state.bindings.alertInfo = .init(id: UUID(),
title: L10n.commonError, title: L10n.errorUnknown,
message: L10n.errorUnknown, primaryButton: .init(title: L10n.actionOk) {
primaryButton: .init(title: L10n.actionDismiss) { [weak self] in self?.actionsSubject.send(.dismiss) }) self.actionsSubject.send(.dismiss)
timeoutTask = nil })
return
} }
await elementCallService.setupCallSession(roomID: configuration.roomProxy.id,
roomDisplayName: configuration.roomProxy.infoPublisher.value.displayName ?? configuration.roomProxy.id)
}
timeoutTask = Task { [weak self] in
try? await Task.sleep(for: .seconds(10))
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
} }
} }
@@ -22,11 +22,10 @@ struct CallScreen: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar(context.viewState.isGenericCallLink ? .visible : .hidden, for: .navigationBar) .toolbar(.hidden, for: .navigationBar)
.toolbar { toolbar } .toolbar { toolbar }
} }
.alert(item: $context.alertInfo) .alert(item: $context.alertInfo)
.preferredColorScheme(context.viewState.isGenericCallLink ? .dark : nil)
} }
@ViewBuilder @ViewBuilder
@@ -8,76 +8,18 @@
import SwiftUI import SwiftUI
private enum GenericCallLinkQueryParameters {
static let appPrompt = "appPrompt"
static let confineToRoom = "confineToRoom"
}
/// Information about how a call should be configured. /// Information about how a call should be configured.
struct ElementCallConfiguration { struct ElementCallConfiguration {
enum Kind { let roomProxy: JoinedRoomProxyProtocol
case genericCallLink(URL) let clientProxy: ClientProxyProtocol
case roomCall(roomProxy: JoinedRoomProxyProtocol, let clientID: String
clientProxy: ClientProxyProtocol, let elementCallBaseURL: URL
clientID: String, let elementCallBaseURLOverride: URL?
voiceOnly: Bool, let voiceOnly: Bool
elementCallBaseURL: URL, let colorScheme: ColorScheme
elementCallBaseURLOverride: URL?,
colorScheme: ColorScheme)
}
/// The type of call being configured i.e. whether it's an external URL or an internal room call.
let kind: Kind
/// Creates a configuration for an external call URL.
init(genericCallLink url: URL) {
if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) {
var fragmentQueryItems = urlComponents.fragmentQueryItems ?? []
fragmentQueryItems.removeAll { $0.name == GenericCallLinkQueryParameters.appPrompt }
fragmentQueryItems.removeAll { $0.name == GenericCallLinkQueryParameters.confineToRoom }
fragmentQueryItems.append(.init(name: GenericCallLinkQueryParameters.appPrompt, value: "false"))
fragmentQueryItems.append(.init(name: GenericCallLinkQueryParameters.confineToRoom, value: "true"))
urlComponents.fragmentQueryItems = fragmentQueryItems
if let adjustedURL = urlComponents.url {
kind = .genericCallLink(adjustedURL)
} else {
MXLog.error("Failed adjusting URL with components: \(urlComponents)")
kind = .genericCallLink(url)
}
} else {
MXLog.error("Failed constructing URL components for url: \(url)")
kind = .genericCallLink(url)
}
}
/// Creates a configuration for an internal room call.
init(roomProxy: JoinedRoomProxyProtocol,
clientProxy: ClientProxyProtocol,
clientID: String,
elementCallBaseURL: URL,
elementCallBaseURLOverride: URL?,
voiceOnly: Bool,
colorScheme: ColorScheme) {
kind = .roomCall(roomProxy: roomProxy,
clientProxy: clientProxy,
clientID: clientID,
voiceOnly: voiceOnly,
elementCallBaseURL: elementCallBaseURL,
elementCallBaseURLOverride: elementCallBaseURLOverride,
colorScheme: colorScheme)
}
/// A string representing the call being configured. /// A string representing the call being configured.
var callRoomID: String { var callRoomID: String {
switch kind { roomProxy.id
case .genericCallLink(let url):
url.absoluteString
case .roomCall(let roomProxy, _, _, _, _, _, _):
roomProxy.id
}
} }
} }
@@ -1,41 +0,0 @@
//
// Copyright 2025 Element Creations Ltd.
// Copyright 2024-2025 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 SwiftUI
class GenericCallLinkWidgetDriver: ElementCallWidgetDriverProtocol {
private let url: URL
let widgetID = UUID().uuidString
let messagePublisher = PassthroughSubject<String, Never>()
private let actionsSubject: PassthroughSubject<ElementCallWidgetDriverAction, Never> = .init()
var actions: AnyPublisher<ElementCallWidgetDriverAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(url: URL) {
self.url = url
}
func start(baseURL: URL,
clientID: String,
colorScheme: ColorScheme,
voiceOnly: Bool,
rageshakeURL: String?,
analyticsConfiguration: ElementCallAnalyticsConfiguration?) async -> Result<URL, ElementCallWidgetDriverError> {
MXLog.error("Nothing to start, use the configuration's URL directly instead.")
return .success(url)
}
func handleMessage(_ message: String) async -> Result<Bool, ElementCallWidgetDriverError> {
// The web view doesn't send us messages through the Widget API, so nothing to implement (yet?).
.failure(.driverNotSetup)
}
}
@@ -11,8 +11,6 @@
<string>applinks:staging.element.io</string> <string>applinks:staging.element.io</string>
<string>applinks:develop.element.io</string> <string>applinks:develop.element.io</string>
<string>applinks:mobile.element.io</string> <string>applinks:mobile.element.io</string>
<string>applinks:call.element.io</string>
<string>applinks:call.element.dev</string>
<string>applinks:matrix.to</string> <string>applinks:matrix.to</string>
<string>webcredentials:*.element.io</string> <string>webcredentials:*.element.io</string>
</array> </array>
-10
View File
@@ -37,16 +37,6 @@
<string>$(MARKETING_VERSION)</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>Element Call</string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.element.call</string>
</array>
</dict>
<dict> <dict>
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
-9
View File
@@ -51,13 +51,6 @@ targets:
CFBundleShortVersionString: $(MARKETING_VERSION) CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION) CFBundleVersion: $(CURRENT_PROJECT_VERSION)
CFBundleURLTypes: [ CFBundleURLTypes: [
{
CFBundleTypeRole: Editor,
CFBundleURLName: "Element Call",
CFBundleURLSchemes: [
io.element.call
]
},
{ {
CFBundleTypeRole: Editor, CFBundleTypeRole: Editor,
CFBundleURLName: "Application", CFBundleURLName: "Application",
@@ -126,8 +119,6 @@ targets:
- applinks:staging.element.io - applinks:staging.element.io
- applinks:develop.element.io - applinks:develop.element.io
- applinks:mobile.element.io - applinks:mobile.element.io
- applinks:call.element.io
- applinks:call.element.dev
- applinks:matrix.to - applinks:matrix.to
# - applinks:localhost?mode=developer # developer mode only works with https (but self-signed is fine). # - applinks:localhost?mode=developer # developer mode only works with https (but self-signed is fine).
- webcredentials:*.element.io - webcredentials:*.element.io
@@ -20,43 +20,6 @@ struct AppRouteURLParserTests {
appRouteURLParser = AppRouteURLParser(appSettings: appSettings) appRouteURLParser = AppRouteURLParser(appSettings: appSettings)
} }
@Test
func elementCallRoutes() throws {
let url = try #require(URL(string: "https://call.element.io/test"))
#expect(appRouteURLParser.route(from: url) == AppRoute.genericCallLink(url: url))
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=https%3A%2F%2Fcall.element.io%2Ftest"))
#expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
}
@Test
func customDomainUniversalLinkCallRoutes() throws {
let url = try #require(URL(string: "https://somecustomdomain.element.io/test"))
#expect(appRouteURLParser.route(from: url) == nil)
}
@Test
func customSchemeLinkCallRoutes() throws {
let urlString = "https://somecustomdomain.element.io/test?param=123"
let url = try #require(URL(string: urlString))
let encodedURLString = try #require(urlString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed))
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=\(encodedURLString)"))
#expect(appRouteURLParser.route(from: customSchemeURL) == AppRoute.genericCallLink(url: url))
}
@Test
func httpCustomSchemeLinkCallRoutes() throws {
let customSchemeURL = try #require(URL(string: "io.element.call:/?url=http%3A%2F%2Fcall.element.io%2Ftest"))
#expect(appRouteURLParser.route(from: customSchemeURL) == nil)
}
@Test @Test
func matrixUserURL() throws { func matrixUserURL() throws {
let userID = "@test:matrix.org" let userID = "@test:matrix.org"
@@ -33,23 +33,6 @@ struct NavigationRootCoordinatorTests {
assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator) assertCoordinatorsEqual(secondRootCoordinator, navigationRootCoordinator.rootCoordinator)
} }
@Test
func overlay() {
let rootCoordinator = SomeTestCoordinator()
navigationRootCoordinator.setRootCoordinator(rootCoordinator)
let overlayCoordinator = SomeTestCoordinator()
navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator)
assertCoordinatorsEqual(rootCoordinator, navigationRootCoordinator.rootCoordinator)
assertCoordinatorsEqual(overlayCoordinator, navigationRootCoordinator.overlayCoordinator)
navigationRootCoordinator.setOverlayCoordinator(nil)
assertCoordinatorsEqual(rootCoordinator, navigationRootCoordinator.rootCoordinator)
#expect(navigationRootCoordinator.overlayCoordinator == nil)
}
// MARK: - Dismissal Callbacks // MARK: - Dismissal Callbacks
@Test @Test
@@ -67,19 +50,6 @@ struct NavigationRootCoordinatorTests {
} }
} }
@Test
func overlayDismissalCallback() async {
let overlayCoordinator = SomeTestCoordinator()
await confirmation("Wait for callback") { confirm in
navigationRootCoordinator.setOverlayCoordinator(overlayCoordinator) {
confirm()
}
navigationRootCoordinator.setOverlayCoordinator(nil)
}
}
// MARK: - Private // MARK: - Private
private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) { private func assertCoordinatorsEqual(_ lhs: CoordinatorProtocol?, _ rhs: CoordinatorProtocol?) {