mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Handle notifications properly when a call is happening. (#3276)
This commit is contained in:
@@ -643,6 +643,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
|
case .pictureInPictureIsAvailable:
|
||||||
|
break
|
||||||
case .pictureInPictureStarted, .pictureInPictureStopped:
|
case .pictureInPictureStarted, .pictureInPictureStopped:
|
||||||
// Don't allow PiP when signed out - the user could login at which point we'd
|
// 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.
|
// need to hand over the call from here to the user session flow coordinator.
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
} else {
|
} else {
|
||||||
Task { await self.startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated) }
|
Task { await self.startRoomFlow(roomID: roomID, via: via, entryPoint: entryPoint, animated: animated) }
|
||||||
}
|
}
|
||||||
|
hideCallScreenOverlay() // Turn any active call into a PiP so that navigation from a notification is visible to the user.
|
||||||
case(.roomList, .deselectRoom, .roomList):
|
case(.roomList, .deselectRoom, .roomList):
|
||||||
dismissRoomFlow(animated: animated)
|
dismissRoomFlow(animated: animated)
|
||||||
|
|
||||||
@@ -604,15 +605,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
.sink { [weak self] action in
|
.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .pictureInPictureStarted(let controller):
|
case .pictureInPictureIsAvailable(let controller):
|
||||||
MXLog.info("Hiding call for PiP presentation.")
|
|
||||||
callScreenPictureInPictureController = controller
|
callScreenPictureInPictureController = controller
|
||||||
|
case .pictureInPictureStarted:
|
||||||
|
MXLog.info("Hiding call for PiP presentation.")
|
||||||
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
||||||
case .pictureInPictureStopped:
|
case .pictureInPictureStopped:
|
||||||
MXLog.info("Restoring call after PiP presentation.")
|
MXLog.info("Restoring call after PiP presentation.")
|
||||||
navigationSplitCoordinator.setOverlayPresentationMode(.fullScreen)
|
navigationSplitCoordinator.setOverlayPresentationMode(.fullScreen)
|
||||||
callScreenPictureInPictureController = nil
|
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
|
callScreenPictureInPictureController = nil
|
||||||
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,12 +625,24 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
analytics.track(screen: .RoomCall)
|
analytics.track(screen: .RoomCall)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dismissCallScreenIfNeeded() {
|
private func hideCallScreenOverlay() {
|
||||||
guard navigationSplitCoordinator.sheetCoordinator is CallScreenCoordinator else {
|
guard let callScreenPictureInPictureController else {
|
||||||
|
MXLog.warning("Picture in picture isn't available, dismissing the call screen.")
|
||||||
|
dismissCallScreenIfNeeded()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationSplitCoordinator.setSheetCoordinator(nil)
|
MXLog.info("Starting picture in picture to hide the call screen overlay.")
|
||||||
|
callScreenPictureInPictureController.startPictureInPicture()
|
||||||
|
navigationSplitCoordinator.setOverlayPresentationMode(.minimized)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissCallScreenIfNeeded() {
|
||||||
|
guard navigationSplitCoordinator.overlayCoordinator is CallScreenCoordinator else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationSplitCoordinator.setOverlayCoordinator(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Secure backup confirmation
|
// MARK: Secure backup confirmation
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ struct CallScreenCoordinatorParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum CallScreenCoordinatorAction {
|
enum CallScreenCoordinatorAction {
|
||||||
/// The call is still ongoing but the user wishes to navigate around the app.
|
/// The call is able to be minimised to picture in picture with the provided controller.
|
||||||
case pictureInPictureStarted(AVPictureInPictureController)
|
///
|
||||||
|
/// **Note:** Manually starting the PiP will not trigger the action below as we don't want
|
||||||
|
/// to change the app's navigation when backgrounding the app with the call screen visible.
|
||||||
|
case pictureInPictureIsAvailable(AVPictureInPictureController)
|
||||||
|
/// The call is still ongoing but the user requested to navigate around the app.
|
||||||
|
case pictureInPictureStarted
|
||||||
/// The call is hidden and the user wishes to return to it.
|
/// The call is hidden and the user wishes to return to it.
|
||||||
case pictureInPictureStopped
|
case pictureInPictureStopped
|
||||||
/// The call is finished and the screen is done with.
|
/// The call is finished and the screen is done with.
|
||||||
@@ -46,8 +51,10 @@ final class CallScreenCoordinator: CoordinatorProtocol {
|
|||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case .pictureInPictureStarted(let controller):
|
case .pictureInPictureIsAvailable(let controller):
|
||||||
actionsSubject.send(.pictureInPictureStarted(controller))
|
actionsSubject.send(.pictureInPictureIsAvailable(controller))
|
||||||
|
case .pictureInPictureStarted:
|
||||||
|
actionsSubject.send(.pictureInPictureStarted)
|
||||||
case .pictureInPictureStopped:
|
case .pictureInPictureStopped:
|
||||||
actionsSubject.send(.pictureInPictureStopped)
|
actionsSubject.send(.pictureInPictureStopped)
|
||||||
case .dismiss:
|
case .dismiss:
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import AVKit
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum CallScreenViewModelAction {
|
enum CallScreenViewModelAction {
|
||||||
case pictureInPictureStarted(AVPictureInPictureController)
|
case pictureInPictureIsAvailable(AVPictureInPictureController)
|
||||||
|
case pictureInPictureStarted
|
||||||
case pictureInPictureStopped
|
case pictureInPictureStopped
|
||||||
case dismiss
|
case dismiss
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,7 @@ struct Bindings {
|
|||||||
|
|
||||||
enum CallScreenViewAction {
|
enum CallScreenViewAction {
|
||||||
case urlChanged(URL?)
|
case urlChanged(URL?)
|
||||||
|
case pictureInPictureIsAvailable(AVPictureInPictureController)
|
||||||
case navigateBack
|
case navigateBack
|
||||||
case pictureInPictureWillStop
|
case pictureInPictureWillStop
|
||||||
case endCall
|
case endCall
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
case .urlChanged(let url):
|
case .urlChanged(let url):
|
||||||
guard let url else { return }
|
guard let url else { return }
|
||||||
MXLog.info("URL changed to: \(url)")
|
MXLog.info("URL changed to: \(url)")
|
||||||
|
case .pictureInPictureIsAvailable(let controller):
|
||||||
|
actionsSubject.send(.pictureInPictureIsAvailable(controller))
|
||||||
case .navigateBack:
|
case .navigateBack:
|
||||||
Task { await handleBackwardsNavigation() }
|
Task { await handleBackwardsNavigation() }
|
||||||
case .pictureInPictureWillStop:
|
case .pictureInPictureWillStop:
|
||||||
@@ -188,7 +190,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
|
|||||||
|
|
||||||
switch await requestPictureInPictureHandler() {
|
switch await requestPictureInPictureHandler() {
|
||||||
case .success(let controller):
|
case .success(let controller):
|
||||||
actionsSubject.send(.pictureInPictureStarted(controller))
|
actionsSubject.send(.pictureInPictureStarted)
|
||||||
case .failure:
|
case .failure:
|
||||||
actionsSubject.send(.dismiss)
|
actionsSubject.send(.dismiss)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ private struct CallView: UIViewRepresentable {
|
|||||||
contentViewController: pictureInPictureViewController))
|
contentViewController: pictureInPictureViewController))
|
||||||
pictureInPictureController.delegate = self
|
pictureInPictureController.delegate = self
|
||||||
self.pictureInPictureController = pictureInPictureController
|
self.pictureInPictureController = pictureInPictureController
|
||||||
|
viewModelContext.send(viewAction: .pictureInPictureIsAvailable(pictureInPictureController))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +223,18 @@ private struct CallView: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||||
|
Task { @MainActor in
|
||||||
|
// Double check that the controller is definitely showing a page that supports picture in picture.
|
||||||
|
// This is necessary as it doesn't get checked when backgrounding the app or tapping a notification.
|
||||||
|
guard case .success(true) = await webViewCanEnterPictureInPicture() else {
|
||||||
|
MXLog.error("Picture in picture started on a webpage that doesn't support it. Ending the call.")
|
||||||
|
viewModelContext?.send(viewAction: .endCall)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nonisolated func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
nonisolated func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||||
Task { await viewModelContext?.send(viewAction: .pictureInPictureWillStop) }
|
Task { await viewModelContext?.send(viewAction: .pictureInPictureWillStop) }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user