Use native video call picture in picture! (#3190)

* Use native video call picture in picture!

* Handle isSupported, isPossible and web view errors.

* Use new canEnterPip method.

* Hide a room's Call button when already joined to the call.

* Tweak feature flag description.

* PR comments.
This commit is contained in:
Doug
2024-08-21 17:48:57 +01:00
committed by GitHub
parent 4e3ab9c04a
commit 304070e8cc
23 changed files with 256 additions and 66 deletions
@@ -47,6 +47,7 @@ final class AppSettings {
case publicSearchEnabled
case fuzzyRoomListSearchEnabled
case pinningEnabled
case elementCallPictureInPictureEnabled
}
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@@ -285,8 +286,8 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.pinningEnabled, defaultValue: false, storageType: .userDefaults(store))
var pinningEnabled
// Not user configurable as it depends on work in EC too.
let elementCallPictureInPictureEnabled = false
@UserPreference(key: UserDefaultsKeys.elementCallPictureInPictureEnabled, defaultValue: false, storageType: .userDefaults(store))
var elementCallPictureInPictureEnabled
#endif
@@ -58,6 +58,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let emojiProvider: EmojiProviderProtocol
private let ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
@@ -90,6 +91,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
emojiProvider: EmojiProviderProtocol,
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
@@ -100,6 +102,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.navigationStackCoordinator = navigationStackCoordinator
self.emojiProvider = emojiProvider
self.ongoingCallRoomIDPublisher = ongoingCallRoomIDPublisher
self.appMediator = appMediator
self.appSettings = appSettings
self.analytics = analytics
@@ -580,6 +583,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
emojiProvider: emojiProvider,
completionSuggestionService: completionSuggestionService,
ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher,
appMediator: appMediator,
appSettings: appSettings,
composerDraftService: composerDraftService)
@@ -1350,6 +1354,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: emojiProvider,
ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher,
appMediator: appMediator,
appSettings: appSettings,
analytics: analytics,
@@ -477,6 +477,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
emojiProvider: EmojiProvider(),
ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher,
appMediator: appMediator,
appSettings: appSettings,
analytics: analytics,
@@ -580,7 +581,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private var callScreenPictureInPictureController: AVPictureInPictureController?
private func presentCallScreen(configuration: ElementCallConfiguration) {
guard elementCallService.ongoingCallRoomID != configuration.callID else {
guard elementCallService.ongoingCallRoomIDPublisher.value != configuration.callRoomID else {
MXLog.info("Returning to existing call.")
callScreenPictureInPictureController?.stopPictureInPicture()
return
@@ -17,12 +17,15 @@
import Combine
import Foundation
struct ElementCallServiceMockConfiguration { }
struct ElementCallServiceMockConfiguration {
var ongoingCallRoomID: String?
}
extension ElementCallServiceMock {
convenience init(_ configuration: ElementCallServiceMockConfiguration) {
self.init()
underlyingActions = PassthroughSubject().eraseToAnyPublisher()
underlyingOngoingCallRoomIDPublisher = .init(.init(configuration.ongoingCallRoomID))
}
}
@@ -4894,7 +4894,11 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<ElementCallServiceAction, Never>!
var ongoingCallRoomID: String?
var ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never> {
get { return underlyingOngoingCallRoomIDPublisher }
set(value) { underlyingOngoingCallRoomIDPublisher = value }
}
var underlyingOngoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>!
//MARK: - setClientProxy
@@ -0,0 +1,30 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
extension UIView {
func addMatchedSubview(_ subview: UIView) {
addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
subview.topAnchor.constraint(equalTo: topAnchor),
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
@@ -27,7 +27,7 @@ struct CallScreenCoordinatorParameters {
enum CallScreenCoordinatorAction {
/// The call is still ongoing but the user wishes to navigate around the app.
case pictureInPictureStarted(AVPictureInPictureController?)
case pictureInPictureStarted(AVPictureInPictureController)
/// The call is hidden and the user wishes to return to it.
case pictureInPictureStopped
/// The call is finished and the screen is done with.
@@ -18,7 +18,7 @@ import AVKit
import Foundation
enum CallScreenViewModelAction {
case pictureInPictureStarted(AVPictureInPictureController?)
case pictureInPictureStarted(AVPictureInPictureController)
case pictureInPictureStopped
case dismiss
}
@@ -36,6 +36,7 @@ struct CallScreenViewState: BindableState {
struct Bindings {
var javaScriptMessageHandler: ((Any) -> Void)?
var javaScriptEvaluator: ((String) async throws -> Any)?
var requestPictureInPictureHandler: (() async -> Result<AVPictureInPictureController, CallScreenError>)?
var alertInfo: AlertInfo<UUID>?
}
@@ -43,4 +44,10 @@ struct Bindings {
enum CallScreenViewAction {
case urlChanged(URL?)
case navigateBack
case pictureInPictureWillStop
case endCall
}
enum CallScreenError: Error {
case pictureInPictureNotAvailable
}
@@ -73,8 +73,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
switch action {
case let .setAudioEnabled(enabled, roomID):
guard roomID == configuration.callID else {
MXLog.error("Received mute request for a different room: \(roomID) != \(configuration.callID)")
guard roomID == configuration.callRoomID else {
MXLog.error("Received mute request for a different room: \(roomID) != \(configuration.callRoomID)")
return
}
@@ -107,28 +107,11 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
case .callEnded:
actionsSubject.send(.dismiss)
case .mediaStateChanged(let audioEnabled, _):
elementCallService.setAudioEnabled(audioEnabled, roomID: configuration.callID)
elementCallService.setAudioEnabled(audioEnabled, roomID: configuration.callRoomID)
}
}
.store(in: &cancellables)
// Use did start otherwise there's a black box left on the screen during the pip controller animation.
NotificationCenter.default.publisher(for: .init("AVPictureInPictureControllerDidStartNotification"))
.sink { [weak self] notification in
guard let self else { return }
let controller = notification.object as? AVPictureInPictureController
actionsSubject.send(.pictureInPictureStarted(controller))
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: .init("AVPictureInPictureControllerWillStopNotification"))
.sink { [weak self] _ in
guard let self else { return }
actionsSubject.send(.pictureInPictureStopped)
Task { try await self.state.bindings.javaScriptEvaluator?("controls.disableCompatPip()") }
}
.store(in: &cancellables)
setupCall()
}
@@ -138,7 +121,11 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
guard let url else { return }
MXLog.info("URL changed to: \(url)")
case .navigateBack:
handleBackwardsNavigation()
Task { await handleBackwardsNavigation() }
case .pictureInPictureWillStop:
actionsSubject.send(.pictureInPictureStopped)
case .endCall:
actionsSubject.send(.dismiss)
}
}
@@ -200,26 +187,19 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
}
}
private func handleBackwardsNavigation() {
#if targetEnvironment(simulator)
if UIDevice.current.isPhone {
MXLog.warning("The iPhone simulator doesn't support PiP.")
actionsSubject.send(.dismiss)
return
}
#endif
guard isPictureInPictureEnabled, state.url != nil else {
private func handleBackwardsNavigation() async {
guard state.url != nil,
isPictureInPictureEnabled,
let requestPictureInPictureHandler = state.bindings.requestPictureInPictureHandler else {
actionsSubject.send(.dismiss)
return
}
Task {
try await state.bindings.javaScriptEvaluator?("controls.enableCompatPip()")
// Enable this check when implemented on web.
// if result as? Bool != true {
// actionsSubject.send(.dismiss)
// }
switch await requestPictureInPictureHandler() {
case .success(let controller):
actionsSubject.send(.pictureInPictureStarted(controller))
case .failure:
actionsSubject.send(.dismiss)
}
}
@@ -14,6 +14,7 @@
// limitations under the License.
//
import AVKit
import Combine
import SFSafeSymbols
import SwiftUI
@@ -46,7 +47,7 @@ struct CallScreen: View {
if context.viewState.url == nil {
ProgressView()
} else {
WebView(url: context.viewState.url, viewModelContext: context)
CallView(url: context.viewState.url, viewModelContext: context)
// This URL is stable, forces view reloads if this representable is ever reused for another url
.id(context.viewState.url)
.ignoresSafeArea(edges: .bottom)
@@ -54,44 +55,52 @@ struct CallScreen: View {
}
}
private struct WebView: UIViewRepresentable {
private struct CallView: UIViewRepresentable {
/// The top-level view this representable displays. It wraps the web view when picture in picture isn't running.
typealias WebViewWrapper = UIView
let url: URL?
let viewModelContext: CallScreenViewModel.Context
func makeUIView(context: Context) -> WKWebView {
context.coordinator.webView
func makeUIView(context: Context) -> WebViewWrapper {
context.coordinator.webViewWrapper
}
func makeCoordinator() -> Coordinator {
Coordinator(viewModelContext: viewModelContext)
}
func updateUIView(_ webView: WKWebView, context: Context) {
func updateUIView(_ callWebView: WebViewWrapper, context: Context) {
if let url {
context.coordinator.url = url
let request = URLRequest(url: url)
webView.load(request)
context.coordinator.load(url)
}
}
@MainActor
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate {
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate, AVPictureInPictureControllerDelegate {
private weak var viewModelContext: CallScreenViewModel.Context?
private let certificateValidator: CertificateValidatorHookProtocol
private(set) var webView: WKWebView!
private var webView: WKWebView!
private var pictureInPictureController: AVPictureInPictureController?
private let pictureInPictureViewController: AVPictureInPictureVideoCallViewController
var url: URL!
/// The view to be shown in the app. This will contain the web view when picture in picture isn't running.
let webViewWrapper = WebViewWrapper(frame: .zero)
private var url: URL!
init(viewModelContext: CallScreenViewModel.Context) {
self.viewModelContext = viewModelContext
certificateValidator = viewModelContext.viewState.certificateValidator
pictureInPictureViewController = AVPictureInPictureVideoCallViewController()
pictureInPictureViewController.preferredContentSize = CGSize(width: 1920, height: 1080)
super.init()
DispatchQueue.main.async {
// Avoid `Publishing changes from within view update warnings`
viewModelContext.javaScriptEvaluator = self.evaluateJavaScript(_:)
DispatchQueue.main.async { // Avoid `Publishing changes from within view update warnings`
viewModelContext.javaScriptEvaluator = self.evaluateJavaScript
viewModelContext.requestPictureInPictureHandler = self.requestPictureInPicture
}
let configuration = WKWebViewConfiguration()
@@ -120,6 +129,21 @@ private struct WebView: UIViewRepresentable {
webView.isOpaque = false
webView.backgroundColor = .compound.bgCanvasDefault
webView.scrollView.backgroundColor = .compound.bgCanvasDefault
webViewWrapper.addMatchedSubview(webView)
if AVPictureInPictureController.isPictureInPictureSupported() {
let pictureInPictureController = AVPictureInPictureController(contentSource: .init(activeVideoCallSourceView: webViewWrapper,
contentViewController: pictureInPictureViewController))
pictureInPictureController.delegate = self
self.pictureInPictureController = pictureInPictureController
}
}
func load(_ url: URL) {
self.url = url
let request = URLRequest(url: url)
webView.load(request)
}
func evaluateJavaScript(_ script: String) async throws -> Any? {
@@ -181,6 +205,57 @@ private struct WebView: UIViewRepresentable {
viewModelContext?.send(viewAction: .urlChanged(webView.url))
}
}
// MARK: - Picture in Picture
func requestPictureInPicture() async -> Result<AVPictureInPictureController, CallScreenError> {
guard let pictureInPictureController,
pictureInPictureController.isPictureInPicturePossible,
case .success(true) = await webViewCanEnterPictureInPicture() else {
return .failure(.pictureInPictureNotAvailable)
}
pictureInPictureController.startPictureInPicture()
return .success(pictureInPictureController)
}
func stopPictureInPicture() {
pictureInPictureController?.stopPictureInPicture()
}
nonisolated func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
Task { @MainActor in
// We move the view via the delegate so it works when you background the app without calling requestPictureInPicture
pictureInPictureViewController.view.addMatchedSubview(webView)
_ = try? await evaluateJavaScript("controls.enablePip()")
}
}
nonisolated func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
Task { await viewModelContext?.send(viewAction: .pictureInPictureWillStop) }
}
nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
Task { @MainActor in
webViewWrapper.addMatchedSubview(webView)
_ = try? await evaluateJavaScript("controls.disablePip()")
}
}
/// Whether the web view can do picture in picture or not (e.g. it is showing an error or the page didn't load).
private func webViewCanEnterPictureInPicture() async -> Result<Bool, CallScreenError> {
do {
guard let canEnterPictureInPicture = try await evaluateJavaScript("controls.canEnterPip()") as? Bool else {
MXLog.error("canEnterPip returned an unexpected value, skipping picture in picture.")
return .failure(.pictureInPictureNotAvailable)
}
MXLog.info("canEnterPip returned \(canEnterPictureInPicture)")
return .success(canEnterPictureInPicture)
} catch {
MXLog.error("Error checking canEnterPip: \(error)")
return .failure(.pictureInPictureNotAvailable)
}
}
}
/// Avoids retain loops between the configuration and webView coordinator
@@ -28,6 +28,7 @@ struct RoomScreenCoordinatorParameters {
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let emojiProvider: EmojiProviderProtocol
let completionSuggestionService: CompletionSuggestionServiceProtocol
let ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>
let appMediator: AppMediatorProtocol
let appSettings: AppSettings
let composerDraftService: ComposerDraftServiceProtocol
@@ -64,6 +65,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
init(parameters: RoomScreenCoordinatorParameters) {
roomViewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider,
ongoingCallRoomIDPublisher: parameters.ongoingCallRoomIDPublisher,
appMediator: parameters.appMediator,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
@@ -46,6 +46,7 @@ struct RoomScreenViewState: BindableState {
var canJoinCall = false
var hasOngoingCall: Bool
var shouldShowCallButton = true
var bindings: RoomScreenViewStateBindings
}
@@ -53,6 +53,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
init(roomProxy: JoinedRoomProxyProtocol,
mediaProvider: MediaProviderProtocol,
ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService) {
@@ -68,7 +69,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
bindings: .init()),
mediaProvider: mediaProvider)
setupSubscriptions()
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
}
override func process(viewAction: RoomScreenViewAction) {
@@ -93,7 +94,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.lastScrollDirection = direction
}
private func setupSubscriptions() {
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
let roomInfoSubscription = roomProxy
.actionsPublisher
.filter { $0 == .roomInfoUpdate }
@@ -135,6 +136,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
self?.setupPinnedEventsTimelineProviderIfNeeded()
}
.store(in: &cancellables)
ongoingCallRoomIDPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] ongoingCallRoomID in
guard let self else { return }
state.shouldShowCallButton = ongoingCallRoomID != roomProxy.id
}
.store(in: &cancellables)
}
private func buildPinnedEventContents(timelineItems: [TimelineItemProxy]) {
@@ -186,6 +195,7 @@ extension RoomScreenViewModel {
static func mock(roomProxyMock: JoinedRoomProxyMock) -> RoomScreenViewModel {
RoomScreenViewModel(roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics)
@@ -175,8 +175,10 @@ struct RoomScreen: View {
if !ProcessInfo.processInfo.isiOSAppOnMac {
ToolbarItem(placement: .primaryAction) {
callButton
.disabled(roomContext.viewState.canJoinCall == false)
if roomContext.viewState.shouldShowCallButton {
callButton
.disabled(!roomContext.viewState.canJoinCall)
}
}
}
}
@@ -53,6 +53,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var elementCallBaseURLOverride: URL? { get set }
var fuzzyRoomListSearchEnabled: Bool { get set }
var pinningEnabled: Bool { get set }
var elementCallPictureInPictureEnabled: Bool { get set }
}
extension AppSettings: DeveloperOptionsProtocol { }
@@ -61,6 +61,11 @@ struct DeveloperOptionsScreen: View {
.autocorrectionDisabled(true)
.autocapitalization(.none)
.foregroundColor(URL(string: elementCallBaseURLString) == nil ? .red : .primary)
Toggle(isOn: $context.elementCallPictureInPictureEnabled) {
Text("Picture in Picture support")
Text("Requires an Element Call deployment with support for signalling PiP availability.")
}
}
Section {
@@ -77,7 +77,7 @@ struct ElementCallConfiguration {
}
/// A string representing the call being configured.
var callID: String {
var callRoomID: String {
switch kind {
case .genericCallLink(let url):
url.absoluteString
@@ -57,9 +57,14 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
private var endUnansweredCallTask: Task<Void, Never>?
private var ongoingCallID: CallID?
private var ongoingCallID: CallID? {
didSet { ongoingCallRoomIDSubject.send(ongoingCallID?.roomID) }
}
var ongoingCallRoomID: String? { ongoingCallID?.roomID }
let ongoingCallRoomIDSubject = CurrentValueSubject<String?, Never>(nil)
var ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never> {
ongoingCallRoomIDSubject.asCurrentValuePublisher()
}
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
var actions: AnyPublisher<ElementCallServiceAction, Never> {
@@ -26,7 +26,7 @@ enum ElementCallServiceAction {
protocol ElementCallServiceProtocol {
var actions: AnyPublisher<ElementCallServiceAction, Never> { get }
var ongoingCallRoomID: String? { get }
var ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never> { get }
func setClientProxy(_ clientProxy: ClientProxyProtocol)
@@ -255,6 +255,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -272,6 +273,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -289,6 +291,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -306,6 +309,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -326,6 +330,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -346,6 +351,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -366,6 +372,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -387,6 +394,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -407,6 +415,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -426,6 +435,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -459,6 +469,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -479,6 +490,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))
@@ -499,6 +511,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
composerDraftService: ComposerDraftServiceMock(.init()))