AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/CallingViewComponent/InfoHeaderViewModel.swift (238 lines of code) (raw):
//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
import Combine
import Foundation
import SwiftUI
class InfoHeaderViewModel: ObservableObject {
@Published var accessibilityLabelTitle: String
@Published var isInfoHeaderDisplayed = true
@Published var isVoiceOverEnabled = false
@Published var accessibilityLabelSubtitle: String
@Published var title = ""
@Published var subtitle: String? = ""
@Published var customButton1ViewModel: IconButtonViewModel?
@Published var customButton2ViewModel: IconButtonViewModel?
private let compositeViewModelFactory: CompositeViewModelFactoryProtocol
private let logger: Logger
private let dispatch: ActionDispatch
private let accessibilityProvider: AccessibilityProviderProtocol
private let localizationProvider: LocalizationProviderProtocol
private var participantsCount: Int = 0
private var callingStatus: CallingStatus = .none
private let enableSystemPipWhenMultitasking: Bool
let enableMultitasking: Bool
var participantListButtonViewModel: IconButtonViewModel!
var dismissButtonViewModel: IconButtonViewModel!
private var cancellables = Set<AnyCancellable>()
private let controlHeaderViewData: CallScreenHeaderViewData?
var isPad = false
init(compositeViewModelFactory: CompositeViewModelFactoryProtocol,
logger: Logger,
localUserState: LocalUserState,
localizationProvider: LocalizationProviderProtocol,
accessibilityProvider: AccessibilityProviderProtocol,
dispatchAction: @escaping ActionDispatch,
enableMultitasking: Bool,
enableSystemPipWhenMultitasking: Bool,
callScreenInfoHeaderState: CallScreenInfoHeaderState,
buttonViewDataState: ButtonViewDataState,
controlHeaderViewData: CallScreenHeaderViewData?
) {
self.compositeViewModelFactory = compositeViewModelFactory
self.controlHeaderViewData = controlHeaderViewData
let infoLabel = localizationProvider.getLocalizedString(.callWith0Person)
self.title = callScreenInfoHeaderState.title ?? infoLabel
self.subtitle = callScreenInfoHeaderState.subtitle ?? ""
self.accessibilityLabelTitle = callScreenInfoHeaderState.title ?? infoLabel
self.accessibilityLabelSubtitle = callScreenInfoHeaderState.subtitle ?? ""
self.dispatch = dispatchAction
self.logger = logger
self.accessibilityProvider = accessibilityProvider
self.localizationProvider = localizationProvider
self.enableMultitasking = enableMultitasking
self.enableSystemPipWhenMultitasking = enableSystemPipWhenMultitasking
self.participantListButtonViewModel = compositeViewModelFactory.makeIconButtonViewModel(
iconName: .showParticipant,
buttonType: .infoButton,
isDisabled: false) { [weak self] in
guard let self = self else {
return
}
self.showParticipantListButtonTapped()
}
self.participantListButtonViewModel.accessibilityLabel = self.localizationProvider.getLocalizedString(
.participantListAccessibilityLabel)
dismissButtonViewModel = compositeViewModelFactory.makeIconButtonViewModel(
iconName: .leftArrow,
buttonType: .infoButton,
isDisabled: false) { [weak self] in
guard let self = self else {
return
}
self.dismissButtonTapped()
}
dismissButtonViewModel.update(
accessibilityLabel: self.localizationProvider.getLocalizedString(.dismissAccessibilityLabel))
self.accessibilityProvider.subscribeToVoiceOverStatusDidChangeNotification(self)
self.accessibilityProvider.subscribeToUIFocusDidUpdateNotification(self)
updateInfoHeaderAvailability()
updateCustomButtons(buttonViewDataState)
}
func formatTimeInterval(_ interval: TimeInterval) -> String {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
if interval >= 3600 {
// If the interval is 1 hour or more, show hours, minutes, and seconds
formatter.allowedUnits = [.hour, .minute, .second]
} else if interval >= 60 {
// If the interval is 1 minute or more, show minutes and seconds
formatter.allowedUnits = [.minute, .second]
} else {
// If the interval is less than 1 minute, show seconds only
formatter.allowedUnits = [.second]
}
return formatter.string(from: interval) ?? "00:00:00"
}
func showParticipantListButtonTapped() {
logger.debug("Show participant list button tapped")
self.displayParticipantsList()
}
func displayParticipantsList() {
dispatch(.showParticipants)
}
func toggleDisplayInfoHeaderIfNeeded() {
guard !isVoiceOverEnabled else {
return
}
if self.isInfoHeaderDisplayed {
hideInfoHeader()
} else {
displayWithTimer()
}
}
func update(localUserState: LocalUserState,
remoteParticipantsState: RemoteParticipantsState,
callingState: CallingState,
visibilityState: VisibilityState,
callScreenInfoHeaderState: CallScreenInfoHeaderState,
buttonViewDataState: ButtonViewDataState
) {
isHoldingCall(callingState: callingState)
let shouldDisplayInfoHeaderValue = shouldDisplayInfoHeader(for: callingStatus)
let newDisplayInfoHeaderValue = shouldDisplayInfoHeader(for: callingState.status)
callingStatus = callingState.status
if isVoiceOverEnabled && newDisplayInfoHeaderValue != shouldDisplayInfoHeaderValue {
updateInfoHeaderAvailability()
}
let updatedRemoteparticipantCount = getParticipantCount(remoteParticipantsState)
if participantsCount != updatedRemoteparticipantCount
&& callScreenInfoHeaderState.title == nil {
participantsCount = updatedRemoteparticipantCount
updateInfoLabel()
}
if visibilityState.currentStatus == .pipModeEntered {
hideInfoHeader()
}
if callScreenInfoHeaderState.title != nil
&& callScreenInfoHeaderState.title != self.title {
self.title = (callScreenInfoHeaderState.title)!
self.accessibilityLabelTitle = self.title
}
if callScreenInfoHeaderState.subtitle != nil
&& callScreenInfoHeaderState.subtitle != self.subtitle {
self.subtitle = callScreenInfoHeaderState.subtitle
self.accessibilityLabelSubtitle = self.subtitle ?? ""
}
updateCustomButtons(buttonViewDataState)
}
private func getParticipantCount(_ remoteParticipantsState: RemoteParticipantsState) -> Int {
let remoteParticipantCountForGridView = remoteParticipantsState.participantInfoList
.filter({ participantInfoModel in
participantInfoModel.status != .inLobby && participantInfoModel.status != .disconnected
})
.count
let filteredOutRemoteParticipantsCount =
remoteParticipantsState.participantInfoList.count - remoteParticipantCountForGridView
return remoteParticipantsState.totalParticipantCount - filteredOutRemoteParticipantsCount
}
private func isHoldingCall(callingState: CallingState) {
guard callingState.status == .localHold,
callingStatus != callingState.status else {
return
}
if isInfoHeaderDisplayed {
isInfoHeaderDisplayed = false
}
}
private func updateInfoLabel() {
let content: String
switch participantsCount {
case 0:
content = localizationProvider.getLocalizedString(.callWith0Person)
case 1:
content = localizationProvider.getLocalizedString(.callWith1Person)
default:
content = localizationProvider.getLocalizedString(.callWithNPerson, participantsCount)
}
title = content
accessibilityLabelTitle = content
}
private func displayWithTimer() {
self.isInfoHeaderDisplayed = true
}
@objc private func hideInfoHeader() {
self.isInfoHeaderDisplayed = false
}
private func updateInfoHeaderAvailability() {
let shouldDisplayInfoHeader = shouldDisplayInfoHeader(for: callingStatus)
isVoiceOverEnabled = accessibilityProvider.isVoiceOverEnabled
if self.isVoiceOverEnabled {
isInfoHeaderDisplayed = shouldDisplayInfoHeader
} else if shouldDisplayInfoHeader {
displayWithTimer()
}
}
private func shouldDisplayInfoHeader(for callingStatus: CallingStatus) -> Bool {
return callingStatus != .inLobby && callingStatus != .localHold
}
private func dismissButtonTapped() {
if self.enableSystemPipWhenMultitasking {
dispatch(.visibilityAction(.pipModeRequested))
} else if self.enableMultitasking {
dispatch(.visibilityAction(.hideRequested))
}
}
private func updateCustomButtons(_ buttonViewDataState: ButtonViewDataState) {
self.customButton1ViewModel = createCustomButtonViewModel(
buttonViewDataState.callScreenHeaderCustomButtonsState.first)
if buttonViewDataState.callScreenHeaderCustomButtonsState.count >= 2 {
self.customButton2ViewModel = createCustomButtonViewModel(
buttonViewDataState.callScreenHeaderCustomButtonsState[1])
}
}
private func createCustomButtonViewModel(_ customButton: CustomButtonState?) -> IconButtonViewModel? {
guard let customButton else {
return nil
}
var buttonViewModel = compositeViewModelFactory.makeIconButtonViewModel(icon: customButton.image,
buttonType: .infoButton,
isDisabled: !customButton.enabled,
isVisible: customButton.visible) { [weak self] in
guard let optionsButton = self?.controlHeaderViewData?.customButtons.first(where: { optionsButton in
optionsButton.id == customButton.id
}) else {
return
}
optionsButton.onClick(optionsButton)
}
buttonViewModel.accessibilityLabel = customButton.title
return buttonViewModel
}
}
extension InfoHeaderViewModel: AccessibilityProviderNotificationsObserver {
func didUIFocusUpdateNotification(_ notification: NSNotification) {
updateInfoHeaderAvailability()
}
func didChangeVoiceOverStatus(_ notification: NSNotification) {
guard isVoiceOverEnabled != accessibilityProvider.isVoiceOverEnabled else {
return
}
updateInfoHeaderAvailability()
}
}