diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index d4d605a69a..c285caa029 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -212,6 +212,7 @@ public struct PresentationGroupCallState: Equatable { public var subscribedToScheduled: Bool public var isVideoEnabled: Bool public var isVideoWatchersLimitReached: Bool + public var hasVideo: Bool public init( myPeerId: EnginePeer.Id, @@ -226,7 +227,8 @@ public struct PresentationGroupCallState: Equatable { scheduleTimestamp: Int32?, subscribedToScheduled: Bool, isVideoEnabled: Bool, - isVideoWatchersLimitReached: Bool + isVideoWatchersLimitReached: Bool, + hasVideo: Bool ) { self.myPeerId = myPeerId self.networkState = networkState @@ -241,6 +243,7 @@ public struct PresentationGroupCallState: Equatable { self.subscribedToScheduled = subscribedToScheduled self.isVideoEnabled = isVideoEnabled self.isVideoWatchersLimitReached = isVideoWatchersLimitReached + self.hasVideo = hasVideo } } diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index eb0553d7a1..55988887b0 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -115,6 +115,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", "//submodules/TelegramUI/Components/BackButtonComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/DirectMediaImageCache", "//submodules/FastBlur", ], diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 4bcace59a3..f990bdf289 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -268,7 +268,8 @@ private extension PresentationGroupCallState { scheduleTimestamp: scheduleTimestamp, subscribedToScheduled: subscribedToScheduled, isVideoEnabled: false, - isVideoWatchersLimitReached: false + isVideoWatchersLimitReached: false, + hasVideo: false ) } } @@ -2971,11 +2972,12 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.updateLocalVideoState() } + self.stateValue.hasVideo = self.hasVideo } public func disableVideo() { self.hasVideo = false - self.useFrontCamera = true; + self.useFrontCamera = true if let _ = self.videoCapturer { self.videoCapturer = nil self.isVideoMutedDisposable.set(nil) @@ -2984,6 +2986,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.updateLocalVideoState() } + self.stateValue.hasVideo = self.hasVideo } private func updateLocalVideoState() { diff --git a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift index 08b7019d97..a6445e05ff 100644 --- a/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift +++ b/submodules/TelegramCallsUI/Sources/ScheduleVideoChatSheetScreen.swift @@ -13,6 +13,10 @@ import BalancedTextComponent import TelegramPresentationData import TelegramStringFormatting import Markdown +import HierarchyTrackingLayer + +private let purple = UIColor(rgb: 0x3252ef) +private let pink = UIColor(rgb: 0xef436c) private final class ScheduleVideoChatSheetContentComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -33,7 +37,11 @@ private final class ScheduleVideoChatSheetContentComponent: Component { } final class View: UIView { + private let hierarchyTrackingLayer: HierarchyTrackingLayer + private let button = ComponentView() + private let buttonBackgroundLayer: SimpleGradientLayer + private let cancelButton = ComponentView() private let title = ComponentView() @@ -52,7 +60,29 @@ private final class ScheduleVideoChatSheetContentComponent: Component { self.dateFormatter.dateStyle = .short self.dateFormatter.timeZone = TimeZone.current + self.hierarchyTrackingLayer = HierarchyTrackingLayer() + + self.buttonBackgroundLayer = SimpleGradientLayer() + self.buttonBackgroundLayer.type = .radial + self.buttonBackgroundLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor] + self.buttonBackgroundLayer.locations = [0.0, 0.85, 1.0] + self.buttonBackgroundLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + let radius = CGSize(width: 1.0, height: 2.0) + let endEndPoint = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0) + self.buttonBackgroundLayer.endPoint = endEndPoint + self.buttonBackgroundLayer.cornerRadius = 10.0 + super.init(frame: frame) + + self.layer.addSublayer(self.hierarchyTrackingLayer) + self.hierarchyTrackingLayer.isInHierarchyUpdated = { [weak self] value in + guard let self else { + return + } + if value { + self.updateAnimations() + } + } } required init?(coder: NSCoder) { @@ -96,6 +126,46 @@ private final class ScheduleVideoChatSheetContentComponent: Component { } } + private func updateAnimations() { + if let _ = self.buttonBackgroundLayer.animation(forKey: "movement") { + } else { + let previousValue = self.buttonBackgroundLayer.startPoint + let previousEndValue = self.buttonBackgroundLayer.endPoint + let newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + self.buttonBackgroundLayer.startPoint = newValue + + let radius = CGSize(width: 1.0, height: 2.0) + let newEndValue = CGPoint(x: (self.buttonBackgroundLayer.startPoint.x + radius.width) * 1.0, y: (self.buttonBackgroundLayer.startPoint.y + radius.height) * 1.0) + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + guard let self else { + return + } + if self.hierarchyTrackingLayer.isInHierarchy { + self.updateAnimations() + } + } + + self.buttonBackgroundLayer.add(animation, forKey: "movement") + + let endAnimation = CABasicAnimation(keyPath: "endPoint") + endAnimation.duration = animation.duration + endAnimation.fromValue = previousEndValue + endAnimation.toValue = newEndValue + + self.buttonBackgroundLayer.add(animation, forKey: "movementEnd") + + CATransaction.commit() + } + } + func update(component: ScheduleVideoChatSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component let _ = previousComponent @@ -233,9 +303,9 @@ private final class ScheduleVideoChatSheetContentComponent: Component { transition: buttonTransition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( - color: UIColor(rgb: 0x3252EF), + color: .clear, foreground: .white, - pressedColor: UIColor(rgb: 0x3252EF).withMultipliedAlpha(0.8) + pressedColor: UIColor(white: 1.0, alpha: 0.1) ), content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( HStack(buttonContents, spacing: 5.0) @@ -256,8 +326,10 @@ private final class ScheduleVideoChatSheetContentComponent: Component { let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) if let buttonView = self.button.view { if buttonView.superview == nil { + self.layer.addSublayer(self.buttonBackgroundLayer) self.addSubview(buttonView) } + transition.setFrame(layer: self.buttonBackgroundLayer, frame: buttonFrame) transition.setFrame(view: buttonView, frame: buttonFrame) } contentHeight += buttonSize.height @@ -302,6 +374,8 @@ private final class ScheduleVideoChatSheetContentComponent: Component { contentHeight += environment.safeInsets.bottom + 14.0 } + self.updateAnimations() + return CGSize(width: availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift index 8d6b3f69bd..435bdecdaf 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatActionButtonComponent.swift @@ -34,11 +34,13 @@ final class VideoChatActionButtonComponent: Component { case audio(audio: Audio) case video case leave + case switchVideo } case audio(audio: Audio) case video(isActive: Bool) case leave + case switchVideo fileprivate var iconType: IconType { switch self { @@ -57,6 +59,8 @@ final class VideoChatActionButtonComponent: Component { return .video case .leave: return .leave + case .switchVideo: + return .switchVideo } } } @@ -174,6 +178,19 @@ final class VideoChatActionButtonComponent: Component { backgroundColor = UIColor(rgb: 0x3252EF) } iconDiameter = 60.0 + case .switchVideo: + titleText = "" + switch component.microphoneState { + case .connecting: + backgroundColor = UIColor(white: 0.1, alpha: 1.0) + case .muted: + backgroundColor = UIColor(rgb: 0x027FFF) + case .unmuted: + backgroundColor = UIColor(rgb: 0x34C659) + case .raiseHand, .scheduled: + backgroundColor = UIColor(rgb: 0x3252EF) + } + iconDiameter = 54.0 case .leave: titleText = "leave" backgroundColor = UIColor(rgb: 0x47191E) @@ -204,6 +221,8 @@ final class VideoChatActionButtonComponent: Component { self.contentImage = UIImage(bundleImageName: iconName)?.precomposed().withRenderingMode(.alwaysTemplate) case .video: self.contentImage = UIImage(bundleImageName: "Call/CallCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate) + case .switchVideo: + self.contentImage = UIImage(bundleImageName: "Call/CallSwitchCameraButton")?.precomposed().withRenderingMode(.alwaysTemplate) case .leave: self.contentImage = generateImage(CGSize(width: 28.0, height: 28.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -275,7 +294,9 @@ final class VideoChatActionButtonComponent: Component { if iconView.superview == nil { self.addSubview(iconView) } - transition.setFrame(view: iconView, frame: iconFrame) + transition.setPosition(view: iconView, position: iconFrame.center) + transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setScale(view: iconView, scale: availableSize.width / 56.0) } return size diff --git a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift index 3bc92c4bcf..f50c490402 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatMuteIconComponent.swift @@ -62,6 +62,7 @@ final class VideoChatMuteIconComponent: Component { self.isUpdating = false } + let previousComponent = self.component self.component = component if case let .mute(isFilled, isMuted) = component.content { @@ -77,7 +78,10 @@ final class VideoChatMuteIconComponent: Component { 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) + if let previousComponent, previousComponent.content == component.content, previousComponent.color == component.color { + } else { + 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 diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift index 834fc03318..bcc2c08954 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantAvatarComponent.swift @@ -136,6 +136,7 @@ final class VideoChatParticipantAvatarComponent: Component { let peer: EnginePeer let myPeerId: EnginePeer.Id let isSpeaking: Bool + let isMutedForMe: Bool let theme: PresentationTheme init( @@ -143,12 +144,14 @@ final class VideoChatParticipantAvatarComponent: Component { peer: EnginePeer, myPeerId: EnginePeer.Id, isSpeaking: Bool, + isMutedForMe: Bool, theme: PresentationTheme ) { self.call = call self.peer = peer self.myPeerId = myPeerId self.isSpeaking = isSpeaking + self.isMutedForMe = isMutedForMe self.theme = theme } @@ -159,10 +162,13 @@ final class VideoChatParticipantAvatarComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.myPeerId != rhs.myPeerId { + return false + } if lhs.isSpeaking != rhs.isSpeaking { return false } - if lhs.myPeerId != rhs.myPeerId { + if lhs.isMutedForMe != rhs.isMutedForMe { return false } if lhs.theme !== rhs.theme { @@ -259,7 +265,15 @@ final class VideoChatParticipantAvatarComponent: Component { } else { tintTransition = .immediate } - tintTransition.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) + let tintColor: UIColor + if component.isMutedForMe { + tintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { + tintColor = UIColor(rgb: 0x33C758) + } else { + tintColor = component.theme.list.itemAccentColor + } + tintTransition.setTintColor(layer: blobView.blobsLayer, color: tintColor) } if component.peer.smallProfileImage != nil { @@ -362,7 +376,15 @@ final class VideoChatParticipantAvatarComponent: Component { avatarNode.layer.transform = CATransform3DMakeScale(1.0 + additionalScale, 1.0 + additionalScale, 1.0) } - ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: component.isSpeaking ? UIColor(rgb: 0x33C758) : component.theme.list.itemAccentColor) + let tintColor: UIColor + if component.isMutedForMe { + tintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { + tintColor = UIColor(rgb: 0x33C758) + } else { + tintColor = component.theme.list.itemAccentColor + } + ComponentTransition.immediate.setTintColor(layer: blobView.blobsLayer, color: tintColor) } if blobView.alpha == 0.0 { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift index 5b480bac46..7375afb149 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantStatusComponent.swift @@ -121,7 +121,9 @@ final class VideoChatParticipantStatusComponent: Component { } if let iconView = muteStatusView.iconView { let iconTintColor: UIColor - if component.isSpeaking { + if let muteState = component.muteState, muteState.mutedByYou { + iconTintColor = UIColor(rgb: 0xff3b30) + } else if component.isSpeaking { iconTintColor = UIColor(rgb: 0x33C758) } else { if let muteState = component.muteState { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift index d295c40a7f..7fd4ae4af7 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift @@ -352,7 +352,10 @@ final class VideoChatParticipantVideoComponent: Component { alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha) } - let videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription + var videoDescription = component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription + if component.isPresentation && component.isMyPeer { + videoDescription = nil + } var isEffectivelyPaused = false if let videoDescription, videoDescription.isPaused { diff --git a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift index dda1b187c8..e7e0c24831 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatParticipantsComponent.swift @@ -1154,9 +1154,16 @@ final class VideoChatParticipantsComponent: Component { let itemFrame = itemLayout.listItemFrame(at: i) + var isMutedForMe = false + if let muteState = participant.muteState, muteState.mutedByYou { + isMutedForMe = true + } + let subtitle: PeerListItemComponent.Subtitle if participant.peer.id == component.call.accountContext.account.peerId { subtitle = PeerListItemComponent.Subtitle(text: "this is you", color: .accent) + } else if let muteState = participant.muteState, muteState.mutedByYou { + subtitle = PeerListItemComponent.Subtitle(text: "muted for you", color: .destructive) } else if component.speakingParticipants.contains(participant.peer.id) { if let volume = participant.volume, volume != 10000 { subtitle = PeerListItemComponent.Subtitle(text: "\(volume / 100)% speaking", color: .constructive) @@ -1190,6 +1197,7 @@ final class VideoChatParticipantsComponent: Component { peer: EnginePeer(participant.peer), myPeerId: component.participants?.myPeerId ?? component.call.accountContext.account.peerId, isSpeaking: component.speakingParticipants.contains(participant.peer.id), + isMutedForMe: isMutedForMe, theme: component.theme )), peer: EnginePeer(participant.peer), diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift index 3c97c76306..47e5a2407b 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScheduledInfoComponent.swift @@ -6,6 +6,7 @@ import MultilineTextComponent import TelegramPresentationData import TelegramStringFormatting import HierarchyTrackingLayer +import AnimatedTextComponent private let purple = UIColor(rgb: 0x3252ef) private let pink = UIColor(rgb: 0xef436c) @@ -13,6 +14,33 @@ private let pink = UIColor(rgb: 0xef436c) private let latePurple = UIColor(rgb: 0x974aa9) private let latePink = UIColor(rgb: 0xf0436c) +private func textItemsForTimeout(value: Int32) -> [AnimatedTextComponent.Item] { + if value < 3600 { + let minutes = value / 60 + let seconds = value % 60 + + var items: [AnimatedTextComponent.Item] = [] + items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 1))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2))) + + return items + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let seconds = value % 60 + + var items: [AnimatedTextComponent.Item] = [] + items.append(AnimatedTextComponent.Item(id: AnyHashable(9), content: .number(Int(hours), minDigits: 1))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(10), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(11), content: .number(Int(minutes), minDigits: 2))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(12), content: .text(":"))) + items.append(AnimatedTextComponent.Item(id: AnyHashable(13), content: .number(Int(seconds), minDigits: 2))) + + return items + } +} + final class VideoChatScheduledInfoComponent: Component { let timestamp: Int32 let strings: PresentationStrings @@ -46,8 +74,11 @@ final class VideoChatScheduledInfoComponent: Component { private let hierarchyTrackingLayer: HierarchyTrackingLayer private var component: VideoChatScheduledInfoComponent? + private weak var state: EmptyComponentState? private var isUpdating: Bool = false + private var countdownTimer: Foundation.Timer? + override init(frame: CGRect) { self.countdownContainerView = UIView() self.countdownMaskView = UIView() @@ -76,6 +107,9 @@ final class VideoChatScheduledInfoComponent: Component { } if value { self.updateAnimations() + } else { + self.countdownTimer?.invalidate() + self.countdownTimer = nil } } } @@ -84,6 +118,10 @@ final class VideoChatScheduledInfoComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + self.countdownTimer?.invalidate() + } + private func updateAnimations() { if let _ = self.countdownGradientLayer.animation(forKey: "movement") { } else { @@ -110,6 +148,15 @@ final class VideoChatScheduledInfoComponent: Component { self.countdownGradientLayer.add(animation, forKey: "movement") CATransaction.commit() } + + if self.countdownTimer == nil { + self.countdownTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self] _ in + guard let self else { + return + } + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }) + } } func update(component: VideoChatScheduledInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { @@ -119,6 +166,7 @@ final class VideoChatScheduledInfoComponent: Component { } self.component = component + self.state = state let titleSize = self.title.update( transition: .immediate, @@ -130,21 +178,20 @@ final class VideoChatScheduledInfoComponent: Component { ) let remainingSeconds: Int32 = max(0, component.timestamp - Int32(Date().timeIntervalSince1970)) - let countdownText: String + var items: [AnimatedTextComponent.Item] = [] if remainingSeconds >= 86400 { - countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + let countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) + items.append(AnimatedTextComponent.Item(id: AnyHashable(0), content: .text(countdownText))) } else { - countdownText = textForTimeout(value: abs(remainingSeconds)) - /*if remainingSeconds < 0 && !self.isLate { - self.isLate = true - self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] - }*/ + items = textItemsForTimeout(value: remainingSeconds) } let countdownTextSize = self.countdownText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: countdownText, font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)) + transition: transition, + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 68.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), + color: .white, + items: items )), environment: {}, containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 400.0) diff --git a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift index ff0435974f..2bc963014d 100644 --- a/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VideoChatScreen.swift @@ -76,6 +76,7 @@ final class VideoChatScreenComponent: Component { var navigationSidebarButton: ComponentView? let videoButton = ComponentView() + var switchVideoButton: ComponentView? let leaveButton = ComponentView() let microphoneButton = ComponentView() @@ -1316,10 +1317,17 @@ final class VideoChatScreenComponent: Component { } } + let actionButtonPlacementArea: (x: CGFloat, width: CGFloat) + if isTwoColumnLayout { + actionButtonPlacementArea = (availableSize.width - sideInset - mainColumnWidth, mainColumnWidth) + } else { + actionButtonPlacementArea = (0.0, availableSize.width) + } + let buttonsSideInset: CGFloat = 26.0 let buttonsWidth: CGFloat = actionButtonDiameter * 2.0 + microphoneButtonDiameter - let remainingButtonsSpace: CGFloat = availableSize.width - buttonsSideInset * 2.0 - buttonsWidth + let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth let effectiveMaxActionMicrophoneButtonSpacing: CGFloat if areButtonsCollapsed { @@ -1355,7 +1363,7 @@ final class VideoChatScreenComponent: Component { } } - let microphoneButtonFrame: CGRect + var microphoneButtonFrame: CGRect if areButtonsCollapsed { microphoneButtonFrame = expandedMicrophoneButtonFrame } else { @@ -1376,8 +1384,41 @@ final class VideoChatScreenComponent: Component { 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)) + var leftActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.minX - actionMicrophoneButtonSpacing - actionButtonDiameter, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + var rightActionButtonFrame = CGRect(origin: CGPoint(x: microphoneButtonFrame.maxX + actionMicrophoneButtonSpacing, y: microphoneButtonFrame.minY + floor((microphoneButtonFrame.height - actionButtonDiameter) * 0.5)), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + + var additionalLeftActionButtonFrame: CGRect? + if let callState = self.callState, callState.hasVideo { + let additionalButtonDiameter: CGFloat + if areButtonsCollapsed { + additionalButtonDiameter = actionButtonDiameter + } else { + additionalButtonDiameter = floor(actionButtonDiameter * 0.64) + } + + if areButtonsCollapsed { + let buttonCount: CGFloat = 4.0 + + let buttonsWidth: CGFloat = actionButtonDiameter * buttonCount + let remainingButtonsSpace: CGFloat = actionButtonPlacementArea.width - buttonsSideInset * 2.0 - buttonsWidth + let maxSpacing: CGFloat = 80.0 + let effectiveSpacing = min(maxSpacing, floor(remainingButtonsSpace / (buttonCount - 1.0))) + + let totalButtonsWidth: CGFloat = buttonsWidth + (buttonCount - 1.0) * effectiveSpacing + let totalButtonsX: CGFloat = actionButtonPlacementArea.x + floor((actionButtonPlacementArea.width - totalButtonsWidth) * 0.5) + additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(0.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + leftActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(1.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + microphoneButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(2.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + rightActionButtonFrame = CGRect(origin: CGPoint(x: totalButtonsX + CGFloat(3.0) * (actionButtonDiameter + effectiveSpacing), y: leftActionButtonFrame.minY), size: CGSize(width: actionButtonDiameter, height: actionButtonDiameter)) + } else { + let additionalButtonSpacing = 12.0 + let totalLeftButtonHeight: CGFloat = leftActionButtonFrame.height + additionalButtonSpacing + additionalButtonDiameter + let totalLeftButtonOriginY: CGFloat = leftActionButtonFrame.minY + floor((leftActionButtonFrame.height - totalLeftButtonHeight) * 0.5) + leftActionButtonFrame.origin.y = totalLeftButtonOriginY + additionalButtonDiameter + additionalButtonSpacing + + additionalLeftActionButtonFrame = CGRect(origin: CGPoint(x: leftActionButtonFrame.minX + floor((leftActionButtonFrame.width - additionalButtonDiameter) * 0.5), y: leftActionButtonFrame.minY - additionalButtonSpacing - additionalButtonDiameter), size: CGSize(width: additionalButtonDiameter, height: additionalButtonDiameter)) + } + } let participantsSize = availableSize @@ -1728,7 +1769,7 @@ final class VideoChatScreenComponent: Component { videoButtonContent = .audio(audio: buttonAudio) } else { //TODO:release - videoButtonContent = .video(isActive: false) + videoButtonContent = .video(isActive: self.callState?.hasVideo ?? false) } let _ = self.videoButton.update( transition: transition, @@ -1763,6 +1804,62 @@ final class VideoChatScreenComponent: Component { transition.setBounds(view: videoButtonView, bounds: CGRect(origin: CGPoint(), size: leftActionButtonFrame.size)) } + if let additionalLeftActionButtonFrame { + let switchVideoButton: ComponentView + var switchVideoButtonTransition = transition + if let current = self.switchVideoButton { + switchVideoButton = current + } else { + switchVideoButtonTransition = switchVideoButtonTransition.withAnimation(.none) + switchVideoButton = ComponentView() + self.switchVideoButton = switchVideoButton + } + + let switchVideoButtonContent: VideoChatActionButtonComponent.Content = .switchVideo + + let _ = switchVideoButton.update( + transition: switchVideoButtonTransition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(VideoChatActionButtonComponent( + strings: environment.strings, + content: switchVideoButtonContent, + microphoneState: actionButtonMicrophoneState, + isCollapsed: areButtonsCollapsed + )), + effectAlignment: .center, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.call.switchVideoCamera() + }, + animateAlpha: false + )), + environment: {}, + containerSize: additionalLeftActionButtonFrame.size + ) + if let switchVideoButtonView = switchVideoButton.view { + var animateIn = false + if switchVideoButtonView.superview == nil { + self.containerView.addSubview(switchVideoButtonView) + animateIn = true + } + switchVideoButtonTransition.setFrame(view: switchVideoButtonView, frame: additionalLeftActionButtonFrame) + if animateIn { + alphaTransition.animateAlpha(view: switchVideoButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: switchVideoButtonView, from: 0.001, to: 1.0) + } + } + } else if let switchVideoButton = self.switchVideoButton { + self.switchVideoButton = nil + if let switchVideoButtonView = switchVideoButton.view { + alphaTransition.setAlpha(view: switchVideoButtonView, alpha: 0.0, completion: { [weak switchVideoButtonView] _ in + switchVideoButtonView?.removeFromSuperview() + }) + transition.setScale(view: switchVideoButtonView, scale: 0.001) + } + } + let _ = self.leaveButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 349aa6da22..972ce43e09 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -169,6 +169,7 @@ public final class PeerListItemComponent: Component { case neutral case accent case constructive + case destructive } public var text: String @@ -937,8 +938,9 @@ public final class PeerListItemComponent: Component { case .accent: labelColor = component.theme.list.itemAccentColor case .constructive: - //TODO:release labelColor = UIColor(rgb: 0x33C758) + case .destructive: + labelColor = UIColor(rgb: 0xff3b30) } var animateLabelDirection: Bool?