AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/ParticipantGridViewModel.swift (223 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import Foundation import Combine class ParticipantGridViewModel: ObservableObject { @Published var shouldUseVerticalStyleGrid = true private let compositeViewModelFactory: CompositeViewModelFactoryProtocol private let localizationProvider: LocalizationProviderProtocol private let accessibilityProvider: AccessibilityProviderProtocol private let isIpadInterface: Bool private let callType: CompositeCallType private var maximumParticipantsDisplayed: Int { return self.visibilityStatus == .pipModeEntered ? 1 : isIpadInterface ? 9 : 6 } private var lastUpdateTimeStamp = Date() private var lastDominantSpeakersUpdatedTimestamp = Date() private var visibilityStatus: VisibilityStatus = .visible private var appStatus: AppStatus = .foreground private(set) var participantsCellViewModelArr: [ParticipantGridCellViewModel] = [] @Published var gridsCount: Int = 0 @Published var displayedParticipantInfoModelArr: [ParticipantInfoModel] = [] let rendererViewManager: RendererViewManager init(compositeViewModelFactory: CompositeViewModelFactoryProtocol, localizationProvider: LocalizationProviderProtocol, accessibilityProvider: AccessibilityProviderProtocol, isIpadInterface: Bool, callType: CompositeCallType, rendererViewManager: RendererViewManager) { self.compositeViewModelFactory = compositeViewModelFactory self.localizationProvider = localizationProvider self.accessibilityProvider = accessibilityProvider self.isIpadInterface = isIpadInterface self.callType = callType self.rendererViewManager = rendererViewManager } func update(callingState: CallingState, captionsState: CaptionsState, rttState: RttState, remoteParticipantsState: RemoteParticipantsState, visibilityState: VisibilityState, lifeCycleState: LifeCycleState) { guard lastUpdateTimeStamp != remoteParticipantsState.lastUpdateTimeStamp || lastDominantSpeakersUpdatedTimestamp != remoteParticipantsState.dominantSpeakersModifiedTimestamp || visibilityStatus != visibilityState.currentStatus || appStatus != lifeCycleState.currentStatus else { return } lastUpdateTimeStamp = remoteParticipantsState.lastUpdateTimeStamp lastDominantSpeakersUpdatedTimestamp = remoteParticipantsState.dominantSpeakersModifiedTimestamp visibilityStatus = visibilityState.currentStatus appStatus = lifeCycleState.currentStatus let remoteParticipants = remoteParticipantsState.participantInfoList .filter { participanInfoModel in participanInfoModel.status != .inLobby && participanInfoModel.status != .disconnected } let dominantSpeakers = remoteParticipantsState.dominantSpeakers let newDisplayedInfoModelArr = getDisplayedInfoViewModels(remoteParticipants, dominantSpeakers, visibilityState) let removedModels = getRemovedInfoModels(for: newDisplayedInfoModelArr) let addedModels = getAddedInfoModels(for: newDisplayedInfoModelArr) let orderedInfoModelArr = sortDisplayedInfoModels(newDisplayedInfoModelArr, removedModels: removedModels, addedModels: addedModels) updateCellViewModel(for: orderedInfoModelArr, lifeCycleState: lifeCycleState) displayedParticipantInfoModelArr = orderedInfoModelArr if callingState.status == .connected || callingState.status == .remoteHold || (callType == .oneToNOutgoing && ( callingState.status == .connecting || callingState.status == .ringing)) { // announce participants list changes only if the user is already connected to the call postParticipantsListUpdateAccessibilityAnnouncements(removedModels: removedModels, addedModels: addedModels) } updateVideoViewManager(displayedRemoteInfoModelArr: displayedParticipantInfoModelArr) if gridsCount != displayedParticipantInfoModelArr.count { gridsCount = displayedParticipantInfoModelArr.count } shouldUseVerticalStyleGrid = ScreenSizeClassKey.defaultValue == .iphonePortraitScreenSize || captionsState.isCaptionsOn || rttState.isRttOn } private func updateVideoViewManager(displayedRemoteInfoModelArr: [ParticipantInfoModel]) { let videoCacheIds: [RemoteParticipantVideoViewId] = displayedRemoteInfoModelArr.compactMap { let screenShareVideoStreamIdentifier = $0.screenShareVideoStreamModel?.videoStreamIdentifier let cameraVideoStreamIdentifier = $0.cameraVideoStreamModel?.videoStreamIdentifier guard let videoStreamIdentifier = screenShareVideoStreamIdentifier ?? cameraVideoStreamIdentifier else { return nil } return RemoteParticipantVideoViewId(userIdentifier: $0.userIdentifier, videoStreamIdentifier: videoStreamIdentifier) } rendererViewManager.updateDisplayedRemoteVideoStream(videoCacheIds) } private func getDisplayedInfoViewModels(_ infoModels: [ParticipantInfoModel], _ dominantSpeakers: [String], _ visibilityState: VisibilityState) -> [ParticipantInfoModel] { if let presentingParticipant = infoModels.first(where: { $0.screenShareVideoStreamModel != nil }) { return [presentingParticipant] } if infoModels.count <= maximumParticipantsDisplayed { return infoModels } var dominantSpeakersOrder = [String: Int]() for idx in 0..<min(maximumParticipantsDisplayed, dominantSpeakers.count) { dominantSpeakersOrder[dominantSpeakers[idx]] = idx } let sortedInfoList = infoModels.sorted(by: { if let order1 = dominantSpeakersOrder[$0.userIdentifier], let order2 = dominantSpeakersOrder[$1.userIdentifier] { return order1 < order2 } if dominantSpeakersOrder[$0.userIdentifier] != nil { return true } if dominantSpeakersOrder[$1.userIdentifier] != nil { return false } if ($0.cameraVideoStreamModel != nil && $1.cameraVideoStreamModel != nil) || ($0.cameraVideoStreamModel == nil && $1.cameraVideoStreamModel == nil) { return true } if $0.cameraVideoStreamModel != nil { return true } else { return false } }) let newDisplayRemoteParticipant = sortedInfoList.prefix(maximumParticipantsDisplayed) // Need to filter if the user is on the lobby or not return Array(newDisplayRemoteParticipant) } private func getRemovedInfoModels(for newInfoModels: [ParticipantInfoModel]) -> [ParticipantInfoModel] { return displayedParticipantInfoModelArr.filter { old in !newInfoModels.contains(where: { new in new.userIdentifier == old.userIdentifier }) } } private func getAddedInfoModels(for newInfoModels: [ParticipantInfoModel]) -> [ParticipantInfoModel] { return newInfoModels.filter { new in !displayedParticipantInfoModelArr.contains(where: { old in new.userIdentifier == old.userIdentifier }) } } private func sortDisplayedInfoModels(_ newInfoModels: [ParticipantInfoModel], removedModels: [ParticipantInfoModel], addedModels: [ParticipantInfoModel]) -> [ParticipantInfoModel] { var localCacheInfoModelArr = displayedParticipantInfoModelArr guard removedModels.count == addedModels.count else { // when there is a gridType change // we just directly update the order based on the latest sorting return newInfoModels } var replacedIndex = [Int]() // Otherwise, we keep those existed participant in same position when there is any update for (index, item) in removedModels.enumerated() { if let removeIndex = localCacheInfoModelArr.firstIndex(where: { $0.userIdentifier == item.userIdentifier }) { localCacheInfoModelArr[removeIndex] = addedModels[index] replacedIndex.append(removeIndex) } } // To update existed participantInfoModel for (index, item) in localCacheInfoModelArr.enumerated() { if !replacedIndex.contains(index), let newItem = newInfoModels.first(where: {$0.userIdentifier == item.userIdentifier}) { localCacheInfoModelArr[index] = newItem } } return localCacheInfoModelArr } private func updateCellViewModel(for displayedRemoteParticipants: [ParticipantInfoModel], lifeCycleState: LifeCycleState) { if participantsCellViewModelArr.count == displayedRemoteParticipants.count { updateOrderedCellViewModels(for: displayedRemoteParticipants, lifeCycleState: lifeCycleState) } else { updateAndReorderCellViewModels(for: displayedRemoteParticipants, lifeCycleState: lifeCycleState) } } private func updateOrderedCellViewModels(for displayedRemoteParticipants: [ParticipantInfoModel], lifeCycleState: LifeCycleState) { guard participantsCellViewModelArr.count == displayedRemoteParticipants.count else { return } for (index, infoModel) in displayedRemoteParticipants.enumerated() { let cellViewModel = participantsCellViewModelArr[index] cellViewModel.update(participantModel: infoModel) } } private func updateAndReorderCellViewModels(for displayedRemoteParticipants: [ParticipantInfoModel], lifeCycleState: LifeCycleState) { var newCellViewModelArr = [ParticipantGridCellViewModel]() for infoModel in displayedRemoteParticipants { if let viewModel = participantsCellViewModelArr.first(where: { $0.participantIdentifier == infoModel.userIdentifier }) { viewModel.update(participantModel: infoModel) newCellViewModelArr.append(viewModel) } else { let cellViewModel = compositeViewModelFactory .makeParticipantCellViewModel(participantModel: infoModel) newCellViewModelArr.append(cellViewModel) } } participantsCellViewModelArr = newCellViewModelArr } private func postParticipantsListUpdateAccessibilityAnnouncements(removedModels: [ParticipantInfoModel], addedModels: [ParticipantInfoModel]) { if !removedModels.isEmpty { if removedModels.count == 1 { accessibilityProvider.postQueuedAnnouncement( localizationProvider.getLocalizedString(.onePersonLeft, removedModels.first!.displayName)) } else { accessibilityProvider.postQueuedAnnouncement( localizationProvider.getLocalizedString(.multiplePeopleLeft, removedModels.count)) } } if !addedModels.isEmpty { if addedModels.count == 1 { accessibilityProvider.postQueuedAnnouncement( localizationProvider.getLocalizedString(.onePersonJoined, addedModels.first!.displayName)) } else { accessibilityProvider.postQueuedAnnouncement( localizationProvider.getLocalizedString(.multiplePeopleJoined, addedModels.count)) } } } }