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 5bd0800fe5
commit df1a407142
19 changed files with 65 additions and 410 deletions

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 */,

View File

@@ -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,
@@ -879,35 +873,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)

View File

@@ -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 }

View File

@@ -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>?
@@ -105,31 +86,6 @@ import SwiftUI
} }
} }
/// 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
func toPresentable() -> AnyView { func toPresentable() -> AnyView {
@@ -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)
}
} }
} }

View File

@@ -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.
} }
} }

View File

@@ -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)

View File

@@ -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.") }

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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,19 +154,13 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
} }
private func setupCall() { private func setupCall() {
switch configuration.kind {
case .genericCallLink(let url):
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):
Task { [weak self] in Task { [weak self] in
guard let self else { return } guard let self else { return }
let baseURL = if let elementCallBaseURLOverride { let baseURL = if let baseURLOverride = configuration.elementCallBaseURLOverride {
elementCallBaseURLOverride baseURLOverride
} else { } else {
elementCallBaseURL configuration.elementCallBaseURL
} }
// We only set the analytics configuration if analytics are enabled // We only set the analytics configuration if analytics are enabled
@@ -192,9 +178,9 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
} }
switch await widgetDriver.start(baseURL: baseURL, switch await widgetDriver.start(baseURL: baseURL,
clientID: clientID, clientID: configuration.clientID,
colorScheme: colorScheme, colorScheme: configuration.colorScheme,
voiceOnly: voiceOnly, voiceOnly: configuration.voiceOnly,
rageshakeURL: rageshakeURL, rageshakeURL: rageshakeURL,
analyticsConfiguration: analyticsConfiguration) { analyticsConfiguration: analyticsConfiguration) {
case .success(let url): case .success(let url):
@@ -209,8 +195,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
return return
} }
await elementCallService.setupCallSession(roomID: roomProxy.id, await elementCallService.setupCallSession(roomID: configuration.roomProxy.id,
roomDisplayName: roomProxy.infoPublisher.value.displayName ?? roomProxy.id) roomDisplayName: configuration.roomProxy.infoPublisher.value.displayName ?? configuration.roomProxy.id)
} }
timeoutTask = Task { [weak self] in timeoutTask = Task { [weak self] in
@@ -224,7 +210,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
timeoutTask = nil timeoutTask = nil
} }
} }
}
/// This should always match the web app value /// This should always match the web app value
private static let earpieceID = "earpiece-id" private static let earpieceID = "earpiece-id"

View File

@@ -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

View File

@@ -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 {
case .genericCallLink(let url):
url.absoluteString
case .roomCall(let roomProxy, _, _, _, _, _, _):
roomProxy.id roomProxy.id
} }
}
} }

View File

@@ -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)
}
}

View File

@@ -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>

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>

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

View File

@@ -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"

View File

@@ -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?) {