AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/Presentation/SwiftUI/Calling/Grid/Cell/ParticipantGridCellView.swift (134 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import Combine import FluentUI import SwiftUI struct ParticipantGridCellView: View { @ObservedObject var viewModel: ParticipantGridCellViewModel let rendererViewManager: RendererViewManager? let avatarViewManager: AvatarViewManagerProtocol @State var avatarImage: UIImage? @State var displayedVideoStreamId: String? @State var isVideoChanging = false let avatarSize: CGFloat = 56 var body: some View { Group { GeometryReader { geometry in if let videoStreamId = displayedVideoStreamId, let rendererViewInfo = getRendererViewInfo(for: videoStreamId) { let zoomable = viewModel.videoViewModel?.videoStreamType == .screenSharing ParticipantGridCellVideoView(videoRendererViewInfo: rendererViewInfo, rendererViewManager: rendererViewManager, zoomable: zoomable, isSpeaking: $viewModel.isSpeaking, displayName: $viewModel.displayName, isMuted: $viewModel.isMuted, isTypingRtt: $viewModel.isTypingRtt) } else { avatarView .frame(width: geometry.size.width, height: geometry.size.height) } } .accessibilityElement(children: .combine) .accessibilityLabel(Text(viewModel.accessibilityLabel)) .accessibilityIdentifier(AccessibilityIdentifier.participantGridCellViewAccessibilityID.rawValue) } .onReceive(viewModel.$videoViewModel) { model in if model?.videoStreamId != displayedVideoStreamId { displayedVideoStreamId = model?.videoStreamId } } .onReceive(viewModel.$participantIdentifier) { updateParticipantViewData(for: $0) } .onReceive(avatarViewManager.updatedId) { guard $0 == viewModel.participantIdentifier else { return } updateParticipantViewData(for: viewModel.participantIdentifier) } } func getRendererViewInfo(for videoStreamId: String) -> ParticipantRendererViewInfo? { guard !videoStreamId.isEmpty else { return nil } let remoteParticipantVideoViewId = RemoteParticipantVideoViewId(userIdentifier: viewModel.participantIdentifier, videoStreamIdentifier: videoStreamId) return rendererViewManager?.getRemoteParticipantVideoRendererView(remoteParticipantVideoViewId) } private func updateParticipantViewData(for identifier: String) { guard let participantViewData = avatarViewManager.avatarStorage.value(forKey: identifier) else { avatarImage = nil viewModel.updateParticipantNameIfNeeded(with: nil) return } if avatarImage !== participantViewData.avatarImage { avatarImage = participantViewData.avatarImage } viewModel.updateParticipantNameIfNeeded(with: participantViewData.displayName) } var avatarView: some View { return VStack(alignment: .center, spacing: 5) { CompositeAvatar(displayName: $viewModel.avatarDisplayName, avatarImage: $avatarImage, isSpeaking: (viewModel.isSpeaking && !viewModel.isMuted) || viewModel.isTypingRtt) .frame(width: avatarSize, height: avatarSize) Spacer().frame(height: 10) ParticipantTitleView(displayName: $viewModel.displayName, isMuted: $viewModel.isMuted, isHold: $viewModel.isHold, titleFont: Fonts.caption1.font, mutedIconSize: 16) if viewModel.isHold { Text(viewModel.getOnHoldString()) .font(Fonts.caption1.font) .lineLimit(1) .foregroundColor(Color(StyleProvider.color.onBackground)) .padding(.top, 8) } } } } struct ParticipantTitleView: View { @Binding var displayName: String? @Binding var isMuted: Bool @Binding var isHold: Bool @Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory @AccessibilityFocusState private var isFocused: Bool // Add focus state let titleFont: Font let mutedIconSize: CGFloat private var isEmpty: Bool { return !isMuted && displayName?.trimmingCharacters(in: .whitespaces).isEmpty == true } private enum Constants { static let hSpace: CGFloat = 4 // MARK: Font Minimum Scale Factor // Under accessibility mode, the largest size is 35 // so the scale factor would be 9/35 or 0.2 static let accessibilityFontScale: CGFloat = 0.2 // UI guideline suggested min font size should be 9. // Since Fonts.caption1 has font size of 12, // so min scale factor should be 9/12 or 0.75 as default. static let defaultFontScale: CGFloat = 0.75 } var body: some View { HStack(alignment: .center, spacing: Constants.hSpace, content: { if let displayName = displayName, !displayName.trimmingCharacters(in: .whitespaces).isEmpty { Text(displayName) .font(titleFont) .lineLimit(1) .minimumScaleFactor(sizeCategory.isAccessibilityCategory ? Constants.accessibilityFontScale : Constants.defaultFontScale) .foregroundColor(Color(StyleProvider.color.onBackground)) } if isMuted && !isHold { Icon(name: .micOff, size: mutedIconSize) .accessibility(hidden: true) } }) .padding(.horizontal, isEmpty ? 0 : 4) .animation(.default, value: true) .accessibilityFocused($isFocused) // Apply accessibility focus .onChange(of: isHold) { newValue in if newValue { isFocused = true // Request focus when put on hold } } } }