diff --git a/submodules/AccountContext/Sources/PresentationCallManager.swift b/submodules/AccountContext/Sources/PresentationCallManager.swift index ebbe029fcf..11f058ac56 100644 --- a/submodules/AccountContext/Sources/PresentationCallManager.swift +++ b/submodules/AccountContext/Sources/PresentationCallManager.swift @@ -188,6 +188,7 @@ public struct PresentationGroupCallState: Equatable { public var raisedHand: Bool public var scheduleTimestamp: Int32? public var subscribedToScheduled: Bool + public var isVideoEnabled: Bool public init( myPeerId: PeerId, @@ -200,7 +201,8 @@ public struct PresentationGroupCallState: Equatable { title: String?, raisedHand: Bool, scheduleTimestamp: Int32?, - subscribedToScheduled: Bool + subscribedToScheduled: Bool, + isVideoEnabled: Bool ) { self.myPeerId = myPeerId self.networkState = networkState @@ -213,6 +215,7 @@ public struct PresentationGroupCallState: Equatable { self.raisedHand = raisedHand self.scheduleTimestamp = scheduleTimestamp self.subscribedToScheduled = subscribedToScheduled + self.isVideoEnabled = isVideoEnabled } } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 02f9df797c..803561a788 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -657,10 +657,21 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - + if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let contentParentNode = extracted let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + + var actionsSpringDamping = springDamping + var actionsDuration = springDuration + var actionsOffset: CGFloat = 0.0 + var contentDuration = springDuration + if case let .extracted(source) = self.source, source.centerVertically { + actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 + actionsSpringDamping *= 1.2 + actionsDuration *= 1.0 + contentDuration *= 0.9 + } let localContentSourceFrame: CGRect if keepInPlace { @@ -673,9 +684,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi reactionContextNode.animateIn(from: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size)) } - self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: actionsSpringDamping, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) - self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true) contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) } @@ -920,7 +931,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi localContentSourceFrame = localSourceFrame } - self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) + var actionsOffset: CGFloat = 0.0 + if case let .extracted(source) = self.source, source.centerVertically { + actionsOffset = -localSourceFrame.width * 0.6 + } + + self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in completedContentNode = true @@ -1319,6 +1335,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } case let .extracted(contentParentNode, keepInPlace): + var centerVertically = false + if case let .extracted(source) = self.source, source.centerVertically { + centerVertically = true + } let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero @@ -1332,13 +1352,17 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) let preferredActionsX: CGFloat let originalActionsY: CGFloat - if keepInPlace { + if centerVertically { + originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) + preferredActionsX = originalProjectedContentViewFrame.1.maxX - actionsSize.width + } else if keepInPlace { originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - actionsSize.height preferredActionsX = max(actionsSideInset, originalProjectedContentViewFrame.1.maxX - actionsSize.width) } else { originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) preferredActionsX = originalProjectedContentViewFrame.1.minX } + var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - actionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: actionsSize) let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX let originalContentY: CGFloat @@ -1366,7 +1390,20 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi var overflowOffset: CGFloat var contentContainerFrame: CGRect - if keepInPlace { + if centerVertically { + overflowOffset = 0.0 + if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { + let totalWidth = originalContentFrame.width + originalActionsFrame.width + contentActionsSpacing + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - totalWidth) / 2.0 + originalContentFrame.width * 0.1), y: floor((layout.size.height - originalContentFrame.height) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.x = contentContainerFrame.maxX + contentActionsSpacing + 14.0 + originalActionsFrame.origin.y = contentContainerFrame.origin.y + contentHeight = layout.size.height + } else { + let totalHeight = originalContentFrame.height + originalActionsFrame.height + contentContainerFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentParentNode.contentRect.minX, y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) + originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing + } + } else if keepInPlace { overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY) if !overflowOffset.isZero { @@ -1396,14 +1433,6 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - if case let .extracted(source) = self.source, source.centerVertically { - let totalHeight = contentContainerFrame.height + originalActionsFrame.height - let updatedOrigin = floor((layout.size.height - totalHeight) / 2.0) - let delta = updatedOrigin - contentContainerFrame.origin.y - contentContainerFrame.origin.y = updatedOrigin - originalActionsFrame.origin.y += delta - } - let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize diff --git a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift index 9d74db9fed..fc48bd1ef5 100644 --- a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift +++ b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit import Display import SwiftSignalKit import AccountContext +import ContextUI final class GroupVideoNode: ASDisplayNode { enum Position { @@ -18,6 +19,8 @@ final class GroupVideoNode: ASDisplayNode { case fillVertical } + let sourceContainerNode: PinchSourceContainerNode + private let containerNode: ASDisplayNode private let videoViewContainer: UIView private let videoView: PresentationCallVideoView @@ -38,16 +41,18 @@ final class GroupVideoNode: ASDisplayNode { } init(videoView: PresentationCallVideoView, backdropVideoView: PresentationCallVideoView?) { + self.sourceContainerNode = PinchSourceContainerNode() + self.containerNode = ASDisplayNode() self.videoViewContainer = UIView() + self.videoViewContainer.isUserInteractionEnabled = false self.videoView = videoView self.backdropVideoViewContainer = UIView() + self.backdropVideoViewContainer.isUserInteractionEnabled = false self.backdropVideoView = backdropVideoView super.init() - - self.isUserInteractionEnabled = false - + if let backdropVideoView = backdropVideoView { self.backdropVideoViewContainer.addSubview(backdropVideoView.view) self.view.addSubview(self.backdropVideoViewContainer) @@ -64,7 +69,9 @@ final class GroupVideoNode: ASDisplayNode { } self.videoViewContainer.addSubview(self.videoView.view) - self.view.addSubview(self.videoViewContainer) + self.addSubnode(self.sourceContainerNode) + self.containerNode.view.addSubview(self.videoViewContainer) + self.sourceContainerNode.contentNode.addSubnode(self.containerNode) self.clipsToBounds = true @@ -91,7 +98,7 @@ final class GroupVideoNode: ASDisplayNode { } }) - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.containerNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } func updateIsBlurred(isBlurred: Bool, light: Bool = false, animated: Bool = true) { @@ -170,6 +177,9 @@ final class GroupVideoNode: ASDisplayNode { func updateLayout(size: CGSize, layoutMode: LayoutMode, transition: ContainedViewLayoutTransition) { self.validLayout = (size, layoutMode) let bounds = CGRect(origin: CGPoint(), size: size) + self.sourceContainerNode.update(size: size, transition: .immediate) + transition.updateFrameAsPositionAndBounds(node: self.sourceContainerNode, frame: bounds) + transition.updateFrameAsPositionAndBounds(node: self.containerNode, frame: bounds) transition.updateFrameAsPositionAndBounds(layer: self.videoViewContainer.layer, frame: bounds) transition.updateFrameAsPositionAndBounds(layer: self.backdropVideoViewContainer.layer, frame: bounds) @@ -264,7 +274,7 @@ final class GroupVideoNode: ASDisplayNode { } if let backdropEffectView = self.backdropEffectView { - let maxSide = max(bounds.width, bounds.height) + 32.0 + let maxSide = max(bounds.width, bounds.height) let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.height - maxSide) / 2.0, width: maxSide, height: maxSide) if case let .animated(duration, .spring) = transition { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index a51febc85d..b38155022e 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -269,7 +269,8 @@ private extension PresentationGroupCallState { title: title, raisedHand: false, scheduleTimestamp: scheduleTimestamp, - subscribedToScheduled: subscribedToScheduled + subscribedToScheduled: subscribedToScheduled, + isVideoEnabled: false ) } } @@ -987,7 +988,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { guard let strongSelf = self else { return } - + var topParticipants: [GroupCallParticipantsContext.Participant] = [] var members = PresentationGroupCallMembers( @@ -1214,7 +1215,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { scheduleTimestamp: self.stateValue.scheduleTimestamp, subscribedToScheduled: self.stateValue.subscribedToScheduled, totalCount: 0, - isVideoEnabled: false, + isVideoEnabled: callInfo.isVideoEnabled, version: 0 ), previousServiceState: nil @@ -1969,7 +1970,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { strongSelf.stateValue.recordingStartTimestamp = state.recordingStartTimestamp strongSelf.stateValue.title = state.title strongSelf.stateValue.scheduleTimestamp = state.scheduleTimestamp - + strongSelf.stateValue.isVideoEnabled = state.isVideoEnabled + strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( id: callInfo.id, accessHash: callInfo.accessHash, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index 1b88b3e3a9..e3befe1332 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -813,7 +813,7 @@ private final class VoiceChatActionButtonBackgroundNode: ASDisplayNode { case .muted: targetColors = [pink.cgColor, purple.cgColor, purple.cgColor] targetScale = 0.85 - outerColor = UIColor(rgb: 0x3b3474) + outerColor = UIColor(rgb: 0x24306b) activeColor = purple } self.updatedColors?(outerColor, activeColor) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift index a07cd0ff4c..433638a50c 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift @@ -174,6 +174,10 @@ class VoiceChatActionItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + guard params.width > 0.0 else { + return + } + strongSelf.activateArea.accessibilityLabel = item.title strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: layout.contentSize.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 2797799d0c..9fc546de84 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -190,6 +190,11 @@ struct VoiceChatPeerEntry: Identifiable { } public final class VoiceChatController: ViewController { + enum DisplayMode { + case modal(isExpanded: Bool, isFilled: Bool) + case fullscreen(controlsHidden: Bool) + } + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private struct ListTransition { let deletions: [ListViewDeleteItem] @@ -450,6 +455,7 @@ public final class VoiceChatController: ViewController { }) case let .peer(peerEntry, _): let peer = peerEntry.peer + var textColor: VoiceChatFullscreenParticipantItem.Color = .generic var color: VoiceChatFullscreenParticipantItem.Color = .generic let icon: VoiceChatFullscreenParticipantItem.Icon var text: VoiceChatParticipantItem.ParticipantText @@ -482,14 +488,17 @@ public final class VoiceChatController: ViewController { text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) } if let muteState = peerEntry.muteState, muteState.mutedByYou { + textColor = .destructive color = .destructive icon = .microphone(true, UIColor(rgb: 0xff3b30)) } else { icon = .microphone(peerEntry.muteState != nil, UIColor.white) + color = .accent } case .speaking: if let muteState = peerEntry.muteState, muteState.mutedByYou { text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + textColor = .destructive color = .destructive icon = .microphone(true, UIColor(rgb: 0xff3b30)) } else { @@ -502,11 +511,13 @@ public final class VoiceChatController: ViewController { } else { text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) } - icon = .microphone(false, UIColor.white) + icon = .microphone(false, UIColor(rgb: 0x34c759)) + textColor = .constructive + color = .constructive } case .raisedHand: text = .none - color = .accent + textColor = .accent icon = .wantsToSpeak case .invited: text = .none @@ -517,7 +528,7 @@ public final class VoiceChatController: ViewController { text = .text(about, textIcon, .generic) } - return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, icon: icon, text: text, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { + return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, icon: icon, text: text, textColor: textColor, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { if let endpointId = peerEntry.effectiveVideoEndpointId { return interaction.getPeerVideo(endpointId, .list) } else { @@ -736,7 +747,6 @@ public final class VoiceChatController: ViewController { private var currentNormalButtonColor: UIColor? private var currentActiveButtonColor: UIColor? - private var switchedToCameraPeers = Set() private var currentEntries: [ListEntry] = [] private var currentFullscreenEntries: [ListEntry] = [] @@ -802,22 +812,18 @@ public final class VoiceChatController: ViewController { private var peerIdToEndpoint: [PeerId: String] = [:] private var currentSpeakers: [PeerId] = [] - private var currentDominantSpeaker: (PeerId, Double)? - private var currentForcedSpeaker: PeerId? + private var currentDominantSpeaker: (PeerId, String?, Double)? + private var currentForcedSpeaker: (PeerId, String?)? private var effectiveSpeaker: (PeerId, String?)? private var updateAvatarDisposable = MetaDisposable() private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) private var currentUpdatingAvatar: TelegramMediaImageRepresentation? + private var connectedOnce = false private var ignoreConnecting = false private var ignoreConnectingTimer: SwiftSignalKit.Timer? - - private enum DisplayMode { - case modal(isExpanded: Bool, isFilled: Bool) - case fullscreen(controlsHidden: Bool) - } - + private var displayMode: DisplayMode = .modal(isExpanded: false, isFilled: false) { didSet { if case let .modal(isExpanded, _) = self.displayMode { @@ -1049,13 +1055,12 @@ public final class VoiceChatController: ViewController { }, switchToPeer: { [weak self] peerId, videoEndpointId, expand in if let strongSelf = self { if expand, let videoEndpointId = videoEndpointId { - strongSelf.currentDominantSpeaker = (peerId, CACurrentMediaTime()) - strongSelf.effectiveSpeaker = (peerId, videoEndpointId) + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) strongSelf.updateDisplayMode(.fullscreen(controlsHidden: false)) } else { strongSelf.currentForcedSpeaker = nil if peerId != strongSelf.currentDominantSpeaker?.0 { - strongSelf.currentDominantSpeaker = (peerId, CACurrentMediaTime()) + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) } strongSelf.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true) } @@ -1644,8 +1649,8 @@ public final class VoiceChatController: ViewController { self.topPanelNode.addSubnode(self.closeButton) self.topPanelNode.addSubnode(self.topCornersNode) - self.bottomPanelNode.addSubnode(self.audioButton) self.bottomPanelNode.addSubnode(self.cameraButton) + self.bottomPanelNode.addSubnode(self.audioButton) self.bottomPanelNode.addSubnode(self.switchCameraButton) self.bottomPanelNode.addSubnode(self.leaveButton) self.bottomPanelNode.addSubnode(self.actionButton) @@ -1711,7 +1716,23 @@ public final class VoiceChatController: ViewController { return } + var animate = false if strongSelf.callState != state { + if let previousCallState = strongSelf.callState { + var networkStateUpdated = false + if case .connecting = previousCallState.networkState, case .connected = state.networkState { + networkStateUpdated = true + strongSelf.connectedOnce = true + } + var canUnmuteUpdated = false + if previousCallState.muteState?.canUnmute != state.muteState?.canUnmute { + canUnmuteUpdated = true + } + if previousCallState.isVideoEnabled != state.isVideoEnabled || (state.isVideoEnabled && networkStateUpdated) || canUnmuteUpdated { + strongSelf.animatingButtonsSwap = true + animate = true + } + } strongSelf.callState = state strongSelf.mainStageNode.callState = state @@ -1744,7 +1765,7 @@ public final class VoiceChatController: ViewController { } if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .immediate) + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: animate ? .animated(duration: 0.4, curve: .spring) : .immediate) } }) @@ -1814,12 +1835,12 @@ public final class VoiceChatController: ViewController { } if maxLevelWithVideo == nil { - if let peerId = strongSelf.currentDominantSpeaker { - maxLevelWithVideo = (peerId.0, 0.0) + if let (peerId, _, _) = strongSelf.currentDominantSpeaker { + maxLevelWithVideo = (peerId, 0.0) } else if strongSelf.peerIdToEndpoint.count > 0 { - for entry in strongSelf.currentEntries { + for entry in strongSelf.currentFullscreenEntries { if case let .peer(peerEntry, _) = entry { - if let _ = peerEntry.effectiveVideoEndpointId { + if let videoEndpointId = peerEntry.effectiveVideoEndpointId { maxLevelWithVideo = (peerEntry.peer.id, 0.0) break } @@ -1830,10 +1851,11 @@ public final class VoiceChatController: ViewController { if case .fullscreen = strongSelf.displayMode, !strongSelf.mainStageNode.animating { if let (peerId, _) = maxLevelWithVideo { - if let peer = strongSelf.currentDominantSpeaker, CACurrentMediaTime() - peer.1 < 2.5 { - } else if strongSelf.currentDominantSpeaker?.0 != peerId { - strongSelf.currentDominantSpeaker = (peerId, CACurrentMediaTime()) - strongSelf.updateMainVideo(waitForFullSize: false) + if let (currentPeerId, _, timestamp) = strongSelf.currentDominantSpeaker { + if CACurrentMediaTime() - timestamp > 2.5 && peerId != currentPeerId { + strongSelf.currentDominantSpeaker = (peerId, nil, CACurrentMediaTime()) + strongSelf.updateMainVideo(waitForFullSize: false) + } } } } @@ -1992,11 +2014,13 @@ public final class VoiceChatController: ViewController { self.mainStageNode.togglePin = { [weak self] in if let strongSelf = self { - if let peerId = strongSelf.currentForcedSpeaker { - strongSelf.currentDominantSpeaker = (peerId, CACurrentMediaTime()) - strongSelf.currentForcedSpeaker = nil - } else { - strongSelf.currentForcedSpeaker = strongSelf.effectiveSpeaker?.0 + if let (peerId, videoEndpointId) = strongSelf.effectiveSpeaker { + if let _ = strongSelf.currentForcedSpeaker { + strongSelf.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) + strongSelf.currentForcedSpeaker = nil + } else { + strongSelf.currentForcedSpeaker = (peerId, videoEndpointId) + } } strongSelf.updateMembers() } @@ -3025,7 +3049,7 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = self.validLayout { self.animatingButtonsSwap = true - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .linear)) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) } } else { self.call.makeOutgoingVideoView { [weak self] view in @@ -3040,7 +3064,7 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = strongSelf.validLayout { strongSelf.animatingButtonsSwap = true - strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .linear)) + strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) } } }, switchCamera: { [weak self] in @@ -3689,81 +3713,19 @@ public final class VoiceChatController: ViewController { let sideButtonMinimalInset: CGFloat = 16.0 let sideButtonOffset = min(42.0, floor((((size.width - 112.0) / 2.0) - sideButtonSize.width) / 2.0)) let sideButtonOrigin = max(sideButtonMinimalInset, floor((size.width - 112.0) / 2.0) - sideButtonOffset - sideButtonSize.width) - - let upperButtonDistance: CGFloat = 12.0 - let firstButtonFrame: CGRect - let secondButtonFrame: CGRect - let thirdButtonFrame: CGRect - let forthButtonFrame: CGRect - - let leftButtonFrame: CGRect - if self.isScheduled { - leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) - } else { - leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height - upperButtonDistance - cameraButtonSize.height) / 2.0) + upperButtonDistance + cameraButtonSize.height), size: sideButtonSize) - } - let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) - + let smallButtons: Bool switch effectiveDisplayMode { case .modal: if isLandscape { smallButtons = true - let sideInset: CGFloat - let buttonsCount: Int - if false { - sideInset = 42.0 - buttonsCount = 3 - } else { - sideInset = 26.0 - buttonsCount = 4 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) - forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) - firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) } else { smallButtons = false - firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) - secondButtonFrame = leftButtonFrame - thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) - forthButtonFrame = rightButtonFrame } - case let .fullscreen(controlsHidden): + case .fullscreen: smallButtons = true - - if isLandscape { - let sideInset: CGFloat - let buttonsCount: Int - if false { - sideInset = 42.0 - buttonsCount = 3 - } else { - sideInset = 26.0 - buttonsCount = 4 - } - let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) - let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) - forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) - firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) - } else { - let sideInset: CGFloat = 26.0 - let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * 4.0) / 3.0) - let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0) - firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) - secondButtonFrame = CGRect(origin: CGPoint(x: sideInset + sideButtonSize.width + spacing, y: y), size: sideButtonSize) - let thirdButtonPreFrame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - sideButtonSize.width - spacing - sideButtonSize.width, y: y), size: sideButtonSize) - thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) - forthButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - sideButtonSize.width, y: y), size: sideButtonSize) - } } - + let actionButtonState: VoiceChatActionButton.State let actionButtonTitle: String let actionButtonSubtitle: String @@ -3857,6 +3819,117 @@ public final class VoiceChatController: ViewController { self.actionButton.isDisabled = !actionButtonEnabled self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true) + var hasCameraButton = self.callState?.isVideoEnabled ?? false + switch actionButtonState { + case let .active(state): + switch state { + case .cantSpeak: + hasCameraButton = false + case .on, .muted: + break + } + case .connecting: + if !self.connectedOnce { + hasCameraButton = false + } + case .scheduled, .button: + hasCameraButton = false + } + + let upperButtonDistance: CGFloat = 12.0 + let firstButtonFrame: CGRect + let secondButtonFrame: CGRect + let thirdButtonFrame: CGRect + let forthButtonFrame: CGRect + + let leftButtonFrame: CGRect + if self.isScheduled || !hasCameraButton { + leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) + } else { + leftButtonFrame = CGRect(origin: CGPoint(x: sideButtonOrigin, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height - upperButtonDistance - cameraButtonSize.height) / 2.0) + upperButtonDistance + cameraButtonSize.height), size: sideButtonSize) + } + let rightButtonFrame = CGRect(origin: CGPoint(x: size.width - sideButtonOrigin - sideButtonSize.width, y: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize) + + switch effectiveDisplayMode { + case .modal: + if isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) + } else { + firstButtonFrame = secondButtonFrame + } + } else { + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: floor(leftButtonFrame.midX - cameraButtonSize.width / 2.0), y: leftButtonFrame.minY - upperButtonDistance - cameraButtonSize.height), size: cameraButtonSize) + } else { + firstButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.center.x - cameraButtonSize.width / 2.0, y: leftButtonFrame.center.y - cameraButtonSize.height / 2.0), size: cameraButtonSize) + } + secondButtonFrame = leftButtonFrame + thirdButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) + forthButtonFrame = rightButtonFrame + } + case let .fullscreen(controlsHidden): + if isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + forthButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset), size: sideButtonSize) + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: x, y: thirdButtonPreFrame.maxY + spacing), size: sideButtonSize) + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: x, y: layout.size.height - sideInset - sideButtonSize.height), size: sideButtonSize) + } else { + firstButtonFrame = secondButtonFrame + } + } else { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let y = controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0) + if hasCameraButton { + firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + } else { + firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = firstButtonFrame + } + let thirdButtonPreFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + thirdButtonFrame = CGRect(origin: CGPoint(x: floor(thirdButtonPreFrame.midX - centralButtonSize.width / 2.0), y: floor(thirdButtonPreFrame.midY - centralButtonSize.height / 2.0)), size: centralButtonSize) + forthButtonFrame = CGRect(origin: CGPoint(x: thirdButtonPreFrame.maxX + spacing, y: y), size: sideButtonSize) + } + } + let buttonHeight = self.scheduleCancelButton.updateLayout(width: size.width - 32.0, transition: .immediate) self.scheduleCancelButton.frame = CGRect(x: 16.0, y: 137.0, width: size.width - 32.0, height: buttonHeight) @@ -3876,7 +3949,9 @@ public final class VoiceChatController: ViewController { } self.updateButtons(transition: buttonsTransition) + self.cameraButton.isUserInteractionEnabled = hasCameraButton if self.audioButton.supernode === self.bottomPanelNode { + transition.updateAlpha(node: self.cameraButton, alpha: hasCameraButton ? 1.0 : 0.0) transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame) if !self.animatingButtonsSwap || transition.isAnimated { @@ -3889,8 +3964,8 @@ public final class VoiceChatController: ViewController { self?.animatingButtonsSwap = false }) } + transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame) } - transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame) transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame) } if isFirstTime { @@ -4275,11 +4350,7 @@ public final class VoiceChatController: ViewController { fullscreenIndex += 1 } } - - - self.requestedVideoChannels = requestedVideoChannels - self.updateRequestedVideoChannels() - + for peer in invitedPeers { if processedPeerIds.contains(peer.id) { continue @@ -4303,11 +4374,15 @@ public final class VoiceChatController: ViewController { ), index)) index += 1 } + + self.requestedVideoChannels = requestedVideoChannels - guard self.didSetDataReady else { + guard self.didSetDataReady || !self.isPanning else { return } + self.updateRequestedVideoChannels() + self.endpointToPeerId = endpointIdToPeerId self.peerIdToEndpoint = peerIdToEndpointId @@ -4318,7 +4393,7 @@ public final class VoiceChatController: ViewController { var canInvite = true var inviteIsLink = false if let peer = self.peer as? TelegramChannel { - if peer.flags.contains(.isGigagroup) || (peer.addressName?.isEmpty ?? true) { + if peer.flags.contains(.isGigagroup) { if peer.flags.contains(.isCreator) || peer.adminRights != nil { } else { canInvite = false @@ -4472,48 +4547,29 @@ public final class VoiceChatController: ViewController { } private func updateMainVideo(waitForFullSize: Bool, updateMembers: Bool = true, force: Bool = false, completion: (() -> Void)? = nil) { - let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker?.0 - guard effectiveMainSpeaker != self.effectiveSpeaker?.0 || force else { + let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker.flatMap { ($0.0, $0.1) } + guard effectiveMainSpeaker?.0 != self.effectiveSpeaker?.0 || effectiveMainSpeaker?.1 != self.effectiveSpeaker?.1 || force else { return } let currentEntries = self.currentFullscreenEntries - var effectivePeer: (PeerId, String?, String?)? = nil - var anyPeer: (PeerId, String?, String?)? = nil - if let peerId = effectiveMainSpeaker { + var effectiveSpeaker: (PeerId, String?)? = nil + var anySpeakerWithVideo: (PeerId, String?)? = nil + var anySpeaker: PeerId? = nil + if let (peerId, preferredVideoEndpointId) = effectiveMainSpeaker { for entry in currentEntries { switch entry { case let .peer(peer, _): if peer.peer.id == peerId { - var effectiveEndpointId = peer.effectiveVideoEndpointId - if self.switchedToCameraPeers.contains(peer.peer.id), let videoEndpointId = peer.videoEndpointId { - effectiveEndpointId = videoEndpointId - } - - var otherEndpointId: String? - if effectiveEndpointId != peer.videoEndpointId { - otherEndpointId = peer.videoEndpointId - } else if effectiveEndpointId != peer.presentationEndpointId { - otherEndpointId = peer.presentationEndpointId - } - - effectivePeer = (peer.peer.id, effectiveEndpointId, otherEndpointId) - } else if anyPeer == nil && peer.effectiveVideoEndpointId != nil { - var effectiveEndpointId = peer.effectiveVideoEndpointId - if self.switchedToCameraPeers.contains(peer.peer.id), let videoEndpointId = peer.videoEndpointId { - effectiveEndpointId = videoEndpointId - } - - var otherEndpointId: String? - if effectiveEndpointId != peer.videoEndpointId { - otherEndpointId = peer.videoEndpointId - } else if effectiveEndpointId != peer.presentationEndpointId { - otherEndpointId = peer.presentationEndpointId - } - - if let endpointId = effectiveEndpointId { - anyPeer = (peer.peer.id, endpointId, otherEndpointId) + if let preferredVideoEndpointId = preferredVideoEndpointId, peer.videoEndpointId == preferredVideoEndpointId || peer.presentationEndpointId == preferredVideoEndpointId { + effectiveSpeaker = (peerId, preferredVideoEndpointId) + } else { + effectiveSpeaker = (peerId, peer.effectiveVideoEndpointId) } + } else if anySpeakerWithVideo == nil, let videoEndpointId = peer.effectiveVideoEndpointId { + anySpeakerWithVideo = (peer.peer.id, videoEndpointId) + } else if anySpeaker == nil { + anySpeaker = peer.peer.id } default: break @@ -4521,22 +4577,21 @@ public final class VoiceChatController: ViewController { } } - if effectivePeer == nil { - if self.currentForcedSpeaker != nil { - self.currentForcedSpeaker = nil - } - if self.currentDominantSpeaker != nil { + if effectiveSpeaker == nil { + self.currentForcedSpeaker = nil + effectiveSpeaker = anySpeakerWithVideo ?? anySpeaker.flatMap { ($0, nil) } + if let (peerId, videoEndpointId) = effectiveSpeaker { + self.currentDominantSpeaker = (peerId, videoEndpointId, CACurrentMediaTime()) + } else { self.currentDominantSpeaker = nil } - effectivePeer = anyPeer } - self.effectiveSpeaker = effectivePeer.flatMap { ($0.0, $0.1) } + self.effectiveSpeaker = effectiveSpeaker if updateMembers { self.updateMembers() } - - self.mainStageNode.update(peer: self.effectiveSpeaker, waitForFullSize: waitForFullSize, completion: { + self.mainStageNode.update(peer: effectiveSpeaker, waitForFullSize: waitForFullSize, completion: { completion?() }) } @@ -4721,6 +4776,7 @@ public final class VoiceChatController: ViewController { self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in if let strongSelf = self { strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) + strongSelf.updateMembers() } }) } @@ -4836,6 +4892,7 @@ public final class VoiceChatController: ViewController { self.mainStageContainerNode.layer.animateBounds(from: previousBounds, to: self.mainStageContainerNode.bounds, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in if let strongSelf = self { strongSelf.contentContainer.insertSubnode(strongSelf.mainStageContainerNode, belowSubnode: strongSelf.transitionContainerNode) + strongSelf.updateMembers() } }) } @@ -5441,13 +5498,85 @@ public final class VoiceChatController: ViewController { } if case .fullscreen(false) = displayMode, case .modal = previousDisplayMode { - self.updateMainVideo(waitForFullSize: true, updateMembers: false, force: true, completion: { + self.updateMainVideo(waitForFullSize: true, updateMembers: true, force: true, completion: { completion() }) } else { completion() } } + + fileprivate var actionButtonPosition: CGPoint { + guard let (layout, _) = self.validLayout else { + return CGPoint() + } + var size = layout.size + if case .regular = layout.metrics.widthClass { + size.width = floor(min(size.width, size.height) * 0.5) + } + let hasCameraButton = self.cameraButton.isUserInteractionEnabled + let centralButtonSide = min(size.width, size.height) - 32.0 + let centralButtonSize = CGSize(width: centralButtonSide, height: centralButtonSide) + + switch self.displayMode { + case .modal: + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0) + let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + return actionButtonFrame.center + } else { + let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - centralButtonSize.width) / 2.0), y: layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + floor((self.effectiveBottomAreaHeight - centralButtonSize.height) / 2.0) - 3.0), size: centralButtonSize) + return actionButtonFrame.center + } + case let .fullscreen(controlsHidden): + if self.isLandscape { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.height - sideInset * 2.0 - sideButtonSize.height * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let x = layout.size.width - fullscreenBottomAreaHeight - layout.safeInsets.right + (controlsHidden ? fullscreenBottomAreaHeight + layout.safeInsets.right + 30.0 : floor((fullscreenBottomAreaHeight - sideButtonSize.width) / 2.0)) + let actionButtonFrame = CGRect(origin: CGPoint(x: x, y: sideInset + sideButtonSize.height + spacing), size: sideButtonSize) + return actionButtonFrame.center + } else { + let sideInset: CGFloat + let buttonsCount: Int + if hasCameraButton { + sideInset = 26.0 + buttonsCount = 4 + } else { + sideInset = 42.0 + buttonsCount = 3 + } + let spacing = floor((layout.size.width - sideInset * 2.0 - sideButtonSize.width * CGFloat(buttonsCount)) / (CGFloat(buttonsCount - 1))) + let y = layout.size.height - self.effectiveBottomAreaHeight - layout.intrinsicInsets.bottom + (controlsHidden ? self.effectiveBottomAreaHeight + layout.intrinsicInsets.bottom + 30.0: floor((self.effectiveBottomAreaHeight - sideButtonSize.height) / 2.0)) + let secondButtonFrame: CGRect + if hasCameraButton { + let firstButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + secondButtonFrame = CGRect(origin: CGPoint(x: firstButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + } else { + secondButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: y), size: sideButtonSize) + } + let actionButtonFrame = CGRect(origin: CGPoint(x: secondButtonFrame.maxX + spacing, y: y), size: sideButtonSize) + return actionButtonFrame.center + } + } + } } private let sharedContext: SharedAccountContext @@ -5522,7 +5651,7 @@ public final class VoiceChatController: ViewController { self.idleTimerExtensionDisposable.dispose() if let currentOverlayController = self.currentOverlayController { - currentOverlayController.animateOut(reclaim: false, completion: { _ in }) + currentOverlayController.animateOut(reclaim: false, targetPosition: CGPoint(), completion: { _ in }) } } @@ -5626,7 +5755,7 @@ public final class VoiceChatController: ViewController { self.reclaimActionButton = { [weak self, weak overlayController] in if let strongSelf = self { - overlayController?.animateOut(reclaim: true, completion: { immediate in + overlayController?.animateOut(reclaim: true, targetPosition: strongSelf.controllerNode.actionButtonPosition, completion: { immediate in if let strongSelf = self, immediate { strongSelf.controllerNode.actionButton.ignoreHierarchyChanges = true strongSelf.controllerNode.bottomPanelNode.addSubnode(strongSelf.controllerNode.actionButton) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift index 39fc53d115..5de7e5942a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -31,16 +31,7 @@ private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) private let borderLineWidth: CGFloat = 2.0 -private let borderImage = generateImage(CGSize(width: tileSize.width, height: tileSize.height), rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - context.setLineWidth(borderLineWidth) - context.setStrokeColor(constructiveColor.cgColor) - - context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: (borderLineWidth - UIScreenPixel) / 2.0, dy: (borderLineWidth - UIScreenPixel) / 2.0), cornerRadius: backgroundCornerRadius - UIScreenPixel).cgPath) - context.strokePath() -}) + private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) private let fadeHeight: CGFloat = 50.0 @@ -78,6 +69,7 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { let peer: Peer let icon: Icon let text: VoiceChatParticipantItem.ParticipantText + let textColor: Color let color: Color let isLandscape: Bool let active: Bool @@ -89,13 +81,14 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { public let selectable: Bool = true - public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, color: Color, isLandscape: Bool, active: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { + public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, textColor: Color, color: Color, isLandscape: Bool, active: Bool, getAudioLevel: (() -> Signal)?, getVideo: @escaping () -> GroupVideoNode?, action: ((ASDisplayNode?) -> Void)?, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, getUpdatingAvatar: @escaping () -> Signal<(TelegramMediaImageRepresentation, Float)?, NoError>) { self.presentationData = presentationData self.nameDisplayOrder = nameDisplayOrder self.context = context self.peer = peer self.icon = icon self.text = text + self.textColor = textColor self.color = color self.isLandscape = isLandscape self.active = active @@ -155,7 +148,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { let backgroundImageNode: ASImageNode private let extractedBackgroundImageNode: ASImageNode let offsetContainerNode: ASDisplayNode - let highlightNode: ASImageNode + let highlightNode: VoiceChatTileHighlightNode private var extractedRect: CGRect? private var nonExtractedRect: CGRect? @@ -212,9 +205,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.extractedBackgroundImageNode.displaysAsynchronously = false self.extractedBackgroundImageNode.alpha = 0.0 - self.highlightNode = ASImageNode() - self.highlightNode.displaysAsynchronously = false - self.highlightNode.image = borderImage + self.highlightNode = VoiceChatTileHighlightNode() self.highlightNode.isHidden = true self.offsetContainerNode = ASDisplayNode() @@ -235,6 +226,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.videoContainerNode.addSubnode(self.videoFadeNode) self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale @@ -464,7 +456,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { var titleColor = item.presentationData.theme.list.itemPrimaryTextColor if !hasVideo || item.active { - switch item.color { + switch item.textColor { case .generic: titleColor = item.presentationData.theme.list.itemPrimaryTextColor case .accent: @@ -492,16 +484,23 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { } else if let channel = item.peer as? TelegramChannel { titleAttributedString = NSAttributedString(string: channel.title, font: currentBoldFont, textColor: titleColor) } - + var wavesColor = UIColor(rgb: 0x34c759) + var gradient: VoiceChatTileHighlightNode.Gradient = .active switch item.color { case .accent: wavesColor = accentColor + case .constructive: + gradient = .speaking case .destructive: wavesColor = destructiveColor default: break } + var titleUpdated = false + if let currentColor = currentItem?.textColor, currentColor != item.textColor { + titleUpdated = true + } let leftInset: CGFloat = 58.0 + params.leftInset @@ -610,6 +609,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.contextSourceNode.contentNode.frame = contentBounds strongSelf.actionContainerNode.frame = contentBounds strongSelf.highlightNode.frame = contentBounds + strongSelf.highlightNode.updateLayout(size: contentBounds.size, transition: .immediate) strongSelf.containerNode.isGestureEnabled = item.contextAction != nil @@ -628,6 +628,15 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { transition = .immediate } + + if titleUpdated, let snapshotView = strongSelf.titleNode.view.snapshotContentTree() { + strongSelf.titleNode.view.superview?.addSubview(snapshotView) + snapshotView.frame = strongSelf.titleNode.view.frame + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + let _ = titleApply() transition.updateFrame(node: strongSelf.titleNode, frame: titleFrame) @@ -652,6 +661,8 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame) + strongSelf.highlightNode.updateGlowAndGradientAnimations(type: gradient, animated: true) + let blobFrame = avatarFrame.insetBy(dx: -18.0, dy: -18.0) if let getAudioLevel = item.getAudioLevel { if !strongSelf.didSetupAudioLevel || currentItem?.peer.id != item.peer.id { @@ -663,6 +674,8 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { return } + strongSelf.highlightNode.updateLevel(CGFloat(value)) + if strongSelf.audioLevelView == nil, value > 0.0 { let audioLevelView = VoiceBlobView( frame: blobFrame, @@ -701,9 +714,6 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { if value > 0.02 { audioLevelView.startAnimating() avatarScale = 1.03 + level * 0.13 - if let wavesColor = strongSelf.wavesColor { - audioLevelView.setColor(wavesColor, animated: true) - } if let silenceTimer = strongSelf.silenceTimer { silenceTimer.invalidate() @@ -721,6 +731,10 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { } } + if let wavesColor = strongSelf.wavesColor { + audioLevelView.setColor(wavesColor, animated: true) + } + if !strongSelf.animatingSelection { let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) transition.updateTransformScale(node: strongSelf.avatarNode, scale: strongSelf.isExtracted ? 1.0 : avatarScale, beginWithCurrentState: true) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index 39f45c8eb9..234e50011e 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -18,6 +18,7 @@ import AvatarNode import AudioBlob import TextFormat import Markdown +import ContextUI private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white) private let backgroundCornerRadius: CGFloat = 11.0 @@ -44,7 +45,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { private let pinButtonNode: HighlightTrackingButtonNode private let pinButtonIconNode: ASImageNode private let pinButtonTitleNode: ImmediateTextNode - private var audioLevelView: VoiceBlobView? + private let audioLevelNode: VoiceChatBlobNode private let audioLevelDisposable = MetaDisposable() private let speakingPeerDisposable = MetaDisposable() private let speakingAudioLevelDisposable = MetaDisposable() @@ -134,12 +135,15 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.backdropAvatarNode.displaysAsynchronously = false self.backdropAvatarNode.isHidden = true + self.audioLevelNode = VoiceChatBlobNode(size: CGSize(width: 300.0, height: 300.0)) + self.avatarNode = ImageNode() self.avatarNode.displaysAsynchronously = false self.avatarNode.isHidden = true self.titleNode = ImmediateTextNode() self.titleNode.alpha = 0.0 + self.titleNode.displaysAsynchronously = false self.titleNode.isUserInteractionEnabled = false self.microphoneNode = VoiceChatMicrophoneNode() @@ -152,6 +156,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.speakingAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) self.speakingTitleNode = ImmediateTextNode() + self.speakingTitleNode.displaysAsynchronously = false super.init() @@ -163,6 +168,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.addSubnode(self.bottomFadeNode) self.addSubnode(self.bottomFillNode) self.addSubnode(self.backdropAvatarNode) + self.addSubnode(self.audioLevelNode) self.addSubnode(self.avatarNode) self.addSubnode(self.titleNode) self.addSubnode(self.microphoneNode) @@ -460,8 +466,8 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.speakingPeerDisposable.set(nil) self.speakingAudioLevelDisposable.set(nil) - let audioLevelView = self.audioLevelView - self.audioLevelView = nil + let audioLevelView = self.speakingAudioLevelView + self.speakingAudioLevelView = nil if !self.speakingContainerNode.alpha.isZero { self.speakingContainerNode.alpha = 0.0 @@ -501,94 +507,62 @@ final class VoiceChatMainStageNode: ASDisplayNode { } } - self.pinButtonTitleNode.isHidden = !pinned - self.pinButtonIconNode.image = !pinned ? generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) : generateTintedImage(image: UIImage(bundleImageName: "Call/Unpin"), color: .white) - - var firstTime = true - var wavesColor = UIColor(rgb: 0x34c759) - if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id { - if let audioLevelView = self.audioLevelView { - self.audioLevelView = nil - audioLevelView.removeFromSuperview() - } - - let blobFrame = self.avatarNode.frame.insetBy(dx: -60.0, dy: -60.0) - self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id) - |> deliverOnMainQueue).start(next: { [weak self] value in - guard let strongSelf = self else { - return - } - - if strongSelf.audioLevelView == nil, value > 0.0 { - let audioLevelView = VoiceBlobView( - frame: blobFrame, - maxLevel: 1.5, - smallBlobRange: (0, 0), - mediumBlobRange: (0.69, 0.87), - bigBlobRange: (0.71, 1.0) - ) - audioLevelView.isHidden = strongSelf.currentPeer?.1 != nil - - audioLevelView.setColor(wavesColor) - audioLevelView.alpha = 1.0 - - strongSelf.audioLevelView = audioLevelView - strongSelf.view.insertSubview(audioLevelView, belowSubview: strongSelf.avatarNode.view) - } - - let level = min(1.5, max(0.0, CGFloat(value))) - if let audioLevelView = strongSelf.audioLevelView { - audioLevelView.updateLevel(CGFloat(value)) - - let avatarScale: CGFloat - if value > 0.02 { - audioLevelView.startAnimating(immediately: firstTime) - avatarScale = 1.03 + level * 0.13 - audioLevelView.setColor(wavesColor, animated: true) - - if let silenceTimer = strongSelf.silenceTimer { - silenceTimer.invalidate() - strongSelf.silenceTimer = nil - } - } else { - avatarScale = 1.0 - if strongSelf.silenceTimer == nil { - let silenceTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in - self?.audioLevelView?.stopAnimating(duration: 0.75) - self?.silenceTimer = nil - }, queue: Queue.mainQueue()) - strongSelf.silenceTimer = silenceTimer - silenceTimer.start() - } - } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) - transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true) - } - firstTime = false - })) - } - + var gradient: VoiceChatBlobNode.Gradient = .active var muted = false var state = peerEntry.state if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { state = .listening } switch state { - case .listening: - if let muteState = peerEntry.muteState, muteState.mutedByYou { + case .listening: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + gradient = .muted + muted = true + } else { + gradient = .active + muted = peerEntry.muteState != nil + } + case .speaking: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + gradient = .muted + muted = true + } else { + gradient = .speaking + muted = false + } + default: muted = true - } else { - muted = peerEntry.muteState != nil - } - case .speaking: - if let muteState = peerEntry.muteState, muteState.mutedByYou { - muted = true - } else { - muted = false - } - case .raisedHand, .invited: - muted = true + } + self.audioLevelNode.updateGlowAndGradientAnimations(type: gradient, animated: true) + + self.pinButtonTitleNode.isHidden = !pinned + self.pinButtonIconNode.image = !pinned ? generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) : generateTintedImage(image: UIImage(bundleImageName: "Call/Unpin"), color: .white) + + self.audioLevelNode.startAnimating(immediately: true) + + if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id { + self.audioLevelDisposable.set((getAudioLevel(peerEntry.peer.id) + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + strongSelf.audioLevelNode.isHidden = strongSelf.currentPeer?.1 != nil + + let level = min(1.5, max(0.0, CGFloat(value))) + + strongSelf.audioLevelNode.updateLevel(CGFloat(value)) + + let avatarScale: CGFloat + if value > 0.02 { + avatarScale = 1.03 + level * 0.13 + } else { + avatarScale = 1.0 + } + + let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) + transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true) + })) } self.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: .white), animated: true) @@ -598,7 +572,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.backdropAvatarNode.isHidden = hidden self.backdropEffectView?.isHidden = hidden self.avatarNode.isHidden = hidden - self.audioLevelView?.isHidden = hidden + self.audioLevelNode.isHidden = hidden } func update(peer: (peer: PeerId, endpointId: String?)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) { @@ -625,6 +599,16 @@ final class VoiceChatMainStageNode: ASDisplayNode { } let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView) + videoNode.sourceContainerNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + videoNode.isUserInteractionEnabled = true let previousVideoNode = strongSelf.currentVideoNode strongSelf.currentVideoNode = videoNode strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backgroundNode) @@ -731,9 +715,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { let avatarSize = CGSize(width: 180.0, height: 180.0) let avatarFrame = CGRect(origin: CGPoint(x: (size.width - avatarSize.width) / 2.0, y: (size.height - avatarSize.height) / 2.0), size: avatarSize) transition.updateFrame(node: self.avatarNode, frame: avatarFrame) - if let audioLevelView = self.audioLevelView { - transition.updatePosition(layer: audioLevelView.layer, position: avatarFrame.center) - } + transition.updateFrame(node: self.audioLevelNode, frame: avatarFrame.insetBy(dx: -60.0, dy: -60.0)) let animationSize = CGSize(width: 36.0, height: 36.0) let titleSize = self.titleNode.updateLayout(size) @@ -818,7 +800,7 @@ class VoiceChatBlobNode: ASDisplayNode { self.foregroundGradientLayer.type = .radial self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] - self.foregroundGradientLayer.locations = [0.0, 0.55, 1.0] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) @@ -835,6 +817,8 @@ class VoiceChatBlobNode: ASDisplayNode { strongSelf.updateAnimations() } } + + self.addSubnode(self.hierarchyTrackingNode) } override func didLoad() { @@ -858,6 +842,14 @@ class VoiceChatBlobNode: ASDisplayNode { self.blobView.updateLevel(level) } + func startAnimating(immediately: Bool) { + self.blobView.startAnimating(immediately: immediately) + } + + func stopAnimating() { + self.blobView.stopAnimating(duration: 0.8) + } + private func setupGradientAnimations() { if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { } else { @@ -890,7 +882,12 @@ class VoiceChatBlobNode: ASDisplayNode { } } + private var gradient: Gradient? func updateGlowAndGradientAnimations(type: Gradient, animated: Bool = true) { + guard self.gradient != type else { + return + } + self.gradient = type let initialColors = self.foregroundGradientLayer.colors let targetColors: [CGColor] switch type { @@ -912,6 +909,7 @@ class VoiceChatBlobNode: ASDisplayNode { override func layout() { super.layout() - self.blobView.frame = CGRect(x: 0.0, y: 0.0, width: self.bounds.width, height: self.bounds.height) + self.blobView.frame = self.bounds + self.foregroundGradientLayer.frame = self.bounds.insetBy(dx: -24.0, dy: -24.0) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift index 3de080559d..e67f3570de 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift @@ -162,14 +162,13 @@ public final class VoiceChatOverlayController: ViewController { private var animating = false private var dismissed = false - func animateOut(reclaim: Bool, completion: @escaping (Bool) -> Void) { + func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { guard let actionButton = self.controller?.actionButton, let leftButton = self.controller?.audioOutputNode, let rightButton = self.controller?.leaveNode, let layout = self.validLayout else { return } if reclaim { self.dismissed = true - let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - bottomAreaHeight / 2.0 - 3.0) if self.isSlidOffscreen { self.isSlidOffscreen = false self.isButtonHidden = true @@ -371,8 +370,8 @@ public final class VoiceChatOverlayController: ViewController { completion?() } - func animateOut(reclaim: Bool, completion: @escaping (Bool) -> Void) { - self.controllerNode.animateOut(reclaim: reclaim, completion: completion) + func animateOut(reclaim: Bool, targetPosition: CGPoint, completion: @escaping (Bool) -> Void) { + self.controllerNode.animateOut(reclaim: reclaim, targetPosition: targetPosition, completion: completion) } public func updateVisibility() { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index c1f369bcc0..955a14a8a7 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -419,12 +419,12 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } let springDuration: Double = isExtracted ? 0.42 : 0.3 - let springDamping: CGFloat = isExtracted ? 104.0 : 1000.0 + let springDamping: CGFloat = isExtracted ? 124.0 : 1000.0 let itemBackgroundColor: UIColor = item.getIsExpanded() ? UIColor(rgb: 0x1c1c1e) : UIColor(rgb: 0x2c2c2e) if !extractedVerticalOffset.isZero { - let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) if isExtracted { strongSelf.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in let bounds = CGRect(origin: CGPoint(), size: size) @@ -856,8 +856,9 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { let constrainedWidth = params.width - leftInset - 12.0 - rightInset - 30.0 - titleIconsWidth let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let expandedWidth = min(params.width - leftInset - rightInset, params.availableHeight - 30.0) let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, false) - let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), item.expandedText ?? item.text, true) + let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(CGSize(width: expandedWidth - 8.0 - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), item.expandedText ?? item.text, params.availableHeight > params.width) let titleSpacing: CGFloat = statusLayout.height == 0.0 ? 0.0 : 1.0 @@ -903,6 +904,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { var extractedHeight = extractedRect.height + expandedStatusLayout.height - statusLayout.height var extractedVerticalOffset: CGFloat = 0.0 if item.peer.smallProfileImage != nil { + extractedRect.size.width = min(extractedRect.width, params.availableHeight - 20.0) extractedVerticalOffset = extractedRect.width extractedHeight += extractedVerticalOffset } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift index 2426ee327d..458401d050 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift @@ -173,7 +173,7 @@ final class VoiceChatPeerProfileNode: ASDisplayNode { if animate { let springDuration: Double = !self.appeared ? 0.42 : 0.3 - let springDamping: CGFloat = !self.appeared ? 104.0 : 1000.0 + let springDamping: CGFloat = !self.appeared ? 124.0 : 1000.0 let initialInfoPosition = self.infoNode.position self.infoNode.layer.position = infoFrame.center @@ -188,9 +188,9 @@ final class VoiceChatPeerProfileNode: ASDisplayNode { } func animateIn(from sourceNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition) { - let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut) let springDuration: Double = 0.42 - let springDamping: CGFloat = 104.0 + let springDamping: CGFloat = 124.0 if let sourceNode = sourceNode as? VoiceChatTileItemNode { let sourceRect = sourceNode.bounds diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift index 207868260b..bef3144367 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift @@ -11,18 +11,7 @@ import TelegramUIPreferences import TelegramPresentationData private let backgroundCornerRadius: CGFloat = 11.0 -private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) private let borderLineWidth: CGFloat = 2.0 -private let borderImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in - let bounds = CGRect(origin: CGPoint(), size: size) - context.clear(bounds) - - context.setLineWidth(borderLineWidth) - context.setStrokeColor(constructiveColor.cgColor) - - context.addPath(UIBezierPath(roundedRect: bounds.insetBy(dx: (borderLineWidth - UIScreenPixel) / 2.0, dy: (borderLineWidth - UIScreenPixel) / 2.0), cornerRadius: backgroundCornerRadius - UIScreenPixel).cgPath) - context.strokePath() -}) final class VoiceChatTileItem: Equatable { enum Icon: Equatable { @@ -107,7 +96,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { private let titleNode: ImmediateTextNode private let iconNode: ASImageNode private var animationNode: VoiceChatMicrophoneNode? - var highlightNode: ASImageNode + var highlightNode: VoiceChatTileHighlightNode private let statusNode: VoiceChatParticipantStatusNode private var profileNode: VoiceChatPeerProfileNode? @@ -151,10 +140,9 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true - self.highlightNode = ASImageNode() - self.highlightNode.contentMode = .scaleToFill - self.highlightNode.image = borderImage?.stretchableImage(withLeftCapWidth: 12, topCapHeight: 12) + self.highlightNode = VoiceChatTileHighlightNode() self.highlightNode.alpha = 0.0 + self.highlightNode.updateGlowAndGradientAnimations(type: .speaking) super.init() @@ -256,19 +244,13 @@ final class VoiceChatTileItemNode: ASDisplayNode { let previousItem = self.item self.item = item - if false, let getAudioLevel = item.getAudioLevel { + if let getAudioLevel = item.getAudioLevel { self.audioLevelDisposable.set((getAudioLevel() |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else { return } - - let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - if value > 0.4 { - transition.updateAlpha(node: strongSelf.highlightNode, alpha: 1.0) - } else { - transition.updateAlpha(node: strongSelf.highlightNode, alpha: 0.0) - } + strongSelf.highlightNode.updateLevel(CGFloat(value)) })) } @@ -368,6 +350,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { transition.updateFrame(node: self.backgroundNode, frame: bounds) transition.updateFrame(node: self.highlightNode, frame: bounds) + self.highlightNode.updateLayout(size: bounds.size, transition: transition) transition.updateFrame(node: self.infoNode, frame: bounds) transition.updateFrame(node: self.fadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: fadeHeight)) @@ -463,8 +446,8 @@ class VoiceChatTileHighlightNode: ASDisplayNode { case muted } - private let maskView: UIView - private let maskLayer = CAShapeLayer() + private var maskView: UIView? + private let maskLayer = CALayer() private let foregroundGradientLayer = CAGradientLayer() @@ -477,8 +460,11 @@ class VoiceChatTileHighlightNode: ASDisplayNode { private var displayLinkAnimator: ConstantDisplayLinkAnimator? override init() { - self.maskView = UIView() - self.maskView.layer.addSublayer(self.maskLayer) + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor, blue.cgColor] + self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) var updateInHierarchy: ((Bool) -> Void)? self.hierarchyTrackingNode = HierarchyTrackingNode({ value in @@ -494,16 +480,29 @@ class VoiceChatTileHighlightNode: ASDisplayNode { } } - displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in + self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in guard let strongSelf = self else { return } strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 } + + self.addSubnode(self.hierarchyTrackingNode) } override func didLoad() { super.didLoad() + self.layer.addSublayer(self.foregroundGradientLayer) + + let maskView = UIView() + maskView.layer.addSublayer(self.maskLayer) + self.maskView = maskView + + self.maskLayer.masksToBounds = true + self.maskLayer.cornerRadius = backgroundCornerRadius - UIScreenPixel + self.maskLayer.borderColor = UIColor.white.cgColor + self.maskLayer.borderWidth = borderLineWidth + self.view.mask = self.maskView } @@ -519,6 +518,15 @@ class VoiceChatTileHighlightNode: ASDisplayNode { self.audioLevel = level } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let bounds = CGRect(origin: CGPoint(), size: size) + if let maskView = self.maskView { + transition.updateFrame(view: maskView, frame: bounds) + } + transition.updateFrame(layer: self.maskLayer, frame: bounds) + transition.updateFrame(layer: self.foregroundGradientLayer, frame: bounds) + } + private func setupGradientAnimations() { if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { } else { @@ -551,7 +559,12 @@ class VoiceChatTileHighlightNode: ASDisplayNode { } } + private var gradient: Gradient? func updateGlowAndGradientAnimations(type: Gradient, animated: Bool = true) { + guard self.gradient != type else { + return + } + self.gradient = type let initialColors = self.foregroundGradientLayer.colors let targetColors: [CGColor] switch type { @@ -568,5 +581,6 @@ class VoiceChatTileHighlightNode: ASDisplayNode { if animated { self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3) } + self.updateAnimations() } }