import Foundation import UIKit import Display import ComponentFlow import MultilineTextComponent import TelegramPresentationData import AppBundle import TelegramCore import AccountContext import SwiftSignalKit import MetalEngine import CallScreen import AvatarNode import ContextUI final class VideoChatParticipantThumbnailComponent: Component { let call: VideoChatCall let theme: PresentationTheme let participant: GroupCallParticipantsContext.Participant let isPresentation: Bool let isSelected: Bool let isSpeaking: Bool let displayVideo: Bool let interfaceOrientation: UIInterfaceOrientation let action: (() -> Void)? let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: VideoChatCall, theme: PresentationTheme, participant: GroupCallParticipantsContext.Participant, isPresentation: Bool, isSelected: Bool, isSpeaking: Bool, displayVideo: Bool, interfaceOrientation: UIInterfaceOrientation, action: (() -> Void)?, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme self.participant = participant self.isPresentation = isPresentation self.isSelected = isSelected self.isSpeaking = isSpeaking self.displayVideo = displayVideo self.interfaceOrientation = interfaceOrientation self.action = action self.contextAction = contextAction } static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool { if lhs.call != rhs.call { return false } if lhs.theme !== rhs.theme { return false } if lhs.participant != rhs.participant { return false } if lhs.isPresentation != rhs.isPresentation { return false } if lhs.isSelected != rhs.isSelected { return false } if lhs.isSpeaking != rhs.isSpeaking { return false } if lhs.displayVideo != rhs.displayVideo { return false } if lhs.interfaceOrientation != rhs.interfaceOrientation { return false } if (lhs.action == nil) != (rhs.action == nil) { return false } if (lhs.contextAction == nil) != (rhs.contextAction == nil) { return false } return true } private struct VideoSpec: Equatable { var resolution: CGSize var rotationAngle: Float var followsDeviceOrientation: Bool init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) { self.resolution = resolution self.rotationAngle = rotationAngle self.followsDeviceOrientation = followsDeviceOrientation } } final class View: ContextControllerSourceView { private static let selectedBorderImage: UIImage? = { return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: UIColor.white, strokeWidth: 2.0)?.withRenderingMode(.alwaysTemplate) }() private var component: VideoChatParticipantThumbnailComponent? private weak var componentState: EmptyComponentState? private var isUpdating: Bool = false private let extractedContainerView: ContextExtractedContentContainingView private let backgroundLayer: SimpleLayer private var avatarNode: AvatarNode? private let title = ComponentView() private let muteStatus = ComponentView() private var selectedBorderView: UIImageView? private var videoSource: AdaptedCallVideoSource? private var videoDisposable: Disposable? private var videoBackgroundLayer: SimpleLayer? private var videoLayer: PrivateCallVideoLayer? private var videoSpec: VideoSpec? override init(frame: CGRect) { self.extractedContainerView = ContextExtractedContentContainingView() self.backgroundLayer = SimpleLayer() self.backgroundLayer.backgroundColor = UIColor(rgb: 0x1C1C1E).cgColor super.init(frame: frame) self.addSubview(self.extractedContainerView) self.targetViewForActivationProgress = self.extractedContainerView.contentView self.extractedContainerView.contentView.layer.addSublayer(self.backgroundLayer) self.extractedContainerView.contentView.clipsToBounds = true self.extractedContainerView.contentView.layer.cornerRadius = 10.0 self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.activated = { [weak self] gesture, _ in guard let self, let component = self.component else { gesture.cancel() return } if let participantPeer = component.participant.peer { component.contextAction?(participantPeer, self.extractedContainerView, gesture) } } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.videoDisposable?.dispose() } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { guard let component = self.component, let action = component.action else { return } action() } } func update(component: VideoChatParticipantThumbnailComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } let previousComponent = self.component let wasSpeaking = previousComponent?.isSpeaking ?? false let speakingAlphaTransition: ComponentTransition if transition.animation.isImmediate { speakingAlphaTransition = .immediate } else { if let previousComponent, previousComponent.isSelected == component.isSelected { if !wasSpeaking { speakingAlphaTransition = .easeInOut(duration: 0.1) } else { speakingAlphaTransition = .easeInOut(duration: 0.25) } } else { speakingAlphaTransition = .immediate } } self.component = component self.componentState = state transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center) transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize)) self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize) let avatarNode: AvatarNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 17.0)) avatarNode.isUserInteractionEnabled = false self.avatarNode = avatarNode self.extractedContainerView.contentView.addSubview(avatarNode.view) } let avatarSize = CGSize(width: 50.0, height: 50.0) let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) * 0.5), y: 7.0), size: avatarSize) transition.setFrame(view: avatarNode.view, frame: avatarFrame) avatarNode.updateSize(size: avatarSize) if component.participant.peer?.smallProfileImage != nil { avatarNode.setPeerV2(context: component.call.accountContext, theme: component.theme, peer: component.participant.peer, displayDimensions: avatarSize) } else { avatarNode.setPeer(context: component.call.accountContext, theme: component.theme, peer: component.participant.peer, displayDimensions: avatarSize) } let muteStatusSize = self.muteStatus.update( transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking) )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) ) let muteStatusFrame = CGRect(origin: CGPoint(x: availableSize.width + 5.0 - muteStatusSize.width, y: availableSize.height + 5.0 - muteStatusSize.height), size: muteStatusSize) if let muteStatusView = self.muteStatus.view as? VideoChatMuteIconComponent.View { if muteStatusView.superview == nil { self.extractedContainerView.contentView.addSubview(muteStatusView) } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setScale(view: muteStatusView, scale: 0.65) } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: component.participant.peer?.compactDisplayTitle ?? "User \(component.participant.id)", font: Font.semibold(13.0), textColor: .white)) )), environment: {}, containerSize: CGSize(width: availableSize.width - 6.0 * 2.0 - 12.0, height: 100.0) ) let titleFrame = CGRect(origin: CGPoint(x: 6.0, y: availableSize.height - 6.0 - titleSize.height), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() titleView.isUserInteractionEnabled = false self.extractedContainerView.contentView.addSubview(titleView) } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) } if component.displayVideo, let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { let videoBackgroundLayer: SimpleLayer if let current = self.videoBackgroundLayer { videoBackgroundLayer = current } else { videoBackgroundLayer = SimpleLayer() videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor self.videoBackgroundLayer = videoBackgroundLayer self.extractedContainerView.contentView.layer.insertSublayer(videoBackgroundLayer, above: avatarNode.layer) videoBackgroundLayer.isHidden = true } let videoLayer: PrivateCallVideoLayer if let current = self.videoLayer { videoLayer = current } else { videoLayer = PrivateCallVideoLayer(enableSharpening: false) self.videoLayer = videoLayer self.extractedContainerView.contentView.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer) self.extractedContainerView.contentView.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer) videoLayer.blurredLayer.opacity = 0.25 if let input = component.call.video(endpointId: videoDescription.endpointId) { let videoSource = AdaptedCallVideoSource(videoStreamSignal: input) self.videoSource = videoSource self.videoDisposable?.dispose() self.videoDisposable = videoSource.addOnUpdated { [weak self] in guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else { return } let videoOutput = videoSource.currentOutput videoLayer.video = videoOutput if let videoOutput { let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation) if self.videoSpec != videoSpec { self.videoSpec = videoSpec if !self.isUpdating { self.componentState?.updated(transition: .immediate, isLocal: true) } } } else { if self.videoSpec != nil { self.videoSpec = nil if !self.isUpdating { self.componentState?.updated(transition: .immediate, isLocal: true) } } } } } } transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) if let videoSpec = self.videoSpec { videoBackgroundLayer.isHidden = component.isSelected videoLayer.blurredLayer.isHidden = component.isSelected videoLayer.isHidden = component.isSelected let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation) var rotatedResolution = videoSpec.resolution var videoIsRotated = false if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne { videoIsRotated = true } if videoIsRotated { rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width) } let videoSize = rotatedResolution.aspectFilled(availableSize) let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize) let blurredVideoSize = rotatedResolution.aspectFilled(availableSize) let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize) let videoResolution = rotatedResolution.aspectFitted(CGSize(width: availableSize.width * 3.0, height: availableSize.height * 3.0)) var rotatedVideoResolution = videoResolution var rotatedVideoFrame = videoFrame var rotatedBlurredVideoFrame = blurredVideoFrame var rotatedVideoBoundsSize = videoFrame.size var rotatedBlurredVideoBoundsSize = blurredVideoFrame.size if videoIsRotated { rotatedVideoBoundsSize = CGSize(width: rotatedVideoBoundsSize.height, height: rotatedVideoBoundsSize.width) rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) rotatedBlurredVideoBoundsSize = CGSize(width: rotatedBlurredVideoBoundsSize.height, height: rotatedBlurredVideoBoundsSize.width) rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center) } rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale)) transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center) transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize)) transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2) transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center) transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize)) transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0)) } } else { if let videoBackgroundLayer = self.videoBackgroundLayer { self.videoBackgroundLayer = nil videoBackgroundLayer.removeFromSuperlayer() } if let videoLayer = self.videoLayer { self.videoLayer = nil videoLayer.blurredLayer.removeFromSuperlayer() videoLayer.removeFromSuperlayer() } self.videoDisposable?.dispose() self.videoDisposable = nil self.videoSource = nil self.videoSpec = nil } if component.isSelected || component.isSpeaking { let selectedBorderView: UIImageView if let current = self.selectedBorderView { selectedBorderView = current speakingAlphaTransition.setTintColor(layer: selectedBorderView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } else { selectedBorderView = UIImageView() self.selectedBorderView = selectedBorderView selectedBorderView.alpha = 0.0 self.extractedContainerView.contentView.addSubview(selectedBorderView) selectedBorderView.image = View.selectedBorderImage selectedBorderView.frame = CGRect(origin: CGPoint(), size: availableSize) speakingAlphaTransition.setAlpha(view: selectedBorderView, alpha: 1.0) ComponentTransition.immediate.setTintColor(layer: selectedBorderView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) } } else if let selectedBorderView = self.selectedBorderView { if !speakingAlphaTransition.animation.isImmediate { if selectedBorderView.alpha != 0.0 { speakingAlphaTransition.setAlpha(view: selectedBorderView, alpha: 0.0, completion: { [weak self, weak selectedBorderView] completed in guard let self, let component = self.component, let selectedBorderView, self.selectedBorderView === selectedBorderView, completed else { return } if !component.isSelected && !component.isSpeaking { selectedBorderView.removeFromSuperview() self.selectedBorderView = nil } }) } } else { self.selectedBorderView = nil selectedBorderView.removeFromSuperview() } } if let selectedBorderView = self.selectedBorderView { transition.setFrame(view: selectedBorderView, frame: CGRect(origin: CGPoint(), size: availableSize)) } return availableSize } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } final class VideoChatExpandedParticipantThumbnailsComponent: Component { final class Participant: Equatable { struct Key: Hashable { var id: GroupCallParticipantsContext.Participant.Id var isPresentation: Bool init(id: GroupCallParticipantsContext.Participant.Id, isPresentation: Bool) { self.id = id self.isPresentation = isPresentation } } let participant: GroupCallParticipantsContext.Participant let isPresentation: Bool var key: Key { return Key(id: self.participant.id, isPresentation: self.isPresentation) } init( participant: GroupCallParticipantsContext.Participant, isPresentation: Bool ) { self.participant = participant self.isPresentation = isPresentation } static func ==(lhs: Participant, rhs: Participant) -> Bool { if lhs === rhs { return true } if lhs.participant != rhs.participant { return false } if lhs.isPresentation != rhs.isPresentation { return false } return true } } let call: VideoChatCall let theme: PresentationTheme let displayVideo: Bool let participants: [Participant] let selectedParticipant: Participant.Key? let speakingParticipants: Set let interfaceOrientation: UIInterfaceOrientation let updateSelectedParticipant: (Participant.Key) -> Void let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? init( call: VideoChatCall, theme: PresentationTheme, displayVideo: Bool, participants: [Participant], selectedParticipant: Participant.Key?, speakingParticipants: Set, interfaceOrientation: UIInterfaceOrientation, updateSelectedParticipant: @escaping (Participant.Key) -> Void, contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)? ) { self.call = call self.theme = theme self.displayVideo = displayVideo self.participants = participants self.selectedParticipant = selectedParticipant self.speakingParticipants = speakingParticipants self.interfaceOrientation = interfaceOrientation self.updateSelectedParticipant = updateSelectedParticipant self.contextAction = contextAction } static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool { if lhs.call != rhs.call { return false } if lhs.theme !== rhs.theme { return false } if lhs.displayVideo != rhs.displayVideo { return false } if lhs.participants != rhs.participants { return false } if lhs.selectedParticipant != rhs.selectedParticipant { return false } if lhs.speakingParticipants != rhs.speakingParticipants { return false } if lhs.interfaceOrientation != rhs.interfaceOrientation { return false } if (lhs.contextAction == nil) != (rhs.contextAction == nil) { return false } return true } private final class ScrollView: UIScrollView { override func touchesShouldCancel(in view: UIView) -> Bool { return true } } private struct ItemLayout { let containerSize: CGSize let containerInsets: UIEdgeInsets let itemCount: Int let itemSize: CGSize let itemSpacing: CGFloat let contentSize: CGSize init(containerSize: CGSize, containerInsets: UIEdgeInsets, itemCount: Int) { self.containerSize = containerSize self.containerInsets = containerInsets self.itemCount = itemCount self.itemSize = CGSize(width: 84.0, height: 84.0) self.itemSpacing = 6.0 let itemsWidth: CGFloat = CGFloat(itemCount) * self.itemSize.width + CGFloat(max(itemCount - 1, 0)) * self.itemSpacing self.contentSize = CGSize(width: self.containerInsets.left + self.containerInsets.right + itemsWidth, height: self.containerInsets.top + self.containerInsets.bottom + self.itemSize.height) } func frame(at index: Int) -> CGRect { let frame = CGRect(origin: CGPoint(x: self.containerInsets.left + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: self.containerInsets.top), size: self.itemSize) return frame } func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) { if self.itemCount == 0 { return (0, -1) } let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: 0.0) var minVisibleRow = Int(floor((offsetRect.minX) / (self.itemSize.width + self.itemSpacing))) minVisibleRow = max(0, minVisibleRow) let maxVisibleRow = Int(ceil((offsetRect.maxX) / (self.itemSize.width + self.itemSpacing))) let minVisibleIndex = minVisibleRow let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) - 1) return (minVisibleIndex, maxVisibleIndex) } } private final class VisibleItem { let view = ComponentView() init() { } } final class View: UIView, UIScrollViewDelegate { private let scrollView: ScrollView private var component: VideoChatExpandedParticipantThumbnailsComponent? private var isUpdating: Bool = false private var ignoreScrolling: Bool = false private var itemLayout: ItemLayout? private var visibleItems: [Participant.Key: VisibleItem] = [:] override init(frame: CGRect) { self.scrollView = ScrollView() super.init(frame: frame) self.scrollView.delaysContentTouches = false self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false self.scrollView.contentInsetAdjustmentBehavior = .never if #available(iOS 13.0, *) { self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false } self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.alwaysBounceVertical = false self.scrollView.scrollsToTop = false self.scrollView.delegate = self self.scrollView.clipsToBounds = true self.addSubview(self.scrollView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreScrolling { self.updateScrolling(transition: .immediate) } } private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } var validListItemIds: [Participant.Key] = [] let visibleListItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds) if visibleListItemRange.maxIndex >= visibleListItemRange.minIndex { for i in visibleListItemRange.minIndex ... visibleListItemRange.maxIndex { let participant = component.participants[i] validListItemIds.append(participant.key) var itemTransition = transition let itemView: VisibleItem if let current = self.visibleItems[participant.key] { itemView = current } else { itemTransition = itemTransition.withAnimation(.none) itemView = VisibleItem() self.visibleItems[participant.key] = itemView } let itemFrame = itemLayout.frame(at: i) let participantKey = participant.key var isSpeaking = false if let participantPeer = participant.participant.peer { isSpeaking = component.speakingParticipants.contains(participantPeer.id) } let _ = itemView.view.update( transition: itemTransition, component: AnyComponent(VideoChatParticipantThumbnailComponent( call: component.call, theme: component.theme, participant: participant.participant, isPresentation: participant.isPresentation, isSelected: component.selectedParticipant == participant.key, isSpeaking: isSpeaking, displayVideo: component.displayVideo, interfaceOrientation: component.interfaceOrientation, action: { [weak self] in guard let self, let component = self.component else { return } component.updateSelectedParticipant(participantKey) }, contextAction: component.contextAction )), environment: {}, containerSize: itemFrame.size ) if let itemComponentView = itemView.view.view { if itemComponentView.superview == nil { itemComponentView.clipsToBounds = true self.scrollView.addSubview(itemComponentView) if !transition.animation.isImmediate { itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) transition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) } } transition.setFrame(view: itemComponentView, frame: itemFrame) } } } var removedListItemIds: [Participant.Key] = [] for (itemId, itemView) in self.visibleItems { if !validListItemIds.contains(itemId) { removedListItemIds.append(itemId) if let itemComponentView = itemView.view.view { if !transition.animation.isImmediate { transition.setScale(view: itemComponentView, scale: 0.001) itemComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak itemComponentView] _ in itemComponentView?.removeFromSuperview() }) } else { itemComponentView.removeFromSuperview() } } } } for itemId in removedListItemIds { self.visibleItems.removeValue(forKey: itemId) } } func update(component: VideoChatExpandedParticipantThumbnailsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false } self.component = component let itemLayout = ItemLayout( containerSize: availableSize, containerInsets: UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0), itemCount: component.participants.count ) self.itemLayout = itemLayout let size = CGSize(width: availableSize.width, height: itemLayout.contentSize.height) self.ignoreScrolling = true if self.scrollView.bounds.size != size { transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)) } let contentSize = CGSize(width: itemLayout.contentSize.width, height: size.height) if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } self.ignoreScrolling = false self.updateScrolling(transition: transition) return size } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }