diff --git a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift index 1e3cf35963..7927876f32 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatExpandedParticipantThumbnailsComponent.swift @@ -134,10 +134,14 @@ final class VideoChatParticipantThumbnailComponent: Component { if transition.animation.isImmediate { speakingAlphaTransition = .immediate } else { - if !wasSpeaking { - speakingAlphaTransition = .easeInOut(duration: 0.1) + if let previousComponent, previousComponent.isSelected == component.isSelected { + if !wasSpeaking { + speakingAlphaTransition = .easeInOut(duration: 0.1) + } else { + speakingAlphaTransition = .easeInOut(duration: 0.25) + } } else { - speakingAlphaTransition = .easeInOut(duration: 0.25) + speakingAlphaTransition = .immediate } } @@ -168,8 +172,7 @@ final class VideoChatParticipantThumbnailComponent: Component { transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, - isFilled: true, - isMuted: component.participant.muteState != nil + content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking) )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) @@ -182,8 +185,6 @@ final class VideoChatParticipantThumbnailComponent: Component { 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) - - speakingAlphaTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white) } let titleSize = self.title.update( @@ -203,8 +204,6 @@ final class VideoChatParticipantThumbnailComponent: Component { } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) - - speakingAlphaTransition.setTintColor(layer: titleView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : .white) } if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { @@ -330,11 +329,13 @@ final class VideoChatParticipantThumbnailComponent: Component { } else { selectedBorderView = UIImageView() self.selectedBorderView = selectedBorderView + selectedBorderView.alpha = 0.0 self.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 { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift index fd78bc6907..3bc92c4bcf 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift @@ -6,50 +6,49 @@ import MultilineTextComponent import TelegramPresentationData import AppBundle import LottieComponent +import BundleIconComponent final class VideoChatMuteIconComponent: Component { + enum Content: Equatable { + case mute(isFilled: Bool, isMuted: Bool) + case screenshare + } + let color: UIColor - let isFilled: Bool - let isMuted: Bool + let content: Content init( color: UIColor, - isFilled: Bool, - isMuted: Bool + content: Content ) { self.color = color - self.isFilled = isFilled - self.isMuted = isMuted + self.content = content } static func ==(lhs: VideoChatMuteIconComponent, rhs: VideoChatMuteIconComponent) -> Bool { if lhs.color != rhs.color { return false } - if lhs.isFilled != rhs.isFilled { - return false - } - if lhs.isMuted != rhs.isMuted { + if lhs.content != rhs.content { return false } return true } final class View: HighlightTrackingButton { - private let icon: VoiceChatMicrophoneNode + private var icon: VoiceChatMicrophoneNode? + private var scheenshareIcon: ComponentView? private var component: VideoChatMuteIconComponent? private var isUpdating: Bool = false private var contentImage: UIImage? - var iconView: UIView { - return self.icon.view + var iconView: UIView? { + return self.icon?.view } override init(frame: CGRect) { - self.icon = VoiceChatMicrophoneNode() - super.init(frame: frame) } @@ -65,14 +64,59 @@ final class VideoChatMuteIconComponent: Component { self.component = component - let animationSize = availableSize - - let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) - if self.icon.view.superview == nil { - self.addSubview(self.icon.view) + if case let .mute(isFilled, isMuted) = component.content { + let icon: VoiceChatMicrophoneNode + if let current = self.icon { + icon = current + } else { + icon = VoiceChatMicrophoneNode() + self.icon = icon + self.addSubview(icon.view) + } + + let animationSize = availableSize + let animationFrame = animationSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) + transition.setFrame(view: icon.view, frame: animationFrame) + icon.update(state: VoiceChatMicrophoneNode.State(muted: isMuted, filled: isFilled, color: component.color), animated: !transition.animation.isImmediate) + } else { + if let icon = self.icon { + self.icon = nil + icon.view.removeFromSuperview() + } + } + + if case .screenshare = component.content { + let scheenshareIcon: ComponentView + if let current = self.scheenshareIcon { + scheenshareIcon = current + } else { + scheenshareIcon = ComponentView() + self.scheenshareIcon = scheenshareIcon + } + let scheenshareIconSize = scheenshareIcon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Call/StatusScreen", + tintColor: component.color + )), + environment: {}, + containerSize: availableSize + ) + let scheenshareIconFrame = scheenshareIconSize.centered(in: CGRect(origin: CGPoint(), size: availableSize)) + if let scheenshareIconView = scheenshareIcon.view { + if scheenshareIconView.superview == nil { + self.addSubview(scheenshareIconView) + } + transition.setPosition(view: scheenshareIconView, position: scheenshareIconFrame.center) + transition.setBounds(view: scheenshareIconView, bounds: CGRect(origin: CGPoint(), size: scheenshareIconFrame.size)) + transition.setScale(view: scheenshareIconView, scale: 1.5) + } + } else { + if let scheenshareIcon = self.scheenshareIcon { + self.scheenshareIcon = nil + scheenshareIcon.view?.removeFromSuperview() + } } - transition.setFrame(view: self.icon.view, frame: animationFrame) - self.icon.update(state: VoiceChatMicrophoneNode.State(muted: component.isMuted, filled: component.isFilled, color: component.color), animated: !transition.animation.isImmediate) return availableSize } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift index 74cab83213..46d54505d8 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift @@ -61,8 +61,7 @@ final class VideoChatParticipantStatusComponent: Component { transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, - isFilled: false, - isMuted: component.isMuted && !component.isSpeaking + content: .mute(isFilled: false, isMuted: component.isMuted && !component.isSpeaking) )), environment: {}, containerSize: CGSize(width: 36.0, height: 36.0) @@ -80,7 +79,9 @@ final class VideoChatParticipantStatusComponent: Component { } else { tintTransition = .immediate } - tintTransition.setTintColor(layer: muteStatusView.iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : UIColor(white: 1.0, alpha: 0.4)) + if let iconView = muteStatusView.iconView { + tintTransition.setTintColor(layer: iconView.layer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : UIColor(white: 1.0, alpha: 0.4)) + } } return size diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index 211064e0c5..c63fec2555 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -40,7 +40,9 @@ final class VideoChatParticipantVideoComponent: Component { let isPresentation: Bool let isSpeaking: Bool let isExpanded: Bool - let bottomInset: CGFloat + let isUIHidden: Bool + let contentInsets: UIEdgeInsets + let controlInsets: UIEdgeInsets weak var rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView? let action: (() -> Void)? @@ -50,7 +52,9 @@ final class VideoChatParticipantVideoComponent: Component { isPresentation: Bool, isSpeaking: Bool, isExpanded: Bool, - bottomInset: CGFloat, + isUIHidden: Bool, + contentInsets: UIEdgeInsets, + controlInsets: UIEdgeInsets, rootVideoLoadingEffectView: VideoChatVideoLoadingEffectView?, action: (() -> Void)? ) { @@ -59,7 +63,9 @@ final class VideoChatParticipantVideoComponent: Component { self.isPresentation = isPresentation self.isSpeaking = isSpeaking self.isExpanded = isExpanded - self.bottomInset = bottomInset + self.isUIHidden = isUIHidden + self.contentInsets = contentInsets + self.controlInsets = controlInsets self.rootVideoLoadingEffectView = rootVideoLoadingEffectView self.action = action } @@ -77,7 +83,13 @@ final class VideoChatParticipantVideoComponent: Component { if lhs.isExpanded != rhs.isExpanded { return false } - if lhs.bottomInset != rhs.bottomInset { + if lhs.isUIHidden != rhs.isUIHidden { + return false + } + if lhs.contentInsets != rhs.contentInsets { + return false + } + if lhs.controlInsets != rhs.controlInsets { return false } if (lhs.action == nil) != (rhs.action == nil) { @@ -153,6 +165,15 @@ final class VideoChatParticipantVideoComponent: Component { self.component = component self.componentState = state + let alphaTransition: ComponentTransition + if !transition.animation.isImmediate { + alphaTransition = .easeInOut(duration: 0.2) + } else { + alphaTransition = .immediate + } + + let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0 + let nameColor = component.participant.peer.nameColor ?? .blue let nameColors = component.call.accountContext.peerNameColors.get(nameColor, dark: true) self.backgroundColor = nameColors.main.withMultiplied(hue: 1.0, saturation: 1.0, brightness: 0.4) @@ -210,25 +231,26 @@ final class VideoChatParticipantVideoComponent: Component { transition: transition, component: AnyComponent(VideoChatMuteIconComponent( color: .white, - isFilled: true, - isMuted: component.participant.muteState != nil + 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 if component.isExpanded { - muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.bottomInset + 1.0 - muteStatusSize.height), size: muteStatusSize) + muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.controlInsets.bottom + 1.0 - muteStatusSize.height), size: muteStatusSize) } else { - muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.bottomInset + 3.0 - muteStatusSize.height), size: muteStatusSize) + muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.controlInsets.bottom + 3.0 - muteStatusSize.height), size: muteStatusSize) } if let muteStatusView = self.muteStatus.view { if muteStatusView.superview == nil { self.addSubview(muteStatusView) + muteStatusView.alpha = controlsAlpha } transition.setPosition(view: muteStatusView, position: muteStatusFrame.center) transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size)) transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7) + alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha) } let titleSize = self.title.update( @@ -241,18 +263,20 @@ final class VideoChatParticipantVideoComponent: Component { ) let titleFrame: CGRect if component.isExpanded { - titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.bottomInset - 8.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 36.0, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height), size: titleSize) } else { - titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.bottomInset - 4.0 - titleSize.height), size: titleSize) + titleFrame = CGRect(origin: CGPoint(x: 29.0, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height), size: titleSize) } if let titleView = self.title.view { if titleView.superview == nil { titleView.layer.anchorPoint = CGPoint() self.addSubview(titleView) + titleView.alpha = controlsAlpha } transition.setPosition(view: titleView, position: titleFrame.origin) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825) + alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } if let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription { @@ -397,7 +421,7 @@ final class VideoChatParticipantVideoComponent: Component { 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)) + let videoResolution = rotatedResolution var rotatedVideoResolution = videoResolution var rotatedVideoFrame = videoFrame @@ -408,6 +432,7 @@ final class VideoChatParticipantVideoComponent: Component { rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center) 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: rotatedVideoFrame.size)) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index 9561b0117c..32649488a0 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -12,21 +12,26 @@ import TelegramPresentationData import PeerListItemComponent final class VideoChatParticipantsComponent: Component { - enum LayoutType: Equatable { - struct Horizontal: Equatable { - var rightColumnWidth: CGFloat - var columnSpacing: CGFloat - var isCentered: Bool + struct Layout: Equatable { + struct Column: Equatable { + var width: CGFloat + var insets: UIEdgeInsets - init(rightColumnWidth: CGFloat, columnSpacing: CGFloat, isCentered: Bool) { - self.rightColumnWidth = rightColumnWidth - self.columnSpacing = columnSpacing - self.isCentered = isCentered + init(width: CGFloat, insets: UIEdgeInsets) { + self.width = width + self.insets = insets } } - case vertical - case horizontal(Horizontal) + var videoColumn: Column? + var mainColumn: Column + var columnSpacing: CGFloat + + init(videoColumn: Column?, mainColumn: Column, columnSpacing: CGFloat) { + self.videoColumn = videoColumn + self.mainColumn = mainColumn + self.columnSpacing = columnSpacing + } } final class Participants: Equatable { @@ -75,10 +80,12 @@ final class VideoChatParticipantsComponent: Component { final class ExpandedVideoState: Equatable { let mainParticipant: VideoParticipantKey let isMainParticipantPinned: Bool + let isUIHidden: Bool - init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool) { + init(mainParticipant: VideoParticipantKey, isMainParticipantPinned: Bool, isUIHidden: Bool) { self.mainParticipant = mainParticipant self.isMainParticipantPinned = isMainParticipantPinned + self.isUIHidden = isUIHidden } static func ==(lhs: ExpandedVideoState, rhs: ExpandedVideoState) -> Bool { @@ -91,6 +98,9 @@ final class VideoChatParticipantsComponent: Component { if lhs.isMainParticipantPinned != rhs.isMainParticipantPinned { return false } + if lhs.isUIHidden != rhs.isUIHidden { + return false + } return true } } @@ -101,12 +111,12 @@ final class VideoChatParticipantsComponent: Component { let expandedVideoState: ExpandedVideoState? let theme: PresentationTheme let strings: PresentationStrings - let layoutType: LayoutType - let collapsedContainerInsets: UIEdgeInsets - let expandedContainerInsets: UIEdgeInsets - let sideInset: CGFloat + let layout: Layout + let expandedInsets: UIEdgeInsets + let safeInsets: UIEdgeInsets let updateMainParticipant: (VideoParticipantKey?) -> Void let updateIsMainParticipantPinned: (Bool) -> Void + let updateIsExpandedUIHidden: (Bool) -> Void init( call: PresentationGroupCall, @@ -115,12 +125,12 @@ final class VideoChatParticipantsComponent: Component { expandedVideoState: ExpandedVideoState?, theme: PresentationTheme, strings: PresentationStrings, - layoutType: LayoutType, - collapsedContainerInsets: UIEdgeInsets, - expandedContainerInsets: UIEdgeInsets, - sideInset: CGFloat, + layout: Layout, + expandedInsets: UIEdgeInsets, + safeInsets: UIEdgeInsets, updateMainParticipant: @escaping (VideoParticipantKey?) -> Void, - updateIsMainParticipantPinned: @escaping (Bool) -> Void + updateIsMainParticipantPinned: @escaping (Bool) -> Void, + updateIsExpandedUIHidden: @escaping (Bool) -> Void ) { self.call = call self.participants = participants @@ -128,12 +138,12 @@ final class VideoChatParticipantsComponent: Component { self.expandedVideoState = expandedVideoState self.theme = theme self.strings = strings - self.layoutType = layoutType - self.collapsedContainerInsets = collapsedContainerInsets - self.expandedContainerInsets = expandedContainerInsets - self.sideInset = sideInset + self.layout = layout + self.expandedInsets = expandedInsets + self.safeInsets = safeInsets self.updateMainParticipant = updateMainParticipant self.updateIsMainParticipantPinned = updateIsMainParticipantPinned + self.updateIsExpandedUIHidden = updateIsExpandedUIHidden } static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool { @@ -152,16 +162,13 @@ final class VideoChatParticipantsComponent: Component { if lhs.strings !== rhs.strings { return false } - if lhs.layoutType != rhs.layoutType { + if lhs.layout != rhs.layout { return false } - if lhs.collapsedContainerInsets != rhs.collapsedContainerInsets { + if lhs.expandedInsets != rhs.expandedInsets { return false } - if lhs.expandedContainerInsets != rhs.expandedContainerInsets { - return false - } - if lhs.sideInset != rhs.sideInset { + if lhs.safeInsets != rhs.safeInsets { return false } return true @@ -178,40 +185,84 @@ final class VideoChatParticipantsComponent: Component { let containerSize: CGSize let sideInset: CGFloat let itemCount: Int + let isDedicatedColumn: Bool let itemSize: CGSize let itemSpacing: CGFloat let lastItemSize: CGFloat + let lastRowItemCount: Int + let lastRowItemSize: CGFloat let itemsPerRow: Int + let rowCount: Int - init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int) { + init(containerSize: CGSize, sideInset: CGFloat, itemCount: Int, isDedicatedColumn: Bool) { self.containerSize = containerSize self.sideInset = sideInset self.itemCount = itemCount + self.isDedicatedColumn = isDedicatedColumn let width: CGFloat = containerSize.width - sideInset * 2.0 self.itemSpacing = 4.0 let itemsPerRow: Int - if itemCount == 1 { - itemsPerRow = 1 + if isDedicatedColumn { + if itemCount <= 2 { + itemsPerRow = 1 + } else { + itemsPerRow = 2 + } } else { - itemsPerRow = 2 + if itemCount == 1 { + itemsPerRow = 1 + } else { + itemsPerRow = 2 + } } self.itemsPerRow = Int(itemsPerRow) let itemWidth = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / CGFloat(itemsPerRow)) let itemHeight = min(180.0, itemWidth) - self.itemSize = CGSize(width: itemWidth, height: itemHeight) + var itemSize = CGSize(width: itemWidth, height: itemHeight) + + self.rowCount = itemCount / self.itemsPerRow + ((itemCount % self.itemsPerRow) != 0 ? 1 : 0) + + if isDedicatedColumn && itemCount != 0 { + let contentHeight = itemSize.height * CGFloat(self.rowCount) + self.itemSpacing * CGFloat(max(0, self.rowCount - 1)) + if contentHeight < containerSize.height { + itemSize.height = (containerSize.height - self.itemSpacing * CGFloat(max(0, self.rowCount - 1))) / CGFloat(self.rowCount) + itemSize.height = floor(itemSize.height) + } + } + + self.itemSize = itemSize self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) + var lastRowItemCount = itemCount % self.itemsPerRow + if lastRowItemCount == 0 { + lastRowItemCount = self.itemsPerRow + } + self.lastRowItemCount = lastRowItemCount + self.lastRowItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(lastRowItemCount - 1) } func frame(at index: Int) -> CGRect { let row = index / self.itemsPerRow let column = index % self.itemsPerRow - let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height)) + let itemWidth: CGFloat + if row == self.rowCount - 1 && column == self.lastRowItemCount - 1 { + itemWidth = self.lastRowItemSize + } else if column == self.itemsPerRow - 1 { + if row == self.rowCount - 1 { + itemWidth = self.lastRowItemSize + } else { + itemWidth = self.lastItemSize + } + } else { + itemWidth = self.itemSize.width + } + + let frame = CGRect(origin: CGPoint(x: self.sideInset + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: itemWidth, height: itemSize.height)) return frame } @@ -237,21 +288,37 @@ final class VideoChatParticipantsComponent: Component { struct ExpandedGrid { let containerSize: CGSize - let layoutType: LayoutType - let containerInsets: UIEdgeInsets + let layout: Layout + let expandedInsets: UIEdgeInsets + let isUIHidden: Bool - init(containerSize: CGSize, layoutType: LayoutType, containerInsets: UIEdgeInsets) { + init(containerSize: CGSize, layout: Layout, expandedInsets: UIEdgeInsets, isUIHidden: Bool) { self.containerSize = containerSize - self.layoutType = layoutType - self.containerInsets = containerInsets + self.layout = layout + self.expandedInsets = expandedInsets + self.isUIHidden = isUIHidden } func itemContainerFrame() -> CGRect { - switch self.layoutType { - case .vertical: - return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top - containerInsets.bottom)) - case .horizontal: - return CGRect(origin: CGPoint(x: self.containerInsets.left, y: self.containerInsets.top), size: CGSize(width: self.containerSize.width - self.containerInsets.left - self.containerInsets.right, height: self.containerSize.height - self.containerInsets.top)) + let containerInsets: UIEdgeInsets + if self.isUIHidden { + containerInsets = UIEdgeInsets() + } else { + containerInsets = self.expandedInsets + } + + if self.layout.videoColumn != nil { + return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom)) + } else { + return CGRect(origin: CGPoint(x: containerInsets.left, y: containerInsets.top), size: CGSize(width: self.containerSize.width - containerInsets.left - containerInsets.right, height: self.containerSize.height - containerInsets.top - containerInsets.bottom)) + } + } + + func itemContainerInsets() -> UIEdgeInsets { + if self.isUIHidden { + return self.expandedInsets + } else { + return UIEdgeInsets() } } } @@ -306,9 +373,10 @@ final class VideoChatParticipantsComponent: Component { } let containerSize: CGSize - let layoutType: LayoutType - let collapsedContainerInsets: UIEdgeInsets - let sideInset: CGFloat + let layout: Layout + let isUIHidden: Bool + let expandedInsets: UIEdgeInsets + let safeInsets: UIEdgeInsets let grid: Grid let expandedGrid: ExpandedGrid let list: List @@ -320,32 +388,41 @@ final class VideoChatParticipantsComponent: Component { let scrollClippingFrame: CGRect let separateVideoScrollClippingFrame: CGRect - init(containerSize: CGSize, layoutType: LayoutType, sideInset: CGFloat, collapsedContainerInsets: UIEdgeInsets, expandedContainerInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) { + init(containerSize: CGSize, layout: Layout, isUIHidden: Bool, expandedInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, gridItemCount: Int, listItemCount: Int, listItemHeight: CGFloat, listTrailingItemHeight: CGFloat) { self.containerSize = containerSize - self.layoutType = layoutType - self.collapsedContainerInsets = collapsedContainerInsets - self.sideInset = sideInset + self.layout = layout + self.isUIHidden = isUIHidden + self.expandedInsets = expandedInsets + self.safeInsets = safeInsets + let listWidth: CGFloat = layout.mainColumn.width let gridWidth: CGFloat - let listWidth: CGFloat - switch layoutType { - case .vertical: - listWidth = containerSize.width - sideInset * 2.0 + let gridSideInset: CGFloat + let gridContainerHeight: CGFloat + if let videoColumn = layout.videoColumn { + gridWidth = videoColumn.width + gridSideInset = videoColumn.insets.left + gridContainerHeight = containerSize.height - videoColumn.insets.top - videoColumn.insets.bottom + } else { gridWidth = listWidth - case let .horizontal(horizontal): - listWidth = horizontal.rightColumnWidth - gridWidth = max(10.0, containerSize.width - sideInset * 2.0 - horizontal.rightColumnWidth - horizontal.columnSpacing) + gridSideInset = layout.mainColumn.insets.left + gridContainerHeight = containerSize.height } - self.grid = Grid(containerSize: CGSize(width: gridWidth, height: containerSize.height), sideInset: 0.0, itemCount: gridItemCount) - self.expandedGrid = ExpandedGrid(containerSize: containerSize, layoutType: layoutType, containerInsets: expandedContainerInsets) - self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: 0.0, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight) + self.grid = Grid(containerSize: CGSize(width: gridWidth, height: gridContainerHeight), sideInset: gridSideInset, itemCount: gridItemCount, isDedicatedColumn: layout.videoColumn != nil) + self.list = List(containerSize: CGSize(width: listWidth, height: containerSize.height), sideInset: layout.mainColumn.insets.left, itemCount: listItemCount, itemHeight: listItemHeight, trailingItemHeight: listTrailingItemHeight) self.spacing = 4.0 - self.gridOffsetY = collapsedContainerInsets.top + if let videoColumn = layout.videoColumn, !isUIHidden { + self.expandedGrid = ExpandedGrid(containerSize: CGSize(width: videoColumn.width + expandedInsets.left, height: containerSize.height), layout: layout, expandedInsets: UIEdgeInsets(top: expandedInsets.top, left: expandedInsets.left, bottom: expandedInsets.bottom, right: 0.0), isUIHidden: isUIHidden) + } else { + self.expandedGrid = ExpandedGrid(containerSize: containerSize, layout: layout, expandedInsets: expandedInsets, isUIHidden: isUIHidden) + } + + self.gridOffsetY = layout.mainColumn.insets.top var listOffsetY: CGFloat = self.gridOffsetY - if case .vertical = layoutType { + if layout.videoColumn == nil { if self.grid.itemCount != 0 { listOffsetY += self.grid.contentHeight() listOffsetY += self.spacing @@ -353,55 +430,54 @@ final class VideoChatParticipantsComponent: Component { } self.listOffsetY = listOffsetY - switch layoutType { - case .vertical: - self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.sideInset, y: collapsedContainerInsets.top), size: CGSize(width: containerSize.width - self.sideInset * 2.0, height: containerSize.height - collapsedContainerInsets.top - collapsedContainerInsets.bottom)) - self.listFrame = CGRect(origin: CGPoint(), size: containerSize) + if let videoColumn = layout.videoColumn { + let columnsWidth: CGFloat = videoColumn.width + layout.columnSpacing + layout.mainColumn.width + let columnsSideInset: CGFloat = floorToScreenPixels((containerSize.width - columnsWidth) * 0.5) - self.separateVideoGridFrame = CGRect(origin: CGPoint(x: -containerSize.width, y: 0.0), size: containerSize) - self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top)) - case let .horizontal(horizontal): - if horizontal.isCentered { - self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - horizontal.rightColumnWidth) * 0.5), y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height)) - } else { - self.listFrame = CGRect(origin: CGPoint(x: containerSize.width - self.sideInset - horizontal.rightColumnWidth, y: 0.0), size: CGSize(width: horizontal.rightColumnWidth, height: containerSize.height)) + var separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) + + var listFrame = CGRect(origin: CGPoint(x: separateVideoGridFrame.maxX + layout.columnSpacing, y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) + if isUIHidden { + listFrame.origin.x += columnsSideInset + layout.mainColumn.width + separateVideoGridFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - columnsWidth) * 0.5), y: 0.0), size: CGSize(width: columnsWidth, height: containerSize.height)) } - self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - collapsedContainerInsets.top)) - self.separateVideoGridFrame = CGRect(origin: CGPoint(x: min(self.sideInset, self.scrollClippingFrame.minX - horizontal.columnSpacing - gridWidth), y: 0.0), size: CGSize(width: gridWidth, height: containerSize.height)) - self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: collapsedContainerInsets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - collapsedContainerInsets.top)) + self.separateVideoGridFrame = separateVideoGridFrame + self.listFrame = listFrame + + self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: videoColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - videoColumn.insets.top)) + self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.listFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) + } else { + self.listFrame = CGRect(origin: CGPoint(x: floor((containerSize.width - listWidth) * 0.5), y: 0.0), size: CGSize(width: listWidth, height: containerSize.height)) + self.scrollClippingFrame = CGRect(origin: CGPoint(x: self.listFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: listWidth, height: containerSize.height - layout.mainColumn.insets.top - layout.mainColumn.insets.bottom)) + + self.separateVideoGridFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 0.0, height: containerSize.height)) + self.separateVideoScrollClippingFrame = CGRect(origin: CGPoint(x: self.separateVideoGridFrame.minX, y: layout.mainColumn.insets.top), size: CGSize(width: self.separateVideoGridFrame.width, height: containerSize.height - layout.mainColumn.insets.top)) } } func contentHeight() -> CGFloat { var result: CGFloat = self.gridOffsetY - switch self.layoutType { - case .vertical: + if self.layout.videoColumn == nil { if self.grid.itemCount != 0 { result += self.grid.contentHeight() result += self.spacing } - case .horizontal: - break } result += self.list.contentHeight() - result += self.collapsedContainerInsets.bottom + result += self.layout.mainColumn.insets.bottom result += 24.0 return result } func separateVideoGridContentHeight() -> CGFloat { var result: CGFloat = self.gridOffsetY - switch self.layoutType { - case .vertical: - break - case .horizontal: + if let videoColumn = self.layout.videoColumn { if self.grid.itemCount != 0 { result += self.grid.contentHeight() } + result += videoColumn.insets.bottom } - result += self.collapsedContainerInsets.bottom - result += 24.0 return result } @@ -414,11 +490,10 @@ final class VideoChatParticipantsComponent: Component { } func gridItemContainerFrame() -> CGRect { - switch self.layoutType { - case .vertical: - return CGRect(origin: CGPoint(x: self.sideInset, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.grid.contentHeight())) - case .horizontal: + if let _ = self.layout.videoColumn { return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.grid.contentHeight())) + } else { + return CGRect(origin: CGPoint(x: 0.0, y: self.gridOffsetY), size: CGSize(width: self.containerSize.width, height: self.grid.contentHeight())) } } @@ -431,11 +506,10 @@ final class VideoChatParticipantsComponent: Component { } func listItemContainerFrame() -> CGRect { - switch self.layoutType { - case .vertical: - return CGRect(origin: CGPoint(x: self.sideInset, y: self.listOffsetY), size: CGSize(width: self.containerSize.width - self.sideInset * 2.0, height: self.list.contentHeight())) - case .horizontal: - return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.listFrame.width, height: self.list.contentHeight())) + if let _ = self.layout.videoColumn { + return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.separateVideoGridFrame.width, height: self.list.contentHeight())) + } else { + return CGRect(origin: CGPoint(x: 0.0, y: self.listOffsetY), size: CGSize(width: self.containerSize.width, height: self.list.contentHeight())) } } @@ -620,6 +694,13 @@ final class VideoChatParticipantsComponent: Component { return } + let alphaTransition: ComponentTransition + if !transition.animation.isImmediate { + alphaTransition = .easeInOut(duration: 0.2) + } else { + alphaTransition = .immediate + } + let gridWasEmpty = self.appliedGridIsEmpty let gridIsEmpty = self.gridParticipants.isEmpty self.appliedGridIsEmpty = gridIsEmpty @@ -637,27 +718,26 @@ final class VideoChatParticipantsComponent: Component { if component.expandedVideoState != nil { expandedGridItemContainerFrame = itemLayout.expandedGrid.itemContainerFrame() } else { - switch itemLayout.layoutType { - case .vertical: - expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) - - if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top { - expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y - expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top - } - if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - component.collapsedContainerInsets.bottom { - expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - component.collapsedContainerInsets.bottom) - } - case .horizontal: + if let videoColumn = itemLayout.layout.videoColumn { expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: itemLayout.separateVideoScrollClippingFrame.minX, dy: 0.0).offsetBy(dx: 0.0, dy: -self.separateVideoScrollView.bounds.minY) - if expandedGridItemContainerFrame.origin.y < component.collapsedContainerInsets.top { - expandedGridItemContainerFrame.size.height -= component.collapsedContainerInsets.top - expandedGridItemContainerFrame.origin.y - expandedGridItemContainerFrame.origin.y = component.collapsedContainerInsets.top + if expandedGridItemContainerFrame.origin.y < videoColumn.insets.top { + expandedGridItemContainerFrame.size.height -= videoColumn.insets.top - expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.origin.y = videoColumn.insets.top } if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height { expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height) } + } else { + expandedGridItemContainerFrame = itemLayout.gridItemContainerFrame().offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + + if expandedGridItemContainerFrame.origin.y < itemLayout.layout.mainColumn.insets.top { + expandedGridItemContainerFrame.size.height -= itemLayout.layout.mainColumn.insets.top - expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.origin.y = itemLayout.layout.mainColumn.insets.top + } + if expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height > itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom { + expandedGridItemContainerFrame.size.height -= (expandedGridItemContainerFrame.origin.y + expandedGridItemContainerFrame.height) - (itemLayout.containerSize.height - itemLayout.layout.mainColumn.insets.bottom) + } } if expandedGridItemContainerFrame.size.height < 0.0 { expandedGridItemContainerFrame.size.height = 0.0 @@ -670,10 +750,9 @@ final class VideoChatParticipantsComponent: Component { var validGridItemIndices: [Int] = [] let visibleGridItemRange: (minIndex: Int, maxIndex: Int) - switch itemLayout.layoutType { - case .vertical: + if itemLayout.layout.videoColumn == nil { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.scrollView.bounds) - case .horizontal: + } else { visibleGridItemRange = itemLayout.visibleGridItemRange(for: self.separateVideoScrollView.bounds) } if visibleGridItemRange.maxIndex >= visibleGridItemRange.minIndex { @@ -707,8 +786,14 @@ final class VideoChatParticipantsComponent: Component { } var isItemExpanded = false - if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { - isItemExpanded = true + var isItemUIHidden = false + if let expandedVideoState = component.expandedVideoState { + if expandedVideoState.mainParticipant == videoParticipantKey { + isItemExpanded = true + } + if expandedVideoState.isUIHidden { + isItemUIHidden = true + } } var suppressItemExpansionCollapseAnimation = false @@ -734,14 +819,28 @@ final class VideoChatParticipantsComponent: Component { itemFrame = itemLayout.gridItemFrame(at: index) } - var itemBottomInset: CGFloat = isItemExpanded ? 96.0 : 0.0 - switch itemLayout.layoutType { - case .vertical: - break - case .horizontal: - if isItemExpanded { - itemBottomInset += itemLayout.expandedGrid.containerInsets.bottom - } + let itemContentInsets: UIEdgeInsets + if isItemExpanded { + itemContentInsets = itemLayout.expandedGrid.itemContainerInsets() + } else { + itemContentInsets = UIEdgeInsets() + } + + var itemControlInsets: UIEdgeInsets + if isItemExpanded { + itemControlInsets = itemContentInsets + itemControlInsets.bottom = max(itemControlInsets.bottom, 96.0) + } else { + itemControlInsets = itemContentInsets + } + + let itemAlpha: CGFloat + if isItemExpanded { + itemAlpha = 1.0 + } else if component.expandedVideoState != nil && itemLayout.layout.videoColumn != nil { + itemAlpha = 0.0 + } else { + itemAlpha = 1.0 } let _ = itemView.view.update( @@ -752,14 +851,16 @@ final class VideoChatParticipantsComponent: Component { isPresentation: videoParticipant.isPresentation, isSpeaking: component.speakingParticipants.contains(videoParticipant.participant.peer.id), isExpanded: isItemExpanded, - bottomInset: itemBottomInset, + isUIHidden: isItemUIHidden, + contentInsets: itemContentInsets, + controlInsets: itemControlInsets, rootVideoLoadingEffectView: self.rootVideoLoadingEffectView, action: { [weak self] in guard let self, let component = self.component else { return } - if component.expandedVideoState?.mainParticipant == videoParticipantKey { - component.updateMainParticipant(nil) + if let expandedVideoState = component.expandedVideoState, expandedVideoState.mainParticipant == videoParticipantKey { + component.updateIsExpandedUIHidden(!expandedVideoState.isUIHidden) } else { component.updateMainParticipant(videoParticipantKey) } @@ -770,6 +871,8 @@ final class VideoChatParticipantsComponent: Component { ) if let itemComponentView = itemView.view.view { if itemComponentView.superview == nil { + itemComponentView.layer.allowsGroupOpacity = true + if isItemExpanded { if let expandedThumbnailsView = self.expandedThumbnailsView?.view { self.expandedGridItemContainer.insertSubview(itemComponentView, belowSubview: expandedThumbnailsView) @@ -781,12 +884,13 @@ final class VideoChatParticipantsComponent: Component { } itemComponentView.frame = itemFrame + itemComponentView.alpha = itemAlpha if !resultingItemTransition.animation.isImmediate { resultingItemTransition.animateScale(view: itemComponentView, from: 0.001, to: 1.0) } - if !resultingItemTransition.animation.isImmediate { - itemComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + if !resultingItemTransition.animation.isImmediate && itemAlpha != 0.0 { + itemComponentView.layer.animateAlpha(from: 0.0, to: itemAlpha, duration: 0.1) } } else if isItemExpanded && itemComponentView.superview != self.expandedGridItemContainer { let fromFrame = itemComponentView.convert(itemComponentView.bounds, to: self.expandedGridItemContainer) @@ -820,6 +924,14 @@ final class VideoChatParticipantsComponent: Component { if !itemView.isCollapsing { resultingItemTransition.setPosition(view: itemComponentView, position: itemFrame.center) resultingItemTransition.setBounds(view: itemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + let resultingItemAlphaTransition: ComponentTransition + if !resultingItemTransition.animation.isImmediate { + resultingItemAlphaTransition = alphaTransition + } else { + resultingItemAlphaTransition = .immediate + } + resultingItemAlphaTransition.setAlpha(view: itemComponentView, alpha: itemAlpha) } } } @@ -1017,6 +1129,18 @@ final class VideoChatParticipantsComponent: Component { )) }*/ + let expandedControlsAlpha: CGFloat = expandedVideoState.isUIHidden ? 0.0 : 1.0 + let expandedThumbnailsAlpha: CGFloat = expandedControlsAlpha + /*if itemLayout.layout.videoColumn == nil { + if expandedVideoState.isUIHidden { + expandedThumbnailsAlpha = 0.0 + } else { + expandedThumbnailsAlpha = 1.0 + } + } else { + expandedThumbnailsAlpha = 0.0 + }*/ + var expandedThumbnailsTransition = transition let expandedThumbnailsView: ComponentView if let current = self.expandedThumbnailsView { @@ -1046,16 +1170,11 @@ final class VideoChatParticipantsComponent: Component { environment: {}, containerSize: itemLayout.expandedGrid.itemContainerFrame().size ) - var expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) - switch itemLayout.layoutType { - case .vertical: - break - case .horizontal: - expandedThumbnailsFrame.origin.y -= itemLayout.expandedGrid.containerInsets.bottom - } + let expandedThumbnailsFrame = CGRect(origin: CGPoint(x: 0.0, y: expandedGridItemContainerFrame.height - expandedThumbnailsSize.height), size: expandedThumbnailsSize) if let expandedThumbnailsComponentView = expandedThumbnailsView.view { if expandedThumbnailsComponentView.superview == nil { self.expandedGridItemContainer.addSubview(expandedThumbnailsComponentView) + expandedThumbnailsComponentView.alpha = expandedThumbnailsAlpha let fromReferenceFrame: CGRect if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { @@ -1066,11 +1185,12 @@ final class VideoChatParticipantsComponent: Component { expandedThumbnailsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.maxY - expandedThumbnailsSize.height), size: expandedThumbnailsFrame.size) - if !transition.animation.isImmediate { + if !transition.animation.isImmediate && expandedThumbnailsAlpha != 0.0 { expandedThumbnailsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } transition.setFrame(view: expandedThumbnailsComponentView, frame: expandedThumbnailsFrame) + alphaTransition.setAlpha(view: expandedThumbnailsComponentView, alpha: expandedThumbnailsAlpha) } var expandedControlsTransition = transition @@ -1113,6 +1233,8 @@ final class VideoChatParticipantsComponent: Component { if expandedControlsComponentView.superview == nil { self.expandedGridItemContainer.addSubview(expandedControlsComponentView) + expandedControlsComponentView.alpha = expandedControlsAlpha + let fromReferenceFrame: CGRect if let index = self.gridParticipants.firstIndex(where: { $0.participant.peer.id == expandedVideoState.mainParticipant.id && $0.isPresentation == expandedVideoState.mainParticipant.isPresentation }) { fromReferenceFrame = self.gridItemViewContainer.convert(itemLayout.gridItemFrame(at: index), to: self.expandedGridItemContainer) @@ -1122,11 +1244,12 @@ final class VideoChatParticipantsComponent: Component { expandedControlsComponentView.frame = CGRect(origin: CGPoint(x: fromReferenceFrame.minX - previousExpandedGridItemContainerFrame.minX, y: fromReferenceFrame.minY - previousExpandedGridItemContainerFrame.minY), size: expandedControlsFrame.size) - if !transition.animation.isImmediate { + if !transition.animation.isImmediate && expandedControlsAlpha != 0.0 { expandedControlsComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } } transition.setFrame(view: expandedControlsComponentView, frame: expandedControlsFrame) + alphaTransition.setAlpha(view: expandedControlsComponentView, alpha: expandedControlsAlpha) } } else { if let expandedThumbnailsView = self.expandedThumbnailsView { @@ -1142,7 +1265,7 @@ final class VideoChatParticipantsComponent: Component { let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.maxY - expandedThumbnailsComponentView.bounds.height), size: expandedThumbnailsComponentView.bounds.size) transition.setFrame(view: expandedThumbnailsComponentView, frame: targetThumbnailsFrame) } - expandedThumbnailsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in + expandedThumbnailsComponentView.layer.animateAlpha(from: expandedThumbnailsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedThumbnailsComponentView] _ in expandedThumbnailsComponentView?.removeFromSuperview() }) } else { @@ -1163,7 +1286,7 @@ final class VideoChatParticipantsComponent: Component { let targetThumbnailsFrame = CGRect(origin: CGPoint(x: targetItemFrame.minX, y: targetItemFrame.minY), size: expandedControlsComponentView.bounds.size) transition.setFrame(view: expandedControlsComponentView, frame: targetThumbnailsFrame) } - expandedControlsComponentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in + expandedControlsComponentView.layer.animateAlpha(from: expandedControlsComponentView.alpha, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak expandedControlsComponentView] _ in expandedControlsComponentView?.removeFromSuperview() }) } else { @@ -1272,10 +1395,10 @@ final class VideoChatParticipantsComponent: Component { let itemLayout = ItemLayout( containerSize: availableSize, - layoutType: component.layoutType, - sideInset: component.sideInset, - collapsedContainerInsets: component.collapsedContainerInsets, - expandedContainerInsets: component.expandedContainerInsets, + layout: component.layout, + isUIHidden: component.expandedVideoState?.isUIHidden ?? false, + expandedInsets: component.expandedInsets, + safeInsets: component.safeInsets, gridItemCount: gridParticipants.count, listItemCount: listParticipants.count, listItemHeight: measureListItemSize.height, @@ -1290,9 +1413,9 @@ final class VideoChatParticipantsComponent: Component { cornerRadius: 10.0 )), environment: {}, - containerSize: CGSize(width: itemLayout.list.containerSize.width, height: itemLayout.list.contentHeight()) + containerSize: CGSize(width: itemLayout.listFrame.width - itemLayout.layout.mainColumn.insets.left - itemLayout.layout.mainColumn.insets.right, height: itemLayout.list.contentHeight()) ) - let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: listItemsBackgroundSize) + let listItemsBackgroundFrame = CGRect(origin: CGPoint(x: itemLayout.layout.mainColumn.insets.left, y: 0.0), size: listItemsBackgroundSize) if let listItemsBackgroundView = self.listItemsBackground.view { if listItemsBackgroundView.superview == nil { self.listItemViewContainer.addSubview(listItemsBackgroundView) @@ -1375,12 +1498,11 @@ final class VideoChatParticipantsComponent: Component { self.ignoreScrolling = false - switch component.layoutType { - case .vertical: + if itemLayout.layout.videoColumn == nil { if self.gridItemViewContainer.superview !== self.scrollView { self.scrollView.addSubview(self.gridItemViewContainer) } - case .horizontal: + } else { if self.gridItemViewContainer.superview !== self.separateVideoScrollView { self.separateVideoScrollView.addSubview(self.gridItemViewContainer) } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index f8ec1e3325..1e98e70c51 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -1041,9 +1041,9 @@ private final class VideoChatScreenComponent: Component { }) { if participant.peer.id != expandedParticipantsVideoState.mainParticipant.id { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } } } @@ -1073,9 +1073,9 @@ private final class VideoChatScreenComponent: Component { return false }) { if participant.presentationDescription != nil { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: true), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } else { - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: VideoChatParticipantsComponent.VideoParticipantKey(id: participant.peer.id, isPresentation: false), isMainParticipantPinned: false, isUIHidden: expandedParticipantsVideoState.isUIHidden) } } else { self.expandedParticipantsVideoState = nil @@ -1227,7 +1227,7 @@ private final class VideoChatScreenComponent: Component { } }) - let sideInset: CGFloat = environment.safeInsets.left + 14.0 + let sideInset: CGFloat = max(environment.safeInsets.left, 14.0) let topInset: CGFloat = environment.statusBarHeight + 2.0 let navigationBarHeight: CGFloat = 61.0 @@ -1333,35 +1333,60 @@ private final class VideoChatScreenComponent: Component { ) } - let participantsLayoutType: VideoChatParticipantsComponent.LayoutType - if availableSize.width > 620.0 { + let maxSingleColumnWidth: CGFloat = 620.0 + let isTwoColumnLayout: Bool + if availableSize.width > maxSingleColumnWidth { if let mappedParticipants, mappedParticipants.participants.contains(where: { $0.videoDescription != nil || $0.presentationDescription != nil }) { - participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal( - rightColumnWidth: 300.0, - columnSpacing: 8.0, - isCentered: false - )) + isTwoColumnLayout = true } else { - participantsLayoutType = .horizontal(VideoChatParticipantsComponent.LayoutType.Horizontal( - rightColumnWidth: 380.0, - columnSpacing: 0.0, - isCentered: true - )) + isTwoColumnLayout = false } } else { - participantsLayoutType = .vertical + isTwoColumnLayout = false + } + + let areButtonsCollapsed: Bool + let mainColumnWidth: CGFloat + let mainColumnSideInset: CGFloat + + if isTwoColumnLayout { + areButtonsCollapsed = false + + mainColumnWidth = 320.0 + mainColumnSideInset = 0.0 + } else { + areButtonsCollapsed = self.expandedParticipantsVideoState != nil + + if availableSize.width > maxSingleColumnWidth { + mainColumnWidth = 420.0 + mainColumnSideInset = 0.0 + } else { + mainColumnWidth = availableSize.width + mainColumnSideInset = sideInset + } } let actionButtonDiameter: CGFloat = 56.0 let expandedMicrophoneButtonDiameter: CGFloat = actionButtonDiameter - let collapsedMicrophoneButtonDiameter: CGFloat = 116.0 + var collapsedMicrophoneButtonDiameter: CGFloat = 116.0 + let maxActionMicrophoneButtonSpacing: CGFloat = 38.0 + let minActionMicrophoneButtonSpacing: CGFloat = 20.0 + + if actionButtonDiameter * 2.0 + collapsedMicrophoneButtonDiameter + maxActionMicrophoneButtonSpacing * 2.0 > mainColumnWidth { + collapsedMicrophoneButtonDiameter = mainColumnWidth - (actionButtonDiameter * 2.0 + minActionMicrophoneButtonSpacing * 2.0) + collapsedMicrophoneButtonDiameter = max(actionButtonDiameter, collapsedMicrophoneButtonDiameter) + } let microphoneButtonDiameter: CGFloat - if case .horizontal = participantsLayoutType { - microphoneButtonDiameter = expandedMicrophoneButtonDiameter + if isTwoColumnLayout { + microphoneButtonDiameter = collapsedMicrophoneButtonDiameter } else { - microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter + if areButtonsCollapsed { + microphoneButtonDiameter = expandedMicrophoneButtonDiameter + } else { + microphoneButtonDiameter = self.expandedParticipantsVideoState == nil ? collapsedMicrophoneButtonDiameter : expandedMicrophoneButtonDiameter + } } let buttonsSideInset: CGFloat = 42.0 @@ -1370,36 +1395,96 @@ private final class VideoChatScreenComponent: Component { let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth let actionMicrophoneButtonSpacing = min(maxActionMicrophoneButtonSpacing, floor(remainingButtonsSpace * 0.5)) - let collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) - let expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) - - let microphoneButtonFrame: CGRect - if case .horizontal = participantsLayoutType { - microphoneButtonFrame = expandedMicrophoneButtonFrame - } else { - if self.expandedParticipantsVideoState == nil { - microphoneButtonFrame = collapsedMicrophoneButtonFrame + var collapsedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - collapsedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - 48.0 - environment.safeInsets.bottom - collapsedMicrophoneButtonDiameter), size: CGSize(width: collapsedMicrophoneButtonDiameter, height: collapsedMicrophoneButtonDiameter)) + var expandedMicrophoneButtonFrame: CGRect = CGRect(origin: CGPoint(x: floor((availableSize.width - expandedMicrophoneButtonDiameter) * 0.5), y: availableSize.height - environment.safeInsets.bottom - expandedMicrophoneButtonDiameter - 12.0), size: CGSize(width: expandedMicrophoneButtonDiameter, height: expandedMicrophoneButtonDiameter)) + if isTwoColumnLayout { + if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden { + collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + sideInset + mainColumnWidth } else { - microphoneButtonFrame = expandedMicrophoneButtonFrame + collapsedMicrophoneButtonFrame.origin.x = availableSize.width - sideInset - mainColumnWidth + floor((mainColumnWidth - collapsedMicrophoneButtonDiameter) * 0.5) + } + expandedMicrophoneButtonFrame = collapsedMicrophoneButtonFrame + } else { + if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden { + expandedMicrophoneButtonFrame.origin.y = availableSize.height + expandedMicrophoneButtonDiameter + 12.0 } } - let collapsedParticipantsClippingY: CGFloat = collapsedMicrophoneButtonFrame.minY - 16.0 - let expandedParticipantsClippingY: CGFloat = expandedMicrophoneButtonFrame.minY - 24.0 + let microphoneButtonFrame: CGRect + if areButtonsCollapsed { + microphoneButtonFrame = expandedMicrophoneButtonFrame + } else { + microphoneButtonFrame = collapsedMicrophoneButtonFrame + } + + let collapsedParticipantsClippingY: CGFloat + collapsedParticipantsClippingY = collapsedMicrophoneButtonFrame.minY - 16.0 + + let expandedParticipantsClippingY: CGFloat + if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.isUIHidden { + if isTwoColumnLayout { + expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0 + } else { + expandedParticipantsClippingY = availableSize.height - max(14.0, environment.safeInsets.bottom) + } + } else { + expandedParticipantsClippingY = expandedMicrophoneButtonFrame.minY - 24.0 + } let leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) let rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) let participantsSize = availableSize - let participantsCollapsedInsets: UIEdgeInsets - let participantsExpandedInsets: UIEdgeInsets - if case .horizontal = participantsLayoutType { - participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - (expandedMicrophoneButtonFrame.minY - 16.0), right: environment.safeInsets.right) - participantsExpandedInsets = participantsCollapsedInsets + let columnSpacing: CGFloat = 14.0 + let participantsLayout: VideoChatParticipantsComponent.Layout + if isTwoColumnLayout { + let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset) + let videoColumnWidth: CGFloat = max(10.0, availableSize.width - sideInset * 2.0 - mainColumnWidth - columnSpacing) + participantsLayout = VideoChatParticipantsComponent.Layout( + videoColumn: VideoChatParticipantsComponent.Layout.Column( + width: videoColumnWidth, + insets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: max(14.0, environment.safeInsets.bottom), right: 0.0) + ), + mainColumn: VideoChatParticipantsComponent.Layout.Column( + width: mainColumnWidth, + insets: mainColumnInsets + ), + columnSpacing: columnSpacing + ) } else { - participantsCollapsedInsets = UIEdgeInsets(top: navigationHeight, left: environment.safeInsets.left, bottom: availableSize.height - collapsedParticipantsClippingY, right: environment.safeInsets.right) - participantsExpandedInsets = UIEdgeInsets(top: environment.statusBarHeight, left: environment.safeInsets.left, bottom: availableSize.height - expandedParticipantsClippingY, right: environment.safeInsets.right) + let mainColumnInsets: UIEdgeInsets = UIEdgeInsets(top: navigationHeight, left: mainColumnSideInset, bottom: availableSize.height - collapsedParticipantsClippingY, right: mainColumnSideInset) + participantsLayout = VideoChatParticipantsComponent.Layout( + videoColumn: nil, + mainColumn: VideoChatParticipantsComponent.Layout.Column( + width: mainColumnWidth, + insets: mainColumnInsets + ), + columnSpacing: columnSpacing + ) + } + + let participantsSafeInsets = UIEdgeInsets( + top: environment.statusBarHeight, + left: environment.safeInsets.left, + bottom: max(14.0, environment.safeInsets.bottom), + right: environment.safeInsets.right + ) + let participantsExpandedInsets: UIEdgeInsets + if isTwoColumnLayout { + participantsExpandedInsets = UIEdgeInsets( + top: navigationHeight, + left: max(14.0, participantsSafeInsets.left), + bottom: participantsSafeInsets.bottom, + right: max(14.0, participantsSafeInsets.right) + ) + } else { + participantsExpandedInsets = UIEdgeInsets( + top: participantsSafeInsets.top, + left: participantsSafeInsets.left, + bottom: availableSize.height - expandedParticipantsClippingY, + right: participantsSafeInsets.right + ) } let _ = self.participants.update( @@ -1411,10 +1496,9 @@ private final class VideoChatScreenComponent: Component { expandedVideoState: self.expandedParticipantsVideoState, theme: environment.theme, strings: environment.strings, - layoutType: participantsLayoutType, - collapsedContainerInsets: participantsCollapsedInsets, - expandedContainerInsets: participantsExpandedInsets, - sideInset: sideInset, + layout: participantsLayout, + expandedInsets: participantsExpandedInsets, + safeInsets: participantsSafeInsets, updateMainParticipant: { [weak self] key in guard let self else { return @@ -1423,7 +1507,7 @@ private final class VideoChatScreenComponent: Component { if let expandedParticipantsVideoState = self.expandedParticipantsVideoState, expandedParticipantsVideoState.mainParticipant == key { return } - self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false) + self.expandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState(mainParticipant: key, isMainParticipantPinned: false, isUIHidden: self.expandedParticipantsVideoState?.isUIHidden ?? false) self.state?.updated(transition: .spring(duration: 0.4)) } else if self.expandedParticipantsVideoState != nil { self.expandedParticipantsVideoState = nil @@ -1439,7 +1523,25 @@ private final class VideoChatScreenComponent: Component { } let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState( mainParticipant: expandedParticipantsVideoState.mainParticipant, - isMainParticipantPinned: isPinned + isMainParticipantPinned: isPinned, + isUIHidden: expandedParticipantsVideoState.isUIHidden + ) + if self.expandedParticipantsVideoState != updatedExpandedParticipantsVideoState { + self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState + self.state?.updated(transition: .spring(duration: 0.4)) + } + }, + updateIsExpandedUIHidden: { [weak self] isUIHidden in + guard let self else { + return + } + guard let expandedParticipantsVideoState = self.expandedParticipantsVideoState else { + return + } + let updatedExpandedParticipantsVideoState = VideoChatParticipantsComponent.ExpandedVideoState( + mainParticipant: expandedParticipantsVideoState.mainParticipant, + isMainParticipantPinned: expandedParticipantsVideoState.isMainParticipantPinned, + isUIHidden: isUIHidden ) if self.expandedParticipantsVideoState != updatedExpandedParticipantsVideoState { self.expandedParticipantsVideoState = updatedExpandedParticipantsVideoState @@ -1484,13 +1586,6 @@ private final class VideoChatScreenComponent: Component { actionButtonMicrophoneState = .connecting } - let areButtonsCollapsed: Bool - if case .horizontal = participantsLayoutType { - areButtonsCollapsed = true - } else { - areButtonsCollapsed = self.expandedParticipantsVideoState != nil - } - let _ = self.microphoneButton.update( transition: transition, component: AnyComponent(VideoChatMicButtonComponent(