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:
@@ -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,
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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>?
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
case .genericCallLink(let url):
|
|
||||||
url.absoluteString
|
|
||||||
case .roomCall(let roomProxy, _, _, _, _, _, _):
|
|
||||||
roomProxy.id
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
Reference in New Issue
Block a user