diff --git a/submodules/AudioBlob/Sources/BlobView.swift b/submodules/AudioBlob/Sources/BlobView.swift index dc7e9823c3..7296b045b8 100644 --- a/submodules/AudioBlob/Sources/BlobView.swift +++ b/submodules/AudioBlob/Sources/BlobView.swift @@ -107,8 +107,8 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco guard !isAnimating else { return } isAnimating = true - mediumBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false) - bigBlob.layer.animateScale(from: 0.5, to: 1, duration: 0.15, removeOnCompletion: false) + mediumBlob.layer.animateScale(from: 0.75, to: 1, duration: 0.35, removeOnCompletion: false) + bigBlob.layer.animateScale(from: 0.75, to: 1, duration: 0.35, removeOnCompletion: false) updateBlobsState() @@ -123,8 +123,8 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco guard isAnimating else { return } isAnimating = false - mediumBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) - bigBlob.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) + mediumBlob.layer.animateScale(from: 1.0, to: 0.75, duration: duration, removeOnCompletion: false) + bigBlob.layer.animateScale(from: 1.0, to: 0.75, duration: duration, removeOnCompletion: false) updateBlobsState() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 52411e7b12..02f9df797c 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -1390,6 +1390,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } else { overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) + + if contentContainerFrame.maxX > layout.size.width { + contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentContainerFrame.width) / 2.0), y: contentContainerFrame.minY), size: contentContainerFrame.size) + } } if case let .extracted(source) = self.source, source.centerVertically { diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 97970434ef..367eeaf715 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -185,7 +185,10 @@ - (void)cleanup { - [_diskCache cleanup]; + TGModernCache *diskCache = _diskCache; + TGDispatchAfter(10.0, dispatch_get_main_queue(), ^{ + [diskCache cleanup]; + }); [[NSFileManager defaultManager] removeItemAtPath:_fullSizeResultsUrl.path error:nil]; [[NSFileManager defaultManager] removeItemAtPath:_paintingImagesUrl.path error:nil]; @@ -991,7 +994,7 @@ + (NSUInteger)diskMemoryLimit { - return 64 * 1024 * 1024; + return 512 * 1024 * 1024; } + (NSUInteger)imageSoftMemoryLimit diff --git a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m index cbf564209e..166715d56b 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoEditorController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoEditorController.m @@ -146,7 +146,6 @@ { _context = context; _actionHandle = [[ASHandle alloc] initWithDelegate:self releaseOnMainThread:true]; - _standaloneEditingContext = [[TGMediaEditingContext alloc] init]; self.automaticallyManageScrollViewInsets = false; self.autoManageStatusBarBackground = false; @@ -2182,10 +2181,14 @@ - (TGMediaEditingContext *)editingContext { - if (_editingContext) + if (_editingContext) { return _editingContext; - else + } else { + if (_standaloneEditingContext == nil) { + _standaloneEditingContext = [[TGMediaEditingContext alloc] init]; + } return _standaloneEditingContext; + } } - (void)doneButtonLongPressed:(UIButton *)sender diff --git a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift index 1200ac2778..574f744121 100644 --- a/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift +++ b/submodules/TelegramCallsUI/Sources/GroupVideoNode.swift @@ -40,12 +40,15 @@ final class GroupVideoNode: ASDisplayNode { self.backdropVideoViewContainer.addSubview(backdropVideoView.view) self.view.addSubview(self.backdropVideoViewContainer) + let effect: UIVisualEffect if #available(iOS 13.0, *) { - let backdropEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterialDark)) - self.view.addSubview(backdropEffectView) - self.backdropEffectView = backdropEffectView + effect = UIBlurEffect(style: .systemThinMaterialDark) } else { + effect = UIBlurEffect(style: .dark) } + let backdropEffectView = UIVisualEffectView(effect: effect) + self.view.addSubview(backdropEffectView) + self.backdropEffectView = backdropEffectView } self.videoViewContainer.addSubview(self.videoView.view) @@ -131,10 +134,15 @@ final class GroupVideoNode: ASDisplayNode { } } + var aspectRatio: CGFloat { + return self.videoView.getAspect() + } + func updateLayout(size: CGSize, isLandscape: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, isLandscape) - transition.updateFrameAsPositionAndBounds(layer: self.videoViewContainer.layer, frame: CGRect(origin: CGPoint(), size: size)) - transition.updateFrameAsPositionAndBounds(layer: self.backdropVideoViewContainer.layer, frame: CGRect(origin: CGPoint(), size: size)) + let bounds = CGRect(origin: CGPoint(), size: size) + transition.updateFrameAsPositionAndBounds(layer: self.videoViewContainer.layer, frame: bounds) + transition.updateFrameAsPositionAndBounds(layer: self.backdropVideoViewContainer.layer, frame: bounds) let orientation = self.videoView.getOrientation() var aspect = self.videoView.getAspect() @@ -194,9 +202,6 @@ final class GroupVideoNode: ASDisplayNode { let transformScale: CGFloat = rotatedVideoFrame.width / videoSize.width transition.updateTransformScale(layer: self.videoViewContainer.layer, scale: transformScale) - let transition: ContainedViewLayoutTransition = .immediate - transition.updateTransformRotation(view: self.videoView.view, angle: angle) - if let backdropVideoView = self.backdropVideoView { rotatedVideoSize = filledSize var rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((size.width - rotatedVideoSize.width) / 2.0), y: floor((size.height - rotatedVideoSize.height) / 2.0)), size: rotatedVideoSize) @@ -217,11 +222,16 @@ final class GroupVideoNode: ASDisplayNode { } if let backdropEffectView = self.backdropEffectView { - transition.updateFrame(view: backdropEffectView, frame: self.bounds) + let maxSide = max(bounds.width, bounds.height) + let squareBounds = CGRect(x: (bounds.width - maxSide) / 2.0, y: (bounds.width - maxSide) / 2.0, width: maxSide, height: maxSide) + transition.updateFrame(view: backdropEffectView, frame: squareBounds) } + let transition: ContainedViewLayoutTransition = .immediate + transition.updateTransformRotation(view: self.videoView.view, angle: angle) + if let effectView = self.effectView { - transition.updateFrame(view: effectView, frame: self.bounds) + transition.updateFrame(view: effectView, frame: bounds) } // TODO: properly fix the issue diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index 68358fce40..a9a7283452 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -657,7 +657,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall { self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - self.isVideoEnabled = accountContext.sharedContext.immediateExperimentalUISettings.demoVideoChats + self.isVideoEnabled = true self.hasVideo = false self.hasScreencast = false diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift index 26250f9077..d1a31df45f 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatActionButton.swift @@ -61,6 +61,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { private let containerNode: ASDisplayNode private let backgroundNode: VoiceChatActionButtonBackgroundNode private let iconNode: VoiceChatActionButtonIconNode + private let labelContainerNode: ASDisplayNode let titleLabel: ImmediateTextNode private let subtitleLabel: ImmediateTextNode private let buttonTitleLabel: ImmediateTextNode @@ -138,6 +139,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { self.backgroundNode = VoiceChatActionButtonBackgroundNode() self.iconNode = VoiceChatActionButtonIconNode(isColored: false) + self.labelContainerNode = ASDisplayNode() self.titleLabel = ImmediateTextNode() self.subtitleLabel = ImmediateTextNode() self.buttonTitleLabel = ImmediateTextNode() @@ -147,9 +149,10 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { super.init() self.addSubnode(self.bottomNode) - self.addSubnode(self.titleLabel) - self.addSubnode(self.subtitleLabel) - + self.labelContainerNode.addSubnode(self.titleLabel) + self.labelContainerNode.addSubnode(self.subtitleLabel) + self.addSubnode(self.labelContainerNode) + self.addSubnode(self.containerNode) self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.iconNode) @@ -242,6 +245,8 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { let subtitleSize = self.subtitleLabel.updateLayout(CGSize(width: size.width, height: .greatestFiniteMagnitude)) let totalHeight = titleSize.height + subtitleSize.height + 1.0 + self.labelContainerNode.frame = CGRect(origin: CGPoint(), size: size) + self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: floor((size.height - totalHeight) / 2.0) + 84.0), size: titleSize) self.subtitleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - subtitleSize.width) / 2.0), y: self.titleLabel.frame.maxY + 1.0), size: subtitleSize) @@ -272,17 +277,19 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode { transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 0.0) } else { - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate if small { - transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.05) - transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.05) + transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? smallScale * 0.9 : smallScale, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? smallIconScale * 0.9 : smallIconScale, delay: 0.0) transition.updateAlpha(node: self.titleLabel, alpha: 0.0) transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint(x: 0.0, y: -40.0)) } else { - transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.05) - transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.05) + transition.updateTransformScale(node: self.backgroundNode, scale: 1.0, delay: 0.0) + transition.updateTransformScale(node: self.iconNode, scale: self.pressing ? 0.9 : 1.0, delay: 0.0) transition.updateAlpha(node: self.titleLabel, alpha: 1.0, delay: 0.05) transition.updateAlpha(node: self.subtitleLabel, alpha: 1.0, delay: 0.05) + transition.updateSublayerTransformOffset(layer: self.labelContainerNode.layer, offset: CGPoint()) } transition.updateAlpha(layer: self.backgroundNode.maskProgressLayer, alpha: 1.0) } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift index c08aea26d4..7e6530d813 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatCameraPreviewController.swift @@ -23,12 +23,12 @@ final class VoiceChatCameraPreviewController: ViewController { private var animatedIn = false private let cameraNode: GroupVideoNode - private let shareCamera: (ASDisplayNode) -> Void + private let shareCamera: (ASDisplayNode, Bool) -> Void private let switchCamera: () -> Void private var presentationDataDisposable: Disposable? - init(context: AccountContext, cameraNode: GroupVideoNode, shareCamera: @escaping (ASDisplayNode) -> Void, switchCamera: @escaping () -> Void) { + init(context: AccountContext, cameraNode: GroupVideoNode, shareCamera: @escaping (ASDisplayNode, Bool) -> Void, switchCamera: @escaping () -> Void) { self.context = context self.cameraNode = cameraNode self.shareCamera = shareCamera @@ -60,9 +60,9 @@ final class VoiceChatCameraPreviewController: ViewController { override public func loadDisplayNode() { self.displayNode = VoiceChatCameraPreviewControllerNode(controller: self, context: self.context, cameraNode: self.cameraNode) - self.controllerNode.shareCamera = { [weak self] in + self.controllerNode.shareCamera = { [weak self] unmuted in if let strongSelf = self { - strongSelf.shareCamera(strongSelf.cameraNode) + strongSelf.shareCamera(strongSelf.cameraNode, unmuted) strongSelf.dismiss() } } @@ -121,6 +121,10 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U private var broadcastPickerView: UIView? private let cancelButton: SolidRoundedButtonNode + private let microphoneButton: HighlightTrackingButtonNode + private let microphoneEffectView: UIVisualEffectView + private let microphoneIconNode: VoiceChatMicrophoneNode + private let switchCameraButton: HighlightTrackingButtonNode private let switchCameraEffectView: UIVisualEffectView private let switchCameraIconNode: ASImageNode @@ -129,7 +133,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U private var applicationStateDisposable: Disposable? - var shareCamera: (() -> Void)? + var shareCamera: ((Bool) -> Void)? var switchCamera: (() -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? @@ -196,6 +200,16 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.previewContainerNode.cornerRadius = 11.0 self.previewContainerNode.backgroundColor = .black + self.microphoneButton = HighlightTrackingButtonNode() + self.microphoneButton.isSelected = true + self.microphoneEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + self.microphoneEffectView.clipsToBounds = true + self.microphoneEffectView.layer.cornerRadius = 24.0 + self.microphoneEffectView.isUserInteractionEnabled = false + + self.microphoneIconNode = VoiceChatMicrophoneNode() + self.microphoneIconNode.update(state: .init(muted: false, filled: true, color: .white), animated: false) + self.switchCameraButton = HighlightTrackingButtonNode() self.switchCameraEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) self.switchCameraEffectView.clipsToBounds = true @@ -234,13 +248,16 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.contentContainerNode.addSubnode(self.previewContainerNode) self.previewContainerNode.addSubnode(self.cameraNode) + self.previewContainerNode.addSubnode(self.microphoneButton) + self.microphoneButton.view.addSubview(self.microphoneEffectView) + self.microphoneButton.addSubnode(self.microphoneIconNode) self.previewContainerNode.addSubnode(self.switchCameraButton) self.switchCameraButton.view.addSubview(self.switchCameraEffectView) self.switchCameraButton.addSubnode(self.switchCameraIconNode) self.cameraButton.pressed = { [weak self] in if let strongSelf = self { - strongSelf.shareCamera?() + strongSelf.shareCamera?(strongSelf.microphoneButton.isSelected) } } self.cancelButton.pressed = { [weak self] in @@ -249,6 +266,19 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U } } + self.microphoneButton.addTarget(self, action: #selector(self.microphonePressed), forControlEvents: .touchUpInside) + self.microphoneButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.microphoneButton, scale: 0.9) + } else { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring) + transition.updateSublayerTransformScale(node: strongSelf.microphoneButton, scale: 1.0) + } + } + } + self.switchCameraButton.addTarget(self, action: #selector(self.switchCameraPressed), forControlEvents: .touchUpInside) self.switchCameraButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -263,6 +293,11 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U } } + @objc private func microphonePressed() { + self.microphoneButton.isSelected = !self.microphoneButton.isSelected + self.microphoneIconNode.update(state: .init(muted: !self.microphoneButton.isSelected, filled: true, color: .white), animated: true) + } + @objc private func switchCameraPressed() { self.switchCamera?() @@ -403,6 +438,12 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U self.cameraNode.frame = CGRect(origin: CGPoint(), size: previewSize) self.cameraNode.updateLayout(size: previewSize, isLandscape: false, transition: .immediate) + let microphoneFrame = CGRect(x: 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0) + transition.updateFrame(node: self.microphoneButton, frame: microphoneFrame) + transition.updateFrame(view: self.microphoneEffectView, frame: CGRect(origin: CGPoint(), size: microphoneFrame.size)) + transition.updateFrameAsPositionAndBounds(node: self.microphoneIconNode, frame: CGRect(origin: CGPoint(x: 1.0, y: 0.0), size: microphoneFrame.size).insetBy(dx: 6.0, dy: 6.0)) + self.microphoneIconNode.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) + let switchCameraFrame = CGRect(x: previewSize.width - 48.0 - 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0) transition.updateFrame(node: self.switchCameraButton, frame: switchCameraFrame) transition.updateFrame(view: self.switchCameraEffectView, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift index 6cca015711..87f385839a 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatController.swift @@ -31,6 +31,7 @@ import LegacyMediaPickerUI import WebSearchUI import MapResourceToAvatarSizes import SolidRoundedButtonNode +import AudioBlob let panelBackgroundColor = UIColor(rgb: 0x1c1c1e) let secondaryPanelBackgroundColor = UIColor(rgb: 0x2c2c2e) @@ -38,7 +39,7 @@ let fullscreenBackgroundColor = UIColor(rgb: 0x000000) private let smallButtonSize = CGSize(width: 36.0, height: 36.0) private let sideButtonSize = CGSize(width: 56.0, height: 56.0) private let topPanelHeight: CGFloat = 63.0 -private let bottomAreaHeight: CGFloat = 205.0 +let bottomAreaHeight: CGFloat = 206.0 private let fullscreenBottomAreaHeight: CGFloat = 80.0 private let bottomGradientHeight: CGFloat = 70.0 @@ -82,7 +83,7 @@ private func decorationBottomGradientImage(dark: Bool) -> UIImage? { } public final class VoiceChatController: ViewController { - private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private struct ListTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] @@ -97,7 +98,7 @@ public final class VoiceChatController: ViewController { private final class Interaction { let updateIsMuted: (PeerId, Bool) -> Void - let pinPeer: (PeerId) -> Void + let switchToPeer: (PeerId, String?, Bool) -> Void let togglePeerVideo: (PeerId) -> Void let openInvite: () -> Void let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void @@ -110,14 +111,14 @@ public final class VoiceChatController: ViewController { init( updateIsMuted: @escaping (PeerId, Bool) -> Void, - pinPeer: @escaping (PeerId) -> Void, + switchToPeer: @escaping (PeerId, String?, Bool) -> Void, togglePeerVideo: @escaping (PeerId) -> Void, openInvite: @escaping () -> Void, peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void, getPeerVideo: @escaping (String, Bool) -> GroupVideoNode? ) { self.updateIsMuted = updateIsMuted - self.pinPeer = pinPeer + self.switchToPeer = switchToPeer self.togglePeerVideo = togglePeerVideo self.openInvite = openInvite self.peerContextAction = peerContextAction @@ -161,7 +162,7 @@ public final class VoiceChatController: ViewController { } } - private struct PeerEntry: Comparable, Identifiable { + fileprivate struct PeerEntry: Comparable, Identifiable { enum State { case listening case speaking @@ -372,6 +373,85 @@ public final class VoiceChatController: ViewController { } } + func tileItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction, videoEndpointId: String) -> VoiceChatTileItem? { + guard case let .peer(peerEntry) = self else { + return nil + } + let peer = peerEntry.peer + + let icon: VoiceChatTileItem.Icon + var text: VoiceChatParticipantItem.ParticipantText + var speaking = false + + var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() + let yourText: String + if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio + } else if peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhoto + } else if (peerEntry.about?.isEmpty ?? true) { + yourText = presentationData.strings.VoiceChat_TapToAddBio + } else { + yourText = presentationData.strings.VoiceChat_You + } + + var state = peerEntry.state + if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { + state = .listening + } + switch state { + case .listening: + if peerEntry.isMyPeer { + text = .text(yourText, textIcon, .accent) + } else if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else if let about = peerEntry.about, !about.isEmpty { + text = .text(about, textIcon, .generic) + } else { + text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) + } + if let muteState = peerEntry.muteState, muteState.mutedByYou { + icon = .microphone(true) + } else { + icon = .microphone(peerEntry.muteState != nil) + } + case .speaking: + if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + icon = .microphone(true) + } else { + if peerEntry.volume != nil { + textIcon.insert(.volume) + } + let volumeValue = peerEntry.volume.flatMap { $0 / 100 } + if let volume = volumeValue, volume != 100 { + text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive) + } else { + text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) + } + icon = .microphone(false) + speaking = true + } + case .raisedHand, .invited: + text = .none + icon = .none + } + + if let about = peerEntry.about, !about.isEmpty { + text = .text(about, textIcon, .generic) + } + + return VoiceChatTileItem(peer: peerEntry.peer, videoEndpointId: videoEndpointId, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, speaking: speaking, icon: icon, text: text, action: { + interaction.switchToPeer(peer.id, videoEndpointId, true) + }, contextAction: { node, gesture in + interaction.peerContextAction(peerEntry, node, gesture) + }, getVideo: { + return interaction.getPeerVideo(videoEndpointId, false) + }, getAudioLevel: { + return interaction.getAudioLevel(peerEntry.peer.id) + }) + } + func fullscreenItem(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem { switch self { case .tiles: @@ -382,16 +462,38 @@ public final class VoiceChatController: ViewController { interaction.openInvite() }) case let .peer(peerEntry): + let peer = peerEntry.peer var color: VoiceChatFullscreenParticipantItem.Color = .generic let icon: VoiceChatFullscreenParticipantItem.Icon + var text: VoiceChatParticipantItem.ParticipantText + var textIcon = VoiceChatParticipantItem.ParticipantText.TextIcon() + let yourText: String + if (peerEntry.about?.isEmpty ?? true) && peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhotoOrBio + } else if peer.smallProfileImage == nil { + yourText = presentationData.strings.VoiceChat_TapToAddPhoto + } else if (peerEntry.about?.isEmpty ?? true) { + yourText = presentationData.strings.VoiceChat_TapToAddBio + } else { + yourText = presentationData.strings.VoiceChat_You + } + var state = peerEntry.state if let muteState = peerEntry.muteState, case .speaking = state, muteState.mutedByYou || !muteState.canUnmute { state = .listening } - switch state { case .listening: + if peerEntry.isMyPeer { + text = .text(yourText, textIcon, .accent) + } else if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) + } else if let about = peerEntry.about, !about.isEmpty { + text = .text(about, textIcon, .generic) + } else { + text = .text(presentationData.strings.VoiceChat_StatusListening, textIcon, .generic) + } if let muteState = peerEntry.muteState, muteState.mutedByYou { color = .destructive icon = .microphone(true, UIColor(rgb: 0xff3b30)) @@ -400,28 +502,44 @@ public final class VoiceChatController: ViewController { } case .speaking: if let muteState = peerEntry.muteState, muteState.mutedByYou { + text = .text(presentationData.strings.VoiceChat_StatusMutedForYou, textIcon, .destructive) color = .destructive icon = .microphone(true, UIColor(rgb: 0xff3b30)) } else { - icon = .microphone(false, UIColor(rgb: 0x34c759)) + if peerEntry.volume != nil { + textIcon.insert(.volume) + } + let volumeValue = peerEntry.volume.flatMap { $0 / 100 } + if let volume = volumeValue, volume != 100 { + text = .text( presentationData.strings.VoiceChat_StatusSpeakingVolume("\(volume)%").0, textIcon, .constructive) + } else { + text = .text(presentationData.strings.VoiceChat_StatusSpeaking, textIcon, .constructive) + } + icon = .microphone(false, UIColor.white) } case .raisedHand: + text = .none color = .accent icon = .wantsToSpeak case .invited: + text = .none icon = .none } - return VoiceChatFullscreenParticipantItem(presentationData: ItemListPresentationData(presentationData), nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peerEntry.peer, icon: icon, color: color, isLandscape: peerEntry.isLandscape, active: peerEntry.active, getAudioLevel: { return interaction.getAudioLevel(peerEntry.peer.id) }, getVideo: { + if let about = peerEntry.about, !about.isEmpty { + 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: { if let endpointId = peerEntry.effectiveVideoEndpointId { return interaction.getPeerVideo(endpointId, true) } else { return nil } }, action: { _ in - interaction.pinPeer(peerEntry.peer.id) + interaction.switchToPeer(peerEntry.peer.id, nil, false) }, contextAction: { node, gesture in -// interaction.peerContextAction(peerEntry, node, gesture) + interaction.peerContextAction(peerEntry, node, gesture) }, getUpdatingAvatar: { return interaction.updateAvatarPromise.get() }) @@ -562,14 +680,12 @@ public final class VoiceChatController: ViewController { private let dimNode: ASDisplayNode private let contentContainer: ASDisplayNode private let backgroundNode: ASDisplayNode - private var toggleFullscreenButton: HighlightTrackingButtonNode private let listNode: ListView private let fullscreenListNode: ListView private let topPanelNode: ASDisplayNode private let topPanelEdgeNode: ASDisplayNode private let topPanelBackgroundNode: ASDisplayNode private let optionsButton: VoiceChatHeaderButton - private var optionsButtonIsAvatar = false private let closeButton: VoiceChatHeaderButton private let topCornersNode: ASImageNode private let bottomPanelCoverNode: ASDisplayNode @@ -584,8 +700,8 @@ public final class VoiceChatController: ViewController { fileprivate let actionButton: VoiceChatActionButton private let leftBorderNode: ASDisplayNode private let rightBorderNode: ASDisplayNode - private let mainVideoNode: VoiceChatMainVideoContainerNode - private let mainVideoContainerNode: ASDisplayNode + private let mainStageNode: VoiceChatMainStageContainerNode + private let mainStageContainerNode: ASDisplayNode private let transitionContainerNode: ASDisplayNode private var isScheduling = false @@ -628,7 +744,6 @@ public final class VoiceChatController: ViewController { private var switchedToCameraPeers = Set() private var currentEntries: [ListEntry] = [] private var currentFullscreenEntries: [ListEntry] = [] - private var videoNodesOrder: [String] = [] private var peerViewDisposable: Disposable? private let leaveDisposable = MetaDisposable() @@ -685,13 +800,15 @@ public final class VoiceChatController: ViewController { private var videoNodes: [String: GroupVideoNode] = [:] private var readyVideoNodes = Set() private var readyVideoDisposables = DisposableDict() + private var wideVideoNodes = Set() + private var videoNodesOrder: [String] = [] private var endpointToPeerId: [String: PeerId] = [:] private var peerIdToEndpoint: [PeerId: String] = [:] - private var currentDominantSpeakerWithVideo: PeerId? - private var currentForcedSpeakerWithVideo: PeerId? - private var effectiveSpeakerWithVideo: (PeerId, String)? + private var currentDominantSpeaker: PeerId? + private var currentForcedSpeaker: PeerId? + private var effectiveSpeaker: (PeerId, String?)? private var updateAvatarDisposable = MetaDisposable() private let updateAvatarPromise = Promise<(TelegramMediaImageRepresentation, Float)?>(nil) @@ -758,9 +875,6 @@ public final class VoiceChatController: ViewController { self.backgroundNode.backgroundColor = self.isScheduling ? panelBackgroundColor : secondaryPanelBackgroundColor self.backgroundNode.clipsToBounds = false - self.toggleFullscreenButton = HighlightTrackingButtonNode() - self.toggleFullscreenButton.alpha = 0.65 - self.listNode = ListView() self.listNode.alpha = self.isScheduling ? 0.0 : 1.0 self.listNode.isUserInteractionEnabled = !self.isScheduling @@ -854,12 +968,12 @@ public final class VoiceChatController: ViewController { self.rightBorderNode.isUserInteractionEnabled = false self.rightBorderNode.clipsToBounds = false - self.mainVideoNode = VoiceChatMainVideoContainerNode(context: self.context, call: self.call) + self.mainStageNode = VoiceChatMainStageContainerNode(context: self.context, call: self.call) - self.mainVideoContainerNode = ASDisplayNode() - self.mainVideoContainerNode.clipsToBounds = true - self.mainVideoContainerNode.isUserInteractionEnabled = false - self.mainVideoContainerNode.isHidden = true + self.mainStageContainerNode = ASDisplayNode() + self.mainStageContainerNode.clipsToBounds = true + self.mainStageContainerNode.isUserInteractionEnabled = false + self.mainStageContainerNode.isHidden = true self.transitionContainerNode = ASDisplayNode() self.transitionContainerNode.clipsToBounds = true @@ -914,27 +1028,26 @@ public final class VoiceChatController: ViewController { self.itemInteraction = Interaction(updateIsMuted: { [weak self] peerId, isMuted in let _ = self?.call.updateMuteState(peerId: peerId, isMuted: isMuted) - }, pinPeer: { [weak self] peerId in + }, switchToPeer: { [weak self] peerId, videoEndpointId, expand in if let strongSelf = self { - if peerId != strongSelf.currentDominantSpeakerWithVideo { - strongSelf.currentDominantSpeakerWithVideo = peerId + if expand, let videoEndpointId = videoEndpointId { + strongSelf.currentDominantSpeaker = peerId + strongSelf.effectiveSpeaker = (peerId, videoEndpointId) + strongSelf.updateDisplayMode(.fullscreen(controlsHidden: false)) + } else { + strongSelf.currentForcedSpeaker = nil + if peerId != strongSelf.currentDominantSpeaker { + strongSelf.currentDominantSpeaker = peerId + } + strongSelf.updateMainVideo(waitForFullSize: false, updateMembers: true, force: true) } - strongSelf.updateMainVideo(waitForFullSize: false, updateMembers: true, force: true) } }, togglePeerVideo: { [weak self] peerId in guard let strongSelf = self else { return } if let strongSelf = self { - if peerId != strongSelf.currentForcedSpeakerWithVideo { - strongSelf.currentForcedSpeakerWithVideo = peerId - } - for entry in strongSelf.currentEntries { - if case let .peer(peerEntry) = entry, peerEntry.peer.id == peerId, let endpoint = peerEntry.effectiveVideoEndpointId { - strongSelf.effectiveSpeakerWithVideo = (peerId, endpoint) - strongSelf.mainVideoNode.updatePeer(peer: strongSelf.effectiveSpeakerWithVideo, waitForFullSize: false) - } - } + } }, openInvite: { [weak self] in guard let strongSelf = self else { @@ -1537,18 +1650,16 @@ public final class VoiceChatController: ViewController { self.contentContainer.addSubnode(self.bottomGradientNode) self.contentContainer.addSubnode(self.bottomPanelBackgroundNode) self.contentContainer.addSubnode(self.bottomPanelNode) - self.contentContainer.addSubnode(self.mainVideoContainerNode) + self.contentContainer.addSubnode(self.mainStageContainerNode) self.contentContainer.addSubnode(self.timerNode) self.contentContainer.addSubnode(self.scheduleTextNode) self.contentContainer.addSubnode(self.fullscreenListNode) self.addSubnode(self.transitionContainerNode) - self.mainVideoContainerNode.addSubnode(self.mainVideoNode) + self.mainStageContainerNode.addSubnode(self.mainStageNode) self.updateDecorationsColors() - - self.toggleFullscreenButton.addTarget(self, action: #selector(self.toggleFullscreenPressed), forControlEvents: .touchUpInside) - + let invitedPeers: Signal<[Peer], NoError> = self.call.invitedPeers |> mapToSignal { ids -> Signal<[Peer], NoError> in return context.account.postbox.transaction { transaction -> [Peer] in @@ -1605,25 +1716,16 @@ public final class VoiceChatController: ViewController { strongSelf.currentSubtitle = subtitle if strongSelf.isScheduling { - strongSelf.optionsButtonIsAvatar = false strongSelf.optionsButton.isUserInteractionEnabled = false strongSelf.optionsButton.alpha = 0.0 strongSelf.closeButton.isUserInteractionEnabled = false strongSelf.closeButton.alpha = 0.0 } else if let callState = strongSelf.callState, callState.canManageCall { - strongSelf.optionsButtonIsAvatar = false strongSelf.optionsButton.isUserInteractionEnabled = true } else if displayAsPeers.count > 1 { - strongSelf.optionsButtonIsAvatar = true - for peer in displayAsPeers { - if peer.peer.id == state.myPeerId { - strongSelf.optionsButton.setContent(.avatar(peer.peer)) - } - } strongSelf.optionsButton.isUserInteractionEnabled = true } else { - strongSelf.optionsButtonIsAvatar = false - strongSelf.optionsButton.isUserInteractionEnabled = false + strongSelf.optionsButton.isUserInteractionEnabled = true } if let (layout, navigationHeight) = strongSelf.validLayout { @@ -1652,9 +1754,7 @@ public final class VoiceChatController: ViewController { } if !strongSelf.didSetDataReady { strongSelf.didSetDataReady = true - - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) - + strongSelf.updateMembers() strongSelf.controller?.dataReady.set(true) } }) @@ -1699,7 +1799,7 @@ public final class VoiceChatController: ViewController { } if maxLevelWithVideo == nil { - if let peerId = strongSelf.currentDominantSpeakerWithVideo { + if let peerId = strongSelf.currentDominantSpeaker { maxLevelWithVideo = (peerId, 0.0) } else if strongSelf.peerIdToEndpoint.count > 0 { for entry in strongSelf.currentEntries { @@ -1714,8 +1814,8 @@ public final class VoiceChatController: ViewController { } if let (peerId, _) = maxLevelWithVideo { - strongSelf.currentDominantSpeakerWithVideo = peerId -// strongSelf.updateMainStageVideo(waitForFullSize: false) + strongSelf.currentDominantSpeaker = peerId + strongSelf.updateMainVideo(waitForFullSize: false) } strongSelf.itemInteraction?.updateAudioLevels(levels) @@ -1829,24 +1929,48 @@ public final class VoiceChatController: ViewController { strongSelf.dismissScheduled() } } - - self.toggleFullscreenButton.highligthedChanged = { [weak self] highlighted in + + self.mainStageNode.tapped = { [weak self] in if let strongSelf = self { - if highlighted { - strongSelf.toggleFullscreenButton.layer.removeAnimation(forKey: "opacity") - strongSelf.toggleFullscreenButton.alpha = 0.26 - } else { - strongSelf.toggleFullscreenButton.alpha = 0.65 - strongSelf.toggleFullscreenButton.layer.animateAlpha(from: 0.26, to: 0.65, duration: 0.2) + let effectiveDisplayMode = strongSelf.displayMode + let nextDisplayMode: DisplayMode + switch effectiveDisplayMode { + case .modal: + nextDisplayMode = effectiveDisplayMode + case let .fullscreen(controlsHidden): + if controlsHidden { + nextDisplayMode = .fullscreen(controlsHidden: false) + } else { + nextDisplayMode = .fullscreen(controlsHidden: true) + } } + strongSelf.updateDisplayMode(nextDisplayMode) } } - self.mainVideoNode.tapped = { [weak self] in + self.mainStageNode.back = { [weak self] in if let strongSelf = self { - strongSelf.toggleDisplayMode() + strongSelf.currentForcedSpeaker = nil + strongSelf.updateDisplayMode(.modal(isExpanded: true, isFilled: true)) + strongSelf.effectiveSpeaker = nil } } + + self.mainStageNode.togglePin = { [weak self] in + if let strongSelf = self { + if let peerId = strongSelf.currentForcedSpeaker { + strongSelf.currentDominantSpeaker = peerId + strongSelf.currentForcedSpeaker = nil + } else { + strongSelf.currentForcedSpeaker = strongSelf.effectiveSpeaker?.0 + } + strongSelf.updateMembers() + } + } + + self.mainStageNode.getAudioLevel = { [weak self] peerId in + return self?.itemInteraction?.getAudioLevel(peerId) ?? .single(0.0) + } } deinit { @@ -1869,22 +1993,8 @@ public final class VoiceChatController: ViewController { self.readyVideoDisposables.dispose() } - @objc private func toggleFullscreenPressed() { - if case .modal = self.displayMode { - } else { - self.displayMode = .fullscreen(controlsHidden: true) - } - - } - private func openSettingsMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { - let canManageCall = !self.optionsButtonIsAvatar - let items: Signal<[ContextMenuItem], NoError> - if canManageCall { - items = self.contextMenuMainItems() - } else { - items = self.contextMenuDisplayAsItems() - } + let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems() if let controller = self.controller { let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: self.darkTheme), source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceNode: self.optionsButton.referenceNode)), items: items, reactionItems: [], gesture: gesture) controller.presentInGlobalOverlay(contextController) @@ -1896,6 +2006,7 @@ public final class VoiceChatController: ViewController { return .single([]) } + let canManageCall = self.callState?.canManageCall == true let avatarSize = CGSize(width: 28.0, height: 28.0) return combineLatest(self.displayAsPeersPromise.get(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.inviteLinksPromise.get()) |> take(1) @@ -1923,34 +2034,36 @@ public final class VoiceChatController: ViewController { } } - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) - }, action: { _, f in - f(.default) + if canManageCall { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditTitle, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) - guard let strongSelf = self else { - return - } - strongSelf.openTitleEditing() - }))) - - var hasPermissions = true - if let chatPeer = chatPeer as? TelegramChannel { - if case .broadcast = chatPeer.info { - hasPermissions = false - } else if chatPeer.flags.contains(.isGigagroup) { - hasPermissions = false - } - } - if hasPermissions { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) - }, action: { c, _ in guard let strongSelf = self else { return } - c.setItems(strongSelf.contextMenuPermissionItems()) + strongSelf.openTitleEditing() }))) + + var hasPermissions = true + if let chatPeer = chatPeer as? TelegramChannel { + if case .broadcast = chatPeer.info { + hasPermissions = false + } else if chatPeer.flags.contains(.isGigagroup) { + hasPermissions = false + } + } + if hasPermissions { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_EditPermissions, icon: { theme -> UIImage? in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.actionSheet.primaryTextColor) + }, action: { c, _ in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuPermissionItems()) + }))) + } } if let inviteLinks = inviteLinks { @@ -1963,56 +2076,58 @@ public final class VoiceChatController: ViewController { }))) } - if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { - items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in - f(.dismissWithoutContent) - - guard let strongSelf = self else { - return - } - - let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { - if let strongSelf = self { - strongSelf.call.setShouldBeRecording(false, title: nil) - - strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in - if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - let context = strongSelf.context - strongSelf.controller?.dismiss(completion: { - Queue.mainQueue().justDispatch { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) - } - }) - - return true - } - return false - }) - } - })]) - self?.controller?.present(alertController, in: .window(.root)) - }), false)) - } else { - if strongSelf.callState?.scheduleTimestamp == nil { - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in - return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) - }, action: { _, f in + if canManageCall { + if let recordingStartTimestamp = strongSelf.callState?.recordingStartTimestamp { + items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { _, f in f(.dismissWithoutContent) guard let strongSelf = self else { return } - let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in - if let strongSelf = self, let title = title { - strongSelf.call.setShouldBeRecording(true, title: title) + let alertController = textAlertController(context: strongSelf.context, forceTheme: strongSelf.darkTheme, title: nil, text: strongSelf.presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.VoiceChat_StopRecordingStop, action: { + if let strongSelf = self { + strongSelf.call.setShouldBeRecording(false, title: nil) - strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) - strongSelf.call.playTone(.recordingStarted) + strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: strongSelf.presentationData.strings.VoiceChat_RecordingSaved), action: { [weak self] value in + if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + let context = strongSelf.context + strongSelf.controller?.dismiss(completion: { + Queue.mainQueue().justDispatch { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil)) + } + }) + + return true + } + return false + }) } - }) - self?.controller?.present(controller, in: .window(.root)) - }))) + })]) + self?.controller?.present(alertController, in: .window(.root)) + }), false)) + } else { + if strongSelf.callState?.scheduleTimestamp == nil { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_StartRecording, icon: { theme -> UIImage? in + return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let controller = voiceChatTitleEditController(sharedContext: strongSelf.context.sharedContext, account: strongSelf.context.account, forceTheme: strongSelf.darkTheme, title: presentationData.strings.VoiceChat_StartRecordingTitle, text: presentationData.strings.VoiceChat_StartRecordingText, placeholder: presentationData.strings.VoiceChat_RecordingTitlePlaceholder, value: nil, maxLength: 40, apply: { title in + if let strongSelf = self, let title = title { + strongSelf.call.setShouldBeRecording(true, title: title) + + strongSelf.presentUndoOverlay(content: .voiceChatRecording(text: strongSelf.presentationData.strings.VoiceChat_RecordingStarted), action: { _ in return false }) + strongSelf.call.playTone(.recordingStarted) + } + }) + self?.controller?.present(controller, in: .window(.root)) + }))) + } } } @@ -2028,7 +2143,7 @@ public final class VoiceChatController: ViewController { strongSelf.call.setIsNoiseSuppressionEnabled(!strongSelf.isNoiseSuppressionEnabled) })))*/ - if let callState = strongSelf.callState, callState.canManageCall { + if canManageCall { let isScheduled = strongSelf.isScheduled items.append(.action(ContextMenuActionItem(text: isScheduled ? strongSelf.presentationData.strings.VoiceChat_CancelVoiceChat : strongSelf.presentationData.strings.VoiceChat_EndVoiceChat, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) @@ -2057,6 +2172,23 @@ public final class VoiceChatController: ViewController { })]) strongSelf.controller?.present(alertController, in: .window(.root)) }))) + } else { + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_LeaveVoiceChat, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let _ = (strongSelf.call.leave(terminateIfPossible: false) + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(completed: { + self?.controller?.dismiss() + }) + }))) } return items } @@ -2068,7 +2200,6 @@ public final class VoiceChatController: ViewController { } let avatarSize = CGSize(width: 28.0, height: 28.0) - let canManageCall = !self.optionsButtonIsAvatar let darkTheme = self.darkTheme return self.displayAsPeersPromise.get() @@ -2146,17 +2277,15 @@ public final class VoiceChatController: ViewController { items.append(.separator) } } - if canManageCall { - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, action: { (c, _) in - guard let strongSelf = self else { - return - } - c.setItems(strongSelf.contextMenuMainItems()) - }))) - } + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, action: { (c, _) in + guard let strongSelf = self else { + return + } + c.setItems(strongSelf.contextMenuMainItems()) + }))) return items } } @@ -2631,7 +2760,7 @@ public final class VoiceChatController: ViewController { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() } @objc private func actionButtonPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -2718,7 +2847,7 @@ public final class VoiceChatController: ViewController { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring)) } - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() default: break } @@ -2847,8 +2976,9 @@ public final class VoiceChatController: ViewController { return } let cameraNode = GroupVideoNode(videoView: view, backdropVideoView: nil) - let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode in + let controller = VoiceChatCameraPreviewController(context: strongSelf.context, cameraNode: cameraNode, shareCamera: { [weak self] videoNode, unmuted in if let strongSelf = self { + strongSelf.call.setIsMuted(action: unmuted ? .unmuted : .muted(isPushToTalkActive: false)) strongSelf.call.requestVideo() if let (layout, navigationHeight) = strongSelf.validLayout { @@ -2980,19 +3110,25 @@ public final class VoiceChatController: ViewController { transition.updateFrame(node: self.transitionContainerNode, frame: CGRect(x: sideInset, y: topEdgeY, width: layout.size.width - sideInset * 2.0, height: max(0.0, bottomEdgeY - topEdgeY))) var isFullscreen = false - if case .fullscreen = self.effectiveDisplayMode { + var bottomInset: CGFloat = 0.0 + var bottomEdgeInset: CGFloat = 0.0 + if case let .fullscreen(controlsHidden) = self.effectiveDisplayMode { isFullscreen = true + if !controlsHidden { + bottomInset = 80.0 + } + bottomEdgeInset = 154.0 } transition.updateAlpha(node: self.bottomGradientNode, alpha: isFullscreen ? 0.0 : 1.0) let videoTopEdgeY = isLandscape ? 0.0 : layoutTopInset let videoBottomEdgeY = self.isLandscape ? layout.size.height : layout.size.height - layout.intrinsicInsets.bottom - 84.0 let videoFrame = CGRect(x: 0.0, y: videoTopEdgeY, width: isLandscape ? layout.size.width - layout.safeInsets.right - 84.0 : layout.size.width, height: videoBottomEdgeY - videoTopEdgeY) - transition.updateFrame(node: self.mainVideoContainerNode, frame: videoFrame) - if !self.mainVideoNode.animating { - transition.updateFrame(node: self.mainVideoNode, frame: CGRect(origin: CGPoint(), size: videoFrame.size)) + transition.updateFrame(node: self.mainStageContainerNode, frame: videoFrame) + if !self.mainStageNode.animating { + transition.updateFrame(node: self.mainStageNode, frame: CGRect(origin: CGPoint(), size: videoFrame.size)) } - self.mainVideoNode.update(size: videoFrame.size, sideInset: 0.0, isLandscape: true, transition: transition) + self.mainStageNode.update(size: videoFrame.size, bottomInset: bottomInset, isLandscape: true, transition: transition) let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: size.width, height: layout.size.height)) @@ -3035,7 +3171,7 @@ public final class VoiceChatController: ViewController { let listMaxY = listTopInset + listSize.height let bottomOffset: CGFloat = min(0.0, bottomEdge - listMaxY) + layout.size.height - bottomPanelHeight - let bottomDelta = self.effectiveBottomAreaHeight - bottomAreaHeight + bottomGradientHeight + let bottomDelta = bottomGradientHeight - bottomEdgeInset let bottomCornersFrame = CGRect(origin: CGPoint(x: sideInset + floorToScreenPixels((size.width - contentWidth) / 2.0), y: -50.0 + bottomOffset + bottomDelta), size: CGSize(width: contentWidth - sideInset * 2.0, height: 50.0)) let previousBottomCornersFrame = self.bottomCornersNode.frame @@ -3143,9 +3279,7 @@ public final class VoiceChatController: ViewController { self.closeButton.setContent(.image(closeButtonImage(dark: isFullscreen)), animated: transition.isAnimated) } - if !self.optionsButtonIsAvatar { - self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) - } + self.optionsButton.setContent(.more(optionsCircleImage(dark: isFullscreen)), animated: transition.isAnimated) self.updateTitle(transition: transition) } @@ -3325,7 +3459,7 @@ public final class VoiceChatController: ViewController { if previousIsLandscape != isLandscape { self.updateDecorationsColors() self.updateDecorationsLayout(transition: transition) - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() } // // if let videoIndex = self.contentContainer.subnodes?.firstIndex(where: { $0 === self.mainStageVideoClippingNode }), let listIndex = self.contentContainer.subnodes?.firstIndex(where: { $0 === self.listNode }) { @@ -3518,9 +3652,7 @@ public final class VoiceChatController: ViewController { forthButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - sideButtonSize.width, y: y), size: sideButtonSize) } } - - self.toggleFullscreenButton.setImage(generateTintedImage(image: smallButtons ? UIImage(bundleImageName: "Media Gallery/Minimize") : UIImage(bundleImageName: "Media Gallery/Fullscreen"), color: .white), for: .normal) - + let actionButtonState: VoiceChatActionButton.State let actionButtonTitle: String let actionButtonSubtitle: String @@ -3623,7 +3755,15 @@ public final class VoiceChatController: ViewController { } : nil) } - self.updateButtons(transition: !isFirstTime ? .animated(duration: 0.3, curve: .linear) : .immediate) + var buttonsTransition: ContainedViewLayoutTransition = .immediate + if !isFirstTime { + if case .animated(_, .spring) = transition { + buttonsTransition = transition + } else { + buttonsTransition = .animated(duration: 0.3, curve: .linear) + } + } + self.updateButtons(transition: buttonsTransition) if self.audioButton.supernode === self.bottomPanelNode { transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame) @@ -3825,6 +3965,10 @@ public final class VoiceChatController: ViewController { }) } + private func updateMembers() { + self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + } + private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, callMembers: ([GroupCallParticipantsContext.Participant], String?), invitedPeers: [Peer], speakingPeers: Set, updatePinnedPeer: Bool = true) { var disableAnimation = false if self.currentCallMembers?.1 != callMembers.1 { @@ -3846,8 +3990,7 @@ public final class VoiceChatController: ViewController { var requestedVideoChannels: [PresentationGroupCallRequestedVideo] = [] var tileItems: [VoiceChatTileItem] = [] var tileMap: [String: VoiceChatTileItem] = [:] - - var fullscreenById: [PeerId: ListEntry] = [:] + var latestWideVideo: String? = nil for member in callMembers.0 { if processedPeerIds.contains(member.peer.id) { @@ -3872,8 +4015,7 @@ public final class VoiceChatController: ViewController { var displayedRaisedHands = strongSelf.displayedRaisedHands displayedRaisedHands.remove(member.peer.id) strongSelf.displayedRaisedHands = displayedRaisedHands - - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) + strongSelf.updateMembers() } }) } @@ -3911,7 +4053,7 @@ public final class VoiceChatController: ViewController { peerIdToEndpointId[member.peer.id] = anyEndpointId } - let entry: ListEntry = .peer(PeerEntry( + let peerEntry = PeerEntry( peer: memberPeer, about: member.about, isMyPeer: self.callState?.myPeerId == member.peer.id, @@ -3924,9 +4066,12 @@ public final class VoiceChatController: ViewController { volume: member.volume, raisedHand: member.hasRaiseHand, displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), - active: memberPeer.id == self.effectiveSpeakerWithVideo?.0, - isLandscape: false - )) + active: memberPeer.id == self.effectiveSpeaker?.0, + isLandscape: self.isLandscape + ) + if peerEntry.active { + self.mainStageNode.update(peerEntry: peerEntry, pinned: self.currentForcedSpeaker != nil) + } var isTile = false if let interaction = self.itemInteraction { @@ -3934,82 +4079,66 @@ public final class VoiceChatController: ViewController { if !self.videoNodesOrder.contains(videoEndpointId) { self.videoNodesOrder.append(videoEndpointId) } - isTile = true - tileMap[videoEndpointId] = VoiceChatTileItem(peer: member.peer, videoEndpointId: videoEndpointId, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: speakingPeers.contains(member.peer.id), icon: .microphone(true), action: { [weak self] in - if let strongSelf = self { - strongSelf.currentDominantSpeakerWithVideo = member.peer.id - strongSelf.effectiveSpeakerWithVideo = (member.peer.id, videoEndpointId) - strongSelf.toggleDisplayMode() - } - }, getVideo: { - return interaction.getPeerVideo(videoEndpointId, false) - }, getAudioLevel: { - return interaction.getAudioLevel(member.peer.id) - }) + if let tileItem = ListEntry.peer(peerEntry).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) { + isTile = true + tileMap[videoEndpointId] = tileItem + } + if self.wideVideoNodes.contains(videoEndpointId) { + latestWideVideo = videoEndpointId + } } if let videoEndpointId = member.videoEndpointId, self.readyVideoNodes.contains(videoEndpointId) { if !self.videoNodesOrder.contains(videoEndpointId) { self.videoNodesOrder.append(videoEndpointId) } - isTile = true - tileMap[videoEndpointId] = VoiceChatTileItem(peer: member.peer, videoEndpointId: videoEndpointId, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, speaking: speakingPeers.contains(member.peer.id), icon: .microphone(true), action: { [weak self] in - if let strongSelf = self { - strongSelf.currentDominantSpeakerWithVideo = member.peer.id - strongSelf.effectiveSpeakerWithVideo = (member.peer.id, videoEndpointId) - strongSelf.toggleDisplayMode() - } - }, getVideo: { - return interaction.getPeerVideo(videoEndpointId, false) - }, getAudioLevel: { - return interaction.getAudioLevel(member.peer.id) - }) + if let tileItem = ListEntry.peer(peerEntry).tileItem(context: self.context, presentationData: self.presentationData, interaction: interaction, videoEndpointId: videoEndpointId) { + isTile = true + tileMap[videoEndpointId] = tileItem + } + if self.wideVideoNodes.contains(videoEndpointId) { + latestWideVideo = videoEndpointId + } } } if !isTile { - entries.append(entry) + entries.append(.peer(peerEntry)) } + fullscreenEntries.append(.peer(peerEntry)) - let fullscreenEntry: ListEntry = .peer(PeerEntry( - peer: memberPeer, - about: member.about, - isMyPeer: self.callState?.myPeerId == member.peer.id, - videoEndpointId: member.videoEndpointId, - presentationEndpointId: member.presentationEndpointId, - activityTimestamp: Int32.max - 1 - index, - state: memberState, - muteState: memberMuteState, - canManageCall: self.callState?.canManageCall ?? false, - volume: member.volume, - raisedHand: member.hasRaiseHand, - displayRaisedHandStatus: self.displayedRaisedHands.contains(member.peer.id), - active: memberPeer.id == self.effectiveSpeakerWithVideo?.0, - isLandscape: self.isLandscape - )) - fullscreenEntries.append(fullscreenEntry) index += 1 if self.callState?.networkState == .connecting { } else { if var videoChannel = member.requestedVideoChannel(quality: .medium) { - if self.effectiveSpeakerWithVideo?.1 == videoChannel.endpointId { + if self.effectiveSpeaker?.1 == videoChannel.endpointId { videoChannel.quality = .full } - requestedVideoChannels.append(videoChannel) } if var presentationChannel = member.requestedPresentationVideoChannel(quality: .medium) { - if self.effectiveSpeakerWithVideo?.1 == presentationChannel.endpointId { + if self.effectiveSpeaker?.1 == presentationChannel.endpointId { presentationChannel.quality = .full } - requestedVideoChannels.append(presentationChannel) } } } - for tileEndpoint in self.videoNodesOrder { - if let tileItem = tileMap[tileEndpoint] { + var preList: [String] = [] + for tileVideoEndpoint in self.videoNodesOrder { + if let _ = tileMap[tileVideoEndpoint] { + preList.append(tileVideoEndpoint) + } + } + + if (tileMap.count % 2) != 0, let last = preList.last, !self.wideVideoNodes.contains(last), let latestWide = latestWideVideo { + self.videoNodesOrder.removeAll(where: { $0 == latestWide }) + self.videoNodesOrder.append(latestWide) + } + + for tileVideoEndpoint in self.videoNodesOrder { + if let tileItem = tileMap[tileVideoEndpoint] { tileItems.append(tileItem) } } @@ -4049,7 +4178,6 @@ public final class VoiceChatController: ViewController { self.endpointToPeerId = endpointIdToPeerId self.peerIdToEndpoint = peerIdToEndpointId - if !tileItems.isEmpty { entries.insert(.tiles(tileItems), at: 0) } @@ -4130,16 +4258,19 @@ public final class VoiceChatController: ViewController { strongSelf.readyVideoDisposables.set((videoNode.ready |> filter { $0 } |> take(1) - ).start(next: { [weak self] _ in - if let strongSelf = self { + ).start(next: { [weak self, weak videoNode] _ in + if let strongSelf = self, let videoNode = videoNode { strongSelf.readyVideoNodes.insert(channel.endpointId) - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) + if videoNode.aspectRatio > 1.25 { + strongSelf.wideVideoNodes.insert(channel.endpointId) + } + strongSelf.updateMembers() } }), forKey: channel.endpointId) strongSelf.videoNodes[channel.endpointId] = videoNode if let _ = strongSelf.validLayout { - strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, callMembers: strongSelf.currentCallMembers ?? ([], nil), invitedPeers: strongSelf.currentInvitedPeers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set()) + strongSelf.updateMembers() } } }) @@ -4180,17 +4311,16 @@ public final class VoiceChatController: ViewController { } } - private func updateMainVideo(waitForFullSize: Bool, currentEntries: [ListEntry]? = nil, updateMembers: Bool = true, force: Bool = false) { - let effectiveMainParticipant = self.currentForcedSpeakerWithVideo ?? self.currentDominantSpeakerWithVideo - guard effectiveMainParticipant != self.effectiveSpeakerWithVideo?.0 || force else { + private func updateMainVideo(waitForFullSize: Bool, updateMembers: Bool = true, force: Bool = false, completion: (() -> Void)? = nil) { + let effectiveMainSpeaker = self.currentForcedSpeaker ?? self.currentDominantSpeaker + guard effectiveMainSpeaker != self.effectiveSpeaker?.0 || force else { return } - let currentEntries = currentEntries ?? self.currentFullscreenEntries - - var effectivePeer: (PeerId, String, String?)? = nil - var anyPeer: (PeerId, String, String?)? = nil - if let peerId = effectiveMainParticipant { + let currentEntries = self.currentFullscreenEntries + var effectivePeer: (PeerId, String?, String?)? = nil + var anyPeer: (PeerId, String?, String?)? = nil + if let peerId = effectiveMainSpeaker { for entry in currentEntries { switch entry { case let .peer(peer): @@ -4207,9 +4337,7 @@ public final class VoiceChatController: ViewController { otherEndpointId = peer.presentationEndpointId } - if let endpointId = effectiveEndpointId { - effectivePeer = (peer.peer.id, endpointId, otherEndpointId) - } + 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 { @@ -4234,21 +4362,23 @@ public final class VoiceChatController: ViewController { } if effectivePeer == nil { - if self.currentForcedSpeakerWithVideo != nil { - self.currentForcedSpeakerWithVideo = nil + if self.currentForcedSpeaker != nil { + self.currentForcedSpeaker = nil } - if self.currentDominantSpeakerWithVideo != nil { - self.currentDominantSpeakerWithVideo = nil + if self.currentDominantSpeaker != nil { + self.currentDominantSpeaker = nil } effectivePeer = anyPeer } - self.effectiveSpeakerWithVideo = effectivePeer.flatMap { ($0.0, $0.1) } + self.effectiveSpeaker = effectivePeer.flatMap { ($0.0, $0.1) } if updateMembers { - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), updatePinnedPeer: false) + self.updateMembers() } - self.mainVideoNode.updatePeer(peer: self.effectiveSpeakerWithVideo, waitForFullSize: false) + self.mainStageNode.update(peer: self.effectiveSpeaker, waitForFullSize: waitForFullSize, completion: { + completion?() + }) } private func updateRequestedVideoChannels() { @@ -4348,11 +4478,7 @@ public final class VoiceChatController: ViewController { self.updateDecorationsLayout(transition: .immediate) } - var translateBounds = !self.isExpanded - if case .fullscreen = self.effectiveDisplayMode { - translateBounds = true - } - + let translateBounds = !self.isExpanded if translateBounds { var bounds = self.contentContainer.bounds bounds.origin.y = -translation @@ -4416,6 +4542,7 @@ public final class VoiceChatController: ViewController { } else { self.panGestureArguments = nil var dismissing = false + self.isExpanded if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { if self.isScheduling { self.dismissScheduled() @@ -4680,7 +4807,7 @@ public final class VoiceChatController: ViewController { } })) - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) + self.updateMembers() } private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) { @@ -4818,47 +4945,33 @@ public final class VoiceChatController: ViewController { return self.isScheduling || self.callState?.scheduleTimestamp != nil } - private func toggleDisplayMode() { + private func updateDisplayMode(_ displayMode: DisplayMode) { guard !self.animatingExpansion else { return } + self.updateMembers() - self.updateMembers(muteState: self.effectiveMuteState, callMembers: self.currentCallMembers ?? ([], nil), invitedPeers: self.currentInvitedPeers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set()) - - let effectiveDisplayMode = self.displayMode - let nextDisplayMode: DisplayMode - let isLandscape = self.isLandscape + let previousDisplayMode = self.displayMode var isFullscreen = false - - switch effectiveDisplayMode { - case .modal: - isFullscreen = true - nextDisplayMode = .fullscreen(controlsHidden: false) - case let .fullscreen(controlsHidden): - if controlsHidden { - if !isLandscape { - nextDisplayMode = .modal(isExpanded: true, isFilled: true) - } else { - isFullscreen = true - nextDisplayMode = .fullscreen(controlsHidden: false) - } - } else { - isFullscreen = true - nextDisplayMode = .fullscreen(controlsHidden: true) - } + if case .fullscreen = displayMode { + isFullscreen = true } let completion = { - self.displayMode = nextDisplayMode + self.displayMode = displayMode self.updateDecorationsColors() - self.mainVideoContainerNode.isHidden = false - self.mainVideoContainerNode.isUserInteractionEnabled = isFullscreen + self.mainStageContainerNode.isHidden = false + self.mainStageContainerNode.isUserInteractionEnabled = isFullscreen - if case .modal = effectiveDisplayMode, case .fullscreen = self.displayMode { + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + + if case .modal = previousDisplayMode, case .fullscreen = self.displayMode { self.fullscreenListNode.isHidden = false + self.updateDecorationsLayout(transition: .immediate) + var minimalVisiblePeerid: (PeerId, CGFloat)? var verticalItemNodes: [PeerId: ASDisplayNode] = [:] self.listNode.forEachItemNode { itemNode in @@ -4896,20 +5009,20 @@ public final class VoiceChatController: ViewController { self.animatingExpansion = true let completion = { - let effectiveSpeakerPeerId = self.effectiveSpeakerWithVideo?.0 + let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 if let effectiveSpeakerPeerId = effectiveSpeakerPeerId, let otherItemNode = verticalItemNodes[effectiveSpeakerPeerId] { - self.mainVideoNode.animateTransitionIn(from: otherItemNode) + self.mainStageNode.animateTransitionIn(from: otherItemNode, transition: transition) } self.fullscreenListNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatFullscreenParticipantItemNode, let item = itemNode.item, let otherItemNode = verticalItemNodes[item.peer.id] { - itemNode.animateTransitionIn(from: otherItemNode, containerNode: self, animate: item.peer.id != effectiveSpeakerPeerId) + itemNode.animateTransitionIn(from: otherItemNode, containerNode: self, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) } } if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) } } if false, let (peerId, _) = minimalVisiblePeerid { @@ -4927,7 +5040,7 @@ public final class VoiceChatController: ViewController { } else { completion() } - } else if case .fullscreen = effectiveDisplayMode, case .modal = self.displayMode { + } else if case .fullscreen = previousDisplayMode, case .modal = self.displayMode { var minimalVisiblePeerid: (PeerId, CGFloat)? var fullscreenItemNodes: [PeerId: VoiceChatFullscreenParticipantItemNode] = [:] self.fullscreenListNode.forEachItemNode { itemNode in @@ -4947,14 +5060,14 @@ public final class VoiceChatController: ViewController { self.animatingExpansion = true let completion = { - let effectiveSpeakerPeerId = self.effectiveSpeakerWithVideo?.0 + let effectiveSpeakerPeerId = self.effectiveSpeaker?.0 var targetTileNode: VoiceChatTileItemNode? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? VoiceChatTilesGridItemNode { for tileNode in itemNode.tileNodes { if let item = tileNode.item, let otherItemNode = fullscreenItemNodes[item.peer.id] { - tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, animate: item.peer.id != effectiveSpeakerPeerId) + tileNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition, animate: item.peer.id != effectiveSpeakerPeerId) if item.peer.id == effectiveSpeakerPeerId { targetTileNode = tileNode @@ -4962,22 +5075,21 @@ public final class VoiceChatController: ViewController { } } } else if let itemNode = itemNode as? VoiceChatParticipantItemNode, let item = itemNode.item, let otherItemNode = fullscreenItemNodes[item.peer.id] { - itemNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode) + itemNode.animateTransitionIn(from: otherItemNode, containerNode: self.transitionContainerNode, transition: transition) } } - if let targetTileNode = targetTileNode { - self.mainVideoNode.animateTransitionOut(to: targetTileNode, completion: { [weak self] in - self?.effectiveSpeakerWithVideo = nil - self?.mainVideoNode.updatePeer(peer: nil, waitForFullSize: false) - self?.fullscreenListNode.isHidden = true - self?.mainVideoContainerNode.isHidden = true - }) - } + self.mainStageNode.animateTransitionOut(to: targetTileNode, transition: transition, completion: { [weak self] in + self?.effectiveSpeaker = nil + self?.mainStageNode.update(peer: nil, waitForFullSize: false) + self?.fullscreenListNode.isHidden = true + self?.mainStageContainerNode.isHidden = true + }) + if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) } } if false, let (peerId, _) = minimalVisiblePeerid { @@ -4997,17 +5109,17 @@ public final class VoiceChatController: ViewController { } } else if case .fullscreen = self.displayMode { self.animatingExpansion = true - // self.updateIsFullscreen(strongSelf.isFullscreen, force: true) if let (layout, navigationHeight) = self.validLayout { - self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .easeInOut)) - self.updateDecorationsLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) + self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition) + self.updateDecorationsLayout(transition: transition) } } } - if case .fullscreen(false) = nextDisplayMode, case .modal = effectiveDisplayMode { - self.mainVideoNode.updatePeer(peer: self.effectiveSpeakerWithVideo, waitForFullSize: true, completion: { + if case .fullscreen(false) = displayMode, case .modal = previousDisplayMode { + self.updateMainVideo(waitForFullSize: true, updateMembers: false, force: true, completion: { completion() }) } else { @@ -5276,21 +5388,39 @@ private final class VoiceChatContextReferenceContentSource: ContextReferenceCont } } -final class VoiceChatMainVideoContainerNode: ASDisplayNode { +private let backArrowImage = NavigationBarTheme.generateBackArrowImage(color: .white) + +final class VoiceChatMainStageContainerNode: ASDisplayNode { private let context: AccountContext private let call: PresentationGroupCall + private var currentPeer: (PeerId, String?)? + private var currentPeerEntry: VoiceChatController.Node.PeerEntry? private var currentVideoNode: GroupVideoNode? private var candidateVideoNode: GroupVideoNode? private let backgroundNode: ASDisplayNode - private let fadeNode: ASImageNode - private var currentPeer: (PeerId, String)? + private let topFadeNode: ASImageNode + private let bottomFadeNode: ASImageNode + private let headerNode: ASDisplayNode + private let backButtonNode: HighlightableButtonNode + private let backButtonArrowNode: ASImageNode + private let pinButtonNode: HighlightTrackingButtonNode + private let pinButtonIconNode: ASImageNode + private let pinButtonTitleNode: ImmediateTextNode + private var audioLevelView: VoiceBlobView? + private let audioLevelDisposable = MetaDisposable() + private var avatarNode: AvatarNode + private let titleNode: ImmediateTextNode + private let microphoneNode: VoiceChatMicrophoneNode private var validLayout: (CGSize, CGFloat, Bool)? var tapped: (() -> Void)? - var otherVideoTapped: (() -> Void)? + var back: (() -> Void)? + var togglePin: (() -> Void)? + + var getAudioLevel: ((PeerId) -> Signal)? private let videoReadyDisposable = MetaDisposable() @@ -5302,12 +5432,27 @@ final class VoiceChatMainVideoContainerNode: ASDisplayNode { self.backgroundNode.alpha = 0.0 self.backgroundNode.backgroundColor = UIColor(rgb: 0x1c1c1e) - self.fadeNode = ASImageNode() - self.fadeNode.alpha = 0.0 - self.fadeNode.displaysAsynchronously = false - self.fadeNode.displayWithoutProcessing = true - self.fadeNode.contentMode = .scaleToFill - self.fadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in + self.topFadeNode = ASImageNode() + self.topFadeNode.alpha = 0.0 + self.topFadeNode.displaysAsynchronously = false + self.topFadeNode.displayWithoutProcessing = true + self.topFadeNode.contentMode = .scaleToFill + self.topFadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let colorsArray = [UIColor(rgb: 0x000000, alpha: 0.7).cgColor, UIColor(rgb: 0x000000, alpha: 0.0).cgColor] as CFArray + var locations: [CGFloat] = [0.0, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + }) + + self.bottomFadeNode = ASImageNode() + self.bottomFadeNode.alpha = 0.0 + self.bottomFadeNode.displaysAsynchronously = false + self.bottomFadeNode.displayWithoutProcessing = true + self.bottomFadeNode.contentMode = .scaleToFill + self.bottomFadeNode.image = generateImage(CGSize(width: 1.0, height: 50.0), rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) @@ -5317,13 +5462,89 @@ final class VoiceChatMainVideoContainerNode: ASDisplayNode { context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) }) + self.headerNode = ASDisplayNode() + self.headerNode.alpha = 0.0 + + self.backButtonArrowNode = ASImageNode() + self.backButtonArrowNode.displayWithoutProcessing = true + self.backButtonArrowNode.displaysAsynchronously = false + self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white) + self.backButtonNode = HighlightableButtonNode() + + self.pinButtonIconNode = ASImageNode() + self.pinButtonIconNode.displayWithoutProcessing = true + self.pinButtonIconNode.displaysAsynchronously = false + self.pinButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) + self.pinButtonTitleNode = ImmediateTextNode() + self.pinButtonTitleNode.isHidden = true + self.pinButtonTitleNode.attributedText = NSAttributedString(string: "Unpin", font: Font.regular(17.0), textColor: .white) + self.pinButtonNode = HighlightableButtonNode() + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0)) + self.avatarNode.isHidden = true + + self.titleNode = ImmediateTextNode() + self.titleNode.alpha = 0.0 + self.titleNode.isUserInteractionEnabled = false + + self.microphoneNode = VoiceChatMicrophoneNode() + self.microphoneNode.alpha = 0.0 + super.init() self.clipsToBounds = true self.cornerRadius = 11.0 self.addSubnode(self.backgroundNode) - self.addSubnode(self.fadeNode) + self.addSubnode(self.topFadeNode) + self.addSubnode(self.bottomFadeNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.microphoneNode) + self.addSubnode(self.headerNode) + + self.headerNode.addSubnode(self.backButtonNode) + self.headerNode.addSubnode(self.backButtonArrowNode) + self.headerNode.addSubnode(self.pinButtonIconNode) + self.headerNode.addSubnode(self.pinButtonTitleNode) + self.headerNode.addSubnode(self.pinButtonNode) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: []) + self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0) + self.backButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backButtonNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonArrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backButtonNode.alpha = 0.4 + strongSelf.backButtonArrowNode.alpha = 0.4 + } else { + strongSelf.backButtonNode.alpha = 1.0 + strongSelf.backButtonArrowNode.alpha = 1.0 + strongSelf.backButtonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.backButtonArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) + + self.pinButtonNode.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.pinButtonTitleNode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonTitleNode.alpha = 0.4 + strongSelf.pinButtonIconNode.alpha = 0.4 + } else { + strongSelf.pinButtonTitleNode.alpha = 1.0 + strongSelf.pinButtonIconNode.alpha = 1.0 + strongSelf.pinButtonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + self.pinButtonNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside) } deinit { @@ -5340,97 +5561,248 @@ final class VoiceChatMainVideoContainerNode: ASDisplayNode { self.tapped?() } + @objc private func backPressed() { + self.back?() + } + + @objc private func pinPressed() { + self.togglePin?() + } + var animating = false - fileprivate func animateTransitionIn(from sourceNode: ASDisplayNode) { - guard let sourceNode = sourceNode as? VoiceChatTileItemNode, let _ = sourceNode.item else { + fileprivate func animateTransitionIn(from sourceNode: ASDisplayNode, transition: ContainedViewLayoutTransition) { + guard let sourceNode = sourceNode as? VoiceChatTileItemNode, let _ = sourceNode.item, let (_, bottomInset, _) = self.validLayout else { return } - + let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 1.0) - alphaTransition.updateAlpha(node: self.fadeNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.titleNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 1.0) + alphaTransition.updateAlpha(node: self.headerNode, alpha: 1.0) + + sourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1) self.animating = true - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let targetFrame = self.frame let startLocalFrame = sourceNode.view.convert(sourceNode.bounds, to: self.supernode?.view) - self.update(size: startLocalFrame.size, sideInset: 0.0, isLandscape: true, force: true, transition: .immediate) + self.update(size: startLocalFrame.size, bottomInset: bottomInset, isLandscape: true, force: true, transition: .immediate) self.frame = startLocalFrame - self.update(size: targetFrame.size, sideInset: 0.0, isLandscape: true, force: true, transition: transition) + self.update(size: targetFrame.size, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition) transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in self?.animating = false }) } - fileprivate func animateTransitionOut(to targetNode: ASDisplayNode, completion: @escaping () -> Void) { - guard let targetNode = targetNode as? VoiceChatTileItemNode, let _ = targetNode.item else { + fileprivate func animateTransitionOut(to targetNode: ASDisplayNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { + guard let (_, bottomInset, _) = self.validLayout else { return } let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0) - alphaTransition.updateAlpha(node: self.fadeNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.bottomFadeNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 0.0) + alphaTransition.updateAlpha(node: self.headerNode, alpha: 0.0) + guard let targetNode = targetNode as? VoiceChatTileItemNode, let _ = targetNode.item else { + completion() + return + } + + targetNode.isHidden = false + targetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.animating = true - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) let initialFrame = self.frame let targetFrame = targetNode.view.convert(targetNode.bounds, to: self.supernode?.view) - self.update(size: targetFrame.size, sideInset: 0.0, isLandscape: true, force: true, transition: transition) + self.update(size: targetFrame.size, bottomInset: bottomInset, isLandscape: true, force: true, transition: transition) transition.updateFrame(node: self, frame: targetFrame, completion: { [weak self] _ in if let strongSelf = self { completion() + strongSelf.animating = false strongSelf.frame = initialFrame - strongSelf.update(size: initialFrame.size, sideInset: 0.0, isLandscape: true, transition: .immediate) + strongSelf.update(size: initialFrame.size, bottomInset: bottomInset, isLandscape: true, transition: .immediate) } }) } - func updatePeer(peer: (peerId: PeerId, endpointId: String)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) { - if self.currentPeer?.0 == peer?.0 && self.currentPeer?.1 == peer?.1 { + private var silenceTimer: SwiftSignalKit.Timer? + + fileprivate func update(peerEntry: VoiceChatController.Node.PeerEntry, pinned: Bool) { + let previousPeerEntry = self.currentPeerEntry + self.currentPeerEntry = peerEntry + if !arePeersEqual(previousPeerEntry?.peer, peerEntry.peer) { + let peer = peerEntry.peer + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + if previousPeerEntry?.peer.id == peerEntry.peer.id { + self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: peer) + } else { + let previousAvatarNode = self.avatarNode + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 42.0)) + self.avatarNode.setPeer(context: self.context, theme: presentationData.theme, peer: peer, synchronousLoad: true) + self.avatarNode.frame = previousAvatarNode.frame + previousAvatarNode.supernode?.insertSubnode(self.avatarNode, aboveSubnode: previousAvatarNode) + previousAvatarNode.removeFromSupernode() + } + self.titleNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: .white) + if let (size, bottomInset, isLandscape) = self.validLayout { + self.update(size: size, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate) + } + } + + 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 wavesColor = UIColor(rgb: 0x34c759) + if let getAudioLevel = self.getAudioLevel, previousPeerEntry?.peer.id != peerEntry.peer.id { + self.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() + 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.5) + 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) + } + })) + } + + 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 { + 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.microphoneNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: .white), animated: true) + } + + fileprivate func update(peer: (peer: PeerId, endpointId: String?)?, waitForFullSize: Bool, completion: (() -> Void)? = nil) { + let previousPeer = self.currentPeer + if previousPeer?.0 == peer?.0 && previousPeer?.1 == peer?.1 { completion?() return } - let previousPeer = self.currentPeer self.currentPeer = peer + if let (_, endpointId) = peer { if endpointId != previousPeer?.1 { - self.call.makeIncomingVideoView(endpointId: endpointId, completion: { [weak self] videoView in - Queue.mainQueue().async { - guard let strongSelf = self, let videoView = videoView else { - return - } - - let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) - if let currentVideoNode = strongSelf.currentVideoNode { - strongSelf.currentVideoNode = nil + if let endpointId = endpointId { + self.avatarNode.isHidden = true + self.audioLevelView?.isHidden = true + + self.call.makeIncomingVideoView(endpointId: endpointId, completion: { [weak self] videoView in + Queue.mainQueue().async { + guard let strongSelf = self, let videoView = videoView else { + return + } - currentVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentVideoNode] _ in - currentVideoNode?.removeFromSupernode() - }) - } - strongSelf.currentVideoNode = videoNode - strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backgroundNode) - if let (size, sideInset, isLandscape) = strongSelf.validLayout { - strongSelf.update(size: size, sideInset: sideInset, isLandscape: isLandscape, transition: .immediate) - } - - if waitForFullSize { - strongSelf.videoReadyDisposable.set((videoNode.ready - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { _ in - Queue.mainQueue().after(0.01) { - completion?() - } - })) - } else { - strongSelf.videoReadyDisposable.set(nil) - completion?() + let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: nil) + if let currentVideoNode = strongSelf.currentVideoNode { + strongSelf.currentVideoNode = nil + + currentVideoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentVideoNode] _ in + currentVideoNode?.removeFromSupernode() + }) + } + strongSelf.currentVideoNode = videoNode + strongSelf.insertSubnode(videoNode, aboveSubnode: strongSelf.backgroundNode) + if let (size, bottomInset, isLandscape) = strongSelf.validLayout { + strongSelf.update(size: size, bottomInset: bottomInset, isLandscape: isLandscape, transition: .immediate) + } + + if waitForFullSize { + strongSelf.videoReadyDisposable.set((videoNode.ready + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + Queue.mainQueue().after(0.01) { + completion?() + } + })) + } else { + strongSelf.videoReadyDisposable.set(nil) + completion?() + } } + }) + } else { + self.avatarNode.isHidden = false + self.audioLevelView?.isHidden = false + if let currentVideoNode = self.currentVideoNode { + currentVideoNode.removeFromSupernode() + self.currentVideoNode = nil } - }) + } } else { + self.audioLevelView?.isHidden = self.currentPeer?.1 != nil completion?() } } else { @@ -5443,8 +5815,8 @@ final class VoiceChatMainVideoContainerNode: ASDisplayNode { } } - func update(size: CGSize, sideInset: CGFloat, isLandscape: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) { - self.validLayout = (size, sideInset, isLandscape) + func update(size: CGSize, bottomInset: CGFloat, isLandscape: Bool, force: Bool = false, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, bottomInset, isLandscape) if self.animating && !force { return @@ -5457,10 +5829,39 @@ final class VoiceChatMainVideoContainerNode: ASDisplayNode { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size)) + 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) + } + + let animationSize = CGSize(width: 36.0, height: 36.0) + let titleSize = self.titleNode.updateLayout(size) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: 12.0 + animationSize.width, y: size.height - bottomInset - titleSize.height - 16.0), size: titleSize)) + + transition.updateFrame(node: self.microphoneNode, frame: CGRect(origin: CGPoint(x: 7.0, y: size.height - bottomInset - animationSize.height - 6.0), size: animationSize)) + var fadeHeight: CGFloat = 50.0 if size.width < size.height { fadeHeight = 140.0 } - transition.updateFrame(node: self.fadeNode, frame: CGRect(x: sideInset, y: size.height - fadeHeight, width: size.width - sideInset * 2.0, height: fadeHeight)) + transition.updateFrame(node: self.bottomFadeNode, frame: CGRect(x: 0.0, y: size.height - fadeHeight, width: size.width, height: fadeHeight)) + transition.updateFrame(node: self.topFadeNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: 50.0)) + + let backSize = self.backButtonNode.measure(CGSize(width: 320.0, height: 100.0)) + if let image = self.backButtonArrowNode.image { + transition.updateFrame(node: self.backButtonArrowNode, frame: CGRect(origin: CGPoint(x: 9.0, y: 12.0), size: image.size)) + } + transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: 28.0, y: 13.0), size: backSize)) + + let unpinSize = self.pinButtonTitleNode.updateLayout(size) + if let image = self.pinButtonIconNode.image { + transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width, y: 0.0), size: image.size)) + transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - unpinSize.width + 4.0, y: 14.0), size: unpinSize)) + transition.updateFrame(node: self.pinButtonNode, frame: CGRect(x: size.width - image.size.width - unpinSize.width, y: 0.0, width: unpinSize.width + image.size.width, height: 44.0)) + } + + transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0))) } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift index df49290f99..f187f2fd1d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatFullscreenParticipantItem.swift @@ -72,6 +72,7 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { let context: AccountContext let peer: Peer let icon: Icon + let text: VoiceChatParticipantItem.ParticipantText let color: Color let isLandscape: Bool let active: Bool @@ -83,12 +84,13 @@ final class VoiceChatFullscreenParticipantItem: ListViewItem { public let selectable: Bool = true - public init(presentationData: ItemListPresentationData, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, peer: Peer, icon: Icon, 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, 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.color = color self.isLandscape = isLandscape self.active = active @@ -157,6 +159,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { let avatarNode: AvatarNode let contentWrapperNode: ASDisplayNode private let titleNode: TextNode + private let statusNode: VoiceChatParticipantStatusNode private var credibilityIconNode: ASImageNode? private let actionContainerNode: ASDisplayNode @@ -174,6 +177,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { private var layoutParams: (VoiceChatFullscreenParticipantItem, ListViewItemLayoutParams, Bool, Bool)? private var isExtracted = false private var animatingExtraction = false + private var animatingSelection = false private var wavesColor: UIColor? let videoContainerNode: ASDisplayNode @@ -183,14 +187,14 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { private var videoReadyDelayed = false private var videoReady = false + private var profileNode: VoiceChatPeerProfileNode? + private var raiseHandTimer: SwiftSignalKit.Timer? var item: VoiceChatFullscreenParticipantItem? { return self.layoutParams?.0 } - private var currentTitle: String? - init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() @@ -231,6 +235,8 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.titleNode.isUserInteractionEnabled = false self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = VoiceChatParticipantStatusNode() self.actionContainerNode = ASDisplayNode() self.actionButtonNode = HighlightableButtonNode() @@ -256,7 +262,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.containerNode.shouldBegin = { [weak self] location in - guard let strongSelf = self else { + guard let _ = self else { return false } return true @@ -268,6 +274,12 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { } contextAction(strongSelf.contextSourceNode, gesture) } + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.updateIsExtracted(isExtracted, transition: transition) + } // self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in // guard let strongSelf = self, let item = strongSelf.layoutParams?.0 else { @@ -575,10 +587,16 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.layoutParams?.0.action?(self.contextSourceNode) } - func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, animate: Bool = true) { + func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, transition: ContainedViewLayoutTransition, animate: Bool = true) { guard let item = self.item else { return } + var duration: Double = 0.2 + var timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue + if case let .animated(transitionDuration, curve) = transition { + duration = transitionDuration + timingFunction = curve.timingFunction + } let initialAnimate = animate if let sourceNode = sourceNode as? VoiceChatTileItemNode { @@ -602,12 +620,11 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.videoContainerNode.insertSubnode(videoNode, at: 0) if animate { - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) videoNode.updateLayout(size: videoSize, isLandscape: true, transition: transition) let scale = sourceNode.bounds.width / videoSize.width - self.videoContainerNode.layer.animateScale(from: sourceNode.bounds.width / videoSize.width, to: tileSize.width / videoSize.width, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.videoContainerNode.layer.animate(from: backgroundCornerRadius * (1.0 / scale) as NSNumber, to: videoCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { _ in + self.videoContainerNode.layer.animateScale(from: sourceNode.bounds.width / videoSize.width, to: tileSize.width / videoSize.width, duration: duration, timingFunction: timingFunction) + self.videoContainerNode.layer.animate(from: backgroundCornerRadius * (1.0 / scale) as NSNumber, to: videoCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: timingFunction, duration: duration, removeOnCompletion: false, completion: { _ in }) self.videoFadeNode.alpha = 1.0 @@ -625,7 +642,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.contextSourceNode.position = targetContainerPosition containerNode.addSubnode(self.contextSourceNode) - self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in + self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self] _ in if let strongSelf = self { strongSelf.contextSourceNode.position = initialPosition strongSelf.containerNode.addSubnode(strongSelf.contextSourceNode) @@ -634,16 +651,16 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { if item.active { self.borderImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.borderImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.borderImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: timingFunction) } - self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: timingFunction) + self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) } else if !initialAnimate { - self.contextSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.contextSourceNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) + self.contextSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contextSourceNode.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) } } else if let sourceNode = sourceNode as? VoiceChatParticipantItemNode, let _ = sourceNode.item { var startContainerPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center @@ -662,8 +679,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.contextSourceNode.position = targetContainerPosition containerNode.addSubnode(self.contextSourceNode) - let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue - self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: 0.2, timingFunction: timingFunction, completion: { [weak self, weak sourceNode] _ in + self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self, weak sourceNode] _ in if let strongSelf = self { sourceNode?.avatarNode.alpha = 1.0 strongSelf.contextSourceNode.position = initialPosition @@ -676,18 +692,49 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { self.borderImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: timingFunction) } - self.avatarNode.layer.animateScale(from: 0.8, to: 1.0, duration: 0.2) + self.avatarNode.layer.animateScale(from: 0.8, to: 1.0, duration: duration, timingFunction: timingFunction) - self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: timingFunction) - self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, timingFunction: timingFunction) - self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2, timingFunction: timingFunction) - self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, timingFunction: timingFunction) + self.backgroundImageNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: timingFunction) + self.backgroundImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contentWrapperNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contentWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: timingFunction) } } } + private func updateIsExtracted(_ isExtracted: Bool, transition: ContainedViewLayoutTransition) { + guard self.isExtracted != isExtracted, let extractedRect = self.extractedRect, let nonExtractedRect = self.nonExtractedRect, let item = self.item else { + return + } + self.isExtracted = isExtracted + if isExtracted { + let profileNode = VoiceChatPeerProfileNode(context: item.context, size: extractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in + self?.contextSourceNode.requestDismiss?() + }) + self.profileNode = profileNode + self.contextSourceNode.contentNode.addSubnode(profileNode) + + profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition) + + self.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self, let profileNode = strongSelf.profileNode { + if profileNode.avatarListWrapperNode.frame.contains(point) { + return profileNode.avatarListNode.view + } + } + return nil + } + } else if let profileNode = self.profileNode { + self.profileNode = nil + profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition) + + self.contextSourceNode.contentNode.customHitTest = nil + } + } + func asyncLayout() -> (_ item: VoiceChatFullscreenParticipantItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, (Bool, Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeStatusLayout = self.statusNode.asyncLayout() let currentItem = self.layoutParams?.0 let hasVideo = self.videoNode != nil @@ -760,6 +807,9 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { let constrainedWidth = params.width - 24.0 - 10.0 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 availableWidth = params.availableHeight + let (statusLayout, _) = makeStatusLayout(CGSize(width: availableWidth - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, true) + let contentSize = tileSize let insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: !last ? 6.0 : 0.0, right: 0.0) @@ -769,7 +819,6 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { if let strongSelf = self { let hadItem = strongSelf.layoutParams?.0 != nil strongSelf.layoutParams = (item, params, first, last) - strongSelf.currentTitle = titleAttributedString?.string strongSelf.wavesColor = wavesColor let videoNode = item.getVideo() @@ -877,7 +926,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { transition.updateFrameAsPositionAndBounds(node: strongSelf.avatarNode, frame: avatarFrame) - let blobFrame = avatarFrame.insetBy(dx: -14.0, dy: -14.0) + let blobFrame = avatarFrame.insetBy(dx: -18.0, dy: -18.0) if let getAudioLevel = item.getAudioLevel { if !strongSelf.didSetupAudioLevel || currentItem?.peer.id != item.peer.id { strongSelf.audioLevelView?.frame = blobFrame @@ -902,7 +951,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { playbackMaskLayer.frame = maskRect playbackMaskLayer.fillRule = .evenOdd let maskPath = UIBezierPath() - maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 14, dy: 14), cornerRadius: 22)) + maskPath.append(UIBezierPath(roundedRect: maskRect.insetBy(dx: 18, dy: 18), cornerRadius: 22)) maskPath.append(UIBezierPath(rect: maskRect)) playbackMaskLayer.path = maskPath.cgPath audioLevelView.layer.mask = playbackMaskLayer @@ -912,6 +961,10 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.audioLevelView = audioLevelView strongSelf.offsetContainerNode.view.insertSubview(audioLevelView, at: 0) + + if let item = strongSelf.item, strongSelf.videoNode != nil || item.active { + audioLevelView.alpha = 0.0 + } } let level = min(1.0, max(0.0, CGFloat(value))) @@ -926,12 +979,13 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { audioLevelView.setColor(wavesColor, animated: true) } } else { - audioLevelView.stopAnimating(duration: 0.5) avatarScale = 1.0 } - let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut) - transition.updateTransformScale(node: strongSelf.avatarNode, scale: strongSelf.isExtracted ? 1.0 : avatarScale, beginWithCurrentState: 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) + } } })) } @@ -1073,19 +1127,27 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.videoContainerNode.layer.animateScale(from: videoContainerScale, to: 0.001, duration: 0.2) strongSelf.avatarNode.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -9.0), duration: 0.2, additive: true) + strongSelf.audioLevelView?.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } transition.updateAlpha(node: videoNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0) transition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0) + if let audioLevelView = strongSelf.audioLevelView { + transition.updateAlpha(layer: audioLevelView.layer, alpha: 1.0) + } } else { if !strongSelf.avatarNode.alpha.isZero { strongSelf.videoContainerNode.layer.animateScale(from: 0.001, to: videoContainerScale, duration: 0.2) strongSelf.avatarNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) + strongSelf.audioLevelView?.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2) strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -9.0), to: CGPoint(), duration: 0.2, additive: true) } transition.updateAlpha(node: videoNode, alpha: 1.0) transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 1.0) transition.updateAlpha(node: strongSelf.avatarNode, alpha: 0.0) + if let audioLevelView = strongSelf.audioLevelView { + transition.updateAlpha(layer: audioLevelView.layer, alpha: 0.0) + } } } else { if item.active { diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift index 869dac20c9..3de080559d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatOverlayController.swift @@ -169,13 +169,13 @@ public final class VoiceChatOverlayController: ViewController { if reclaim { self.dismissed = true - let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height - layout.intrinsicInsets.bottom - 205.0 / 2.0 - 2.0) + 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 actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) - actionButton.position = CGPoint(x: targetPosition.x, y: 205.0 / 2.0) + actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false @@ -191,7 +191,7 @@ public final class VoiceChatOverlayController: ViewController { actionButton.layer.removeAllAnimations() actionButton.layer.sublayerTransform = CATransform3DIdentity actionButton.update(snap: false, animated: false) - actionButton.position = CGPoint(x: targetPosition.x, y: 205.0 / 2.0) + actionButton.position = CGPoint(x: targetPosition.x, y: bottomAreaHeight / 2.0) leftButton.isHidden = false rightButton.isHidden = false diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index be870f5cab..b96de0c7fe 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -138,7 +138,7 @@ private let accentColor: UIColor = UIColor(rgb: 0x007aff) private let constructiveColor: UIColor = UIColor(rgb: 0x34c759) private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) -private class VoiceChatParticipantStatusNode: ASDisplayNode { +class VoiceChatParticipantStatusNode: ASDisplayNode { private var iconNodes: [ASImageNode] private let textNode: TextNode @@ -156,10 +156,10 @@ private class VoiceChatParticipantStatusNode: ASDisplayNode { self.addSubnode(self.textNode) } - func asyncLayout() -> (_ size: CGSize, _ text: VoiceChatParticipantItem.ParticipantText, _ transparent: Bool) -> (CGSize, () -> Void) { + func asyncLayout() -> (_ size: CGSize, _ text: VoiceChatParticipantItem.ParticipantText, _ expanded: Bool) -> (CGSize, () -> Void) { let makeTextLayout = TextNode.asyncLayout(self.textNode) - return { size, text, transparent in + return { size, text, expanded in let statusFont = Font.regular(14.0) var attributedString: NSAttributedString? @@ -184,9 +184,6 @@ private class VoiceChatParticipantStatusNode: ASDisplayNode { case .destructive: textColorValue = destructiveColor } - if transparent { - textColorValue = UIColor(rgb: 0xffffff, alpha: 0.65) - } color = textColorValue attributedString = NSAttributedString(string: text, font: statusFont, textColor: textColorValue) default: @@ -207,7 +204,7 @@ private class VoiceChatParticipantStatusNode: ASDisplayNode { icons.append(image) } - let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: size.width - (iconSize.width + spacing) * CGFloat(icons.count), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: expanded ? 4 : 1, truncationType: .end, constrainedSize: CGSize(width: size.width - (iconSize.width + spacing) * CGFloat(icons.count), height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var contentSize = textLayout.size contentSize.width += (iconSize.width + spacing) * CGFloat(icons.count) @@ -388,7 +385,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.isExtracted = isExtracted - let inset: CGFloat = 12.0 + let inset: CGFloat = 0.0 if isExtracted { strongSelf.contextSourceNode.contentNode.customHitTest = { [weak self] point in if let strongSelf = self { @@ -492,8 +489,6 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { strongSelf.avatarNode.isHidden = true avatarListWrapperNode.contentNode.addSubnode(transitionNode) - - strongSelf.avatarTransitionNode = transitionNode let avatarListContainerNode = ASDisplayNode() @@ -503,8 +498,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { avatarListContainerNode.cornerRadius = targetRect.width / 2.0 avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) - avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in - }) + avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: avatarInitialRect.center), to: NSValue(cgPoint: avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping) radiusTransition.updateCornerRadius(node: avatarListContainerNode, cornerRadius: 0.0) @@ -576,10 +570,10 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { alphaTransition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0, delay: isExtracted ? 0.0 : 0.1) let offsetInitialSublayerTransform = strongSelf.offsetContainerNode.layer.sublayerTransform - strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -33 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0) + strongSelf.offsetContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? -43 : 0.0, isExtracted ? extractedVerticalOffset : 0.0, 0.0) let actionInitialSublayerTransform = strongSelf.actionContainerNode.layer.sublayerTransform - strongSelf.actionContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? 21.0 : 0.0, 0.0, 0.0) + strongSelf.actionContainerNode.layer.sublayerTransform = CATransform3DMakeTranslation(isExtracted ? 43.0 : 0.0, 0.0, 0.0) let initialBackgroundPosition = strongSelf.backgroundImageNode.position strongSelf.backgroundImageNode.layer.position = rect.center @@ -636,7 +630,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { transition.updateAlpha(node: strongSelf.actionContainerNode, alpha: isExtracted ? 0.0 : 1.0) transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? inset : 0.0, y: isExtracted ? extractedVerticalOffset : 0.0)) - transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -24.0 : 0.0, y: 0.0)) + transition.updateSublayerTransformOffset(layer: strongSelf.actionContainerNode.layer, offset: CGPoint(x: isExtracted ? -inset * 2.0 : 0.0, y: 0.0)) transition.updateAlpha(node: strongSelf.backgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in if !isExtracted { @@ -659,11 +653,17 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.layoutParams?.0.action?(self.contextSourceNode) } - func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode) { + func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, transition: ContainedViewLayoutTransition) { guard let _ = self.item, let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode, let _ = sourceNode.item else { return } - + var duration: Double = 0.2 + var timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue + if case let .animated(transitionDuration, curve) = transition { + duration = transitionDuration + timingFunction = curve.timingFunction + } + let startContainerAvatarPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center var animate = true if startContainerAvatarPosition.x < -tileSize.width || startContainerAvatarPosition.x > containerNode.frame.width + tileSize.width { @@ -673,13 +673,13 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { sourceNode.avatarNode.alpha = 0.0 let initialAvatarPosition = self.avatarNode.position - let targetContainerAvatarPosition = self.avatarNode.view.convert(self.avatarNode.bounds, to: containerNode.view).center - + let initialBackgroundPosition = sourceNode.backgroundImageNode.position + let initialContentPosition = sourceNode.contentWrapperNode.position + let startContainerBackgroundPosition = sourceNode.backgroundImageNode.view.convert(sourceNode.backgroundImageNode.bounds, to: containerNode.view).center let startContainerContentPosition = sourceNode.contentWrapperNode.view.convert(sourceNode.contentWrapperNode.bounds, to: containerNode.view).center - let initialBackgroundPosition = sourceNode.backgroundImageNode.position - let initialContentPosition = sourceNode.contentWrapperNode.position + let targetContainerAvatarPosition = self.avatarNode.view.convert(self.avatarNode.bounds, to: containerNode.view).center sourceNode.backgroundImageNode.position = targetContainerAvatarPosition sourceNode.contentWrapperNode.position = targetContainerAvatarPosition @@ -687,9 +687,8 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { containerNode.addSubnode(sourceNode.contentWrapperNode) sourceNode.borderImageNode.alpha = 0.0 - - let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue - sourceNode.backgroundImageNode.layer.animatePosition(from: startContainerBackgroundPosition, to: targetContainerAvatarPosition, duration: 0.2, timingFunction: timingFunction, completion: { [weak sourceNode] _ in + + sourceNode.backgroundImageNode.layer.animatePosition(from: startContainerBackgroundPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak sourceNode] _ in if let sourceNode = sourceNode { sourceNode.backgroundImageNode.alpha = 1.0 sourceNode.borderImageNode.alpha = 1.0 @@ -698,7 +697,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } }) - sourceNode.contentWrapperNode.layer.animatePosition(from: startContainerContentPosition, to: targetContainerAvatarPosition, duration: 0.2, timingFunction: timingFunction, completion: { [weak sourceNode] _ in + sourceNode.contentWrapperNode.layer.animatePosition(from: startContainerContentPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak sourceNode] _ in if let sourceNode = sourceNode { sourceNode.avatarNode.alpha = 1.0 sourceNode.contentWrapperNode.position = initialContentPosition @@ -709,19 +708,18 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { self.avatarNode.position = targetContainerAvatarPosition containerNode.addSubnode(self.avatarNode) - self.avatarNode.layer.animateScale(from: 1.25, to: 1.0, duration: 0.2, timingFunction: timingFunction) - - self.avatarNode.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: 0.2, timingFunction: timingFunction, completion: { [weak self] _ in + self.avatarNode.layer.animateScale(from: 1.25, to: 1.0, duration: duration, timingFunction: timingFunction) + self.avatarNode.layer.animatePosition(from: startContainerAvatarPosition, to: targetContainerAvatarPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self] _ in if let strongSelf = self { strongSelf.avatarNode.position = initialAvatarPosition strongSelf.offsetContainerNode.addSubnode(strongSelf.avatarNode) } }) - sourceNode.backgroundImageNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, timingFunction: timingFunction) - sourceNode.backgroundImageNode.layer.animateAlpha(from: sourceNode.backgroundImageNode.alpha, to: 0.0, duration: 0.35, timingFunction: timingFunction) - sourceNode.contentWrapperNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, timingFunction: timingFunction) - sourceNode.contentWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, timingFunction: timingFunction) + sourceNode.backgroundImageNode.layer.animateScale(from: 1.0, to: 0.001, duration: duration, timingFunction: timingFunction) + sourceNode.backgroundImageNode.layer.animateAlpha(from: sourceNode.backgroundImageNode.alpha, to: 0.0, duration: duration, timingFunction: timingFunction) + sourceNode.contentWrapperNode.layer.animateScale(from: 1.0, to: 0.001, duration: duration, timingFunction: timingFunction) + sourceNode.contentWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: timingFunction) } } @@ -819,7 +817,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { 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 (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, false) + let (expandedStatusLayout, expandedStatusApply) = makeExpandedStatusLayout(CGSize(width: params.width - leftInset - 8.0 - rightInset - expandedRightInset, height: CGFloat.greatestFiniteMagnitude), item.expandedText ?? item.text, true) let titleSpacing: CGFloat = statusLayout.height == 0.0 ? 0.0 : 1.0 @@ -861,7 +859,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { animationFrame = CGRect(x: params.width - animationSize.width - 6.0 - params.rightInset, y: floor((layout.contentSize.height - animationSize.height) / 2.0) + 1.0, width: animationSize.width, height: animationSize.height) titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + verticalOffset), size: titleLayout.size) - var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + var extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: params.leftInset, dy: 0.0) var extractedHeight = extractedRect.height + expandedStatusLayout.height - statusLayout.height var extractedVerticalOffset: CGFloat = 0.0 if item.peer.smallProfileImage != nil { @@ -996,7 +994,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { return } - if false, strongSelf.audioLevelView == nil, value > 0.0 { + if strongSelf.audioLevelView == nil, value > 0.0 { let audioLevelView = VoiceBlobView( frame: blobFrame, maxLevel: 1.5, @@ -1034,7 +1032,6 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { audioLevelView.setColor(wavesColor, animated: true) } } else { - audioLevelView.stopAnimating(duration: 0.5) avatarScale = 1.0 } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift index c07919819a..96d5e7e813 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPeerProfileNode.swift @@ -1,8 +1,341 @@ -// -// VoiceChatPeerProfileNode.swift -// _idx_TelegramCallsUI_5BDA0798_ios_min9.0 -// -// Created by Ilya Laktyushin on 11.05.2021. -// - import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TelegramPresentationData +import TelegramUIPreferences +import PresentationDataUtils +import AvatarNode +import TelegramStringFormatting +import ContextUI +import AccountContext +import LegacyComponents +import PeerInfoAvatarListNode + +private let backgroundCornerRadius: CGFloat = 14.0 + +final class VoiceChatPeerProfileNode: ASDisplayNode { + private let context: AccountContext + private let size: CGSize + private var peer: Peer + private var text: VoiceChatParticipantItem.ParticipantText + private let customNode: ASDisplayNode? + private let additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError> + + private let backgroundImageNode: ASImageNode + private let avatarListContainerNode: ASDisplayNode + let avatarListWrapperNode: PinchSourceContainerNode + let avatarListNode: PeerInfoAvatarListContainerNode + private var videoFadeNode: ASImageNode + private let infoNode: ASDisplayNode + private let titleNode: ImmediateTextNode + private let statusNode: VoiceChatParticipantStatusNode + + private var videoNode: GroupVideoNode? + + private var appeared = false + + init(context: AccountContext, size: CGSize, peer: Peer, text: VoiceChatParticipantItem.ParticipantText, customNode: ASDisplayNode? = nil, additionalEntry: Signal<(TelegramMediaImageRepresentation, Float)?, NoError>, requestDismiss: (() -> Void)?) { + self.context = context + self.size = size + self.peer = peer + self.text = text + self.customNode = customNode + self.additionalEntry = additionalEntry + + self.backgroundImageNode = ASImageNode() + self.backgroundImageNode.clipsToBounds = true + self.backgroundImageNode.displaysAsynchronously = false + self.backgroundImageNode.displayWithoutProcessing = true + + self.videoFadeNode = ASImageNode() + self.videoFadeNode.displaysAsynchronously = false + self.videoFadeNode.contentMode = .scaleToFill + + self.avatarListContainerNode = ASDisplayNode() + self.avatarListContainerNode.clipsToBounds = true + + self.avatarListWrapperNode = PinchSourceContainerNode() + self.avatarListWrapperNode.clipsToBounds = true + self.avatarListWrapperNode.cornerRadius = backgroundCornerRadius + + self.avatarListNode = PeerInfoAvatarListContainerNode(context: context) + self.avatarListNode.backgroundColor = .clear + self.avatarListNode.peer = peer + self.avatarListNode.firstFullSizeOnly = true + self.avatarListNode.offsetLocation = true + self.avatarListNode.customCenterTapAction = { + requestDismiss?() + } + + self.infoNode = ASDisplayNode() + + self.titleNode = ImmediateTextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.contentMode = .left + self.titleNode.contentsScale = UIScreen.main.scale + + self.statusNode = VoiceChatParticipantStatusNode() + self.statusNode.isUserInteractionEnabled = false + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.backgroundImageNode) + self.addSubnode(self.infoNode) + self.addSubnode(self.videoFadeNode) + self.addSubnode(self.avatarListWrapperNode) + self.infoNode.addSubnode(self.titleNode) + self.infoNode.addSubnode(self.statusNode) + + self.avatarListContainerNode.addSubnode(self.avatarListNode) + self.avatarListContainerNode.addSubnode(self.avatarListNode.controlsClippingOffsetNode) + self.avatarListWrapperNode.contentNode.addSubnode(self.avatarListContainerNode) + + self.avatarListWrapperNode.activate = { [weak self] sourceNode in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.controlsContainerNode.alpha = 0.0 + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + return UIScreen.main.bounds + }) + context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController) + } + self.avatarListWrapperNode.deactivated = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListWrapperNode.contentNode.layer.animate(from: 0.0 as NSNumber, to: backgroundCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3, completion: { _ in + }) + } + self.avatarListWrapperNode.animatedOut = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.avatarListNode.controlsContainerNode.alpha = 1.0 + strongSelf.avatarListNode.controlsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + + self.updateInfo(size: size, animate: false) + } + + func updateInfo(size: CGSize, animate: Bool) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let titleFont = Font.regular(17.0) + let titleColor = UIColor.white + var titleAttributedString: NSAttributedString? + if let user = self.peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + switch presentationData.nameDisplayOrder { + case .firstLast: + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)) + case .lastFirst: + string.append(NSAttributedString(string: lastName, font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: titleColor)) + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: titleColor)) + } + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleFont, textColor: titleColor) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleFont, textColor: titleColor) + } else { + titleAttributedString = NSAttributedString(string: presentationData.strings.User_DeletedAccount, font: titleFont, textColor: titleColor) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: titleColor) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: titleColor) + } + self.titleNode.attributedText = titleAttributedString + + let titleSize = self.titleNode.updateLayout(CGSize(width: self.size.width - 24.0, height: size.height)) + + let makeStatusLayout = self.statusNode.asyncLayout() + let (statusLayout, statusApply) = makeStatusLayout(CGSize(width: self.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude), self.text, true) + let _ = statusApply() + + self.titleNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: titleSize) + self.statusNode.frame = CGRect(origin: CGPoint(x: 14.0, y: titleSize.height + 3.0), size: statusLayout) + + let totalHeight = titleSize.height + statusLayout.height + 3.0 + 8.0 + let infoFrame = CGRect(x: 0.0, y: size.height - totalHeight, width: self.size.width, height: totalHeight) + + if animate { + let springDuration: Double = !self.appeared ? 0.42 : 0.3 + let springDamping: CGFloat = !self.appeared ? 104.0 : 1000.0 + + let initialInfoPosition = self.infoNode.position + self.infoNode.layer.position = infoFrame.center + let initialInfoBounds = self.infoNode.bounds + self.infoNode.layer.bounds = CGRect(origin: CGPoint(), size: infoFrame.size) + + self.infoNode.layer.animateSpring(from: NSValue(cgPoint: initialInfoPosition), to: NSValue(cgPoint: self.infoNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.infoNode.layer.animateSpring(from: NSValue(cgRect: initialInfoBounds), to: NSValue(cgRect: self.infoNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else { + self.infoNode.frame = infoFrame + } + } + + func animateIn(from sourceNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition) { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let springDuration: Double = 0.42 + let springDamping: CGFloat = 104.0 + + if let sourceNode = sourceNode as? VoiceChatTileItemNode { + let sourceRect = sourceNode.bounds + self.backgroundImageNode.frame = sourceNode.bounds + self.updateInfo(size: sourceNode.bounds.size, animate: false) + self.updateInfo(size: targetRect.size, animate: true) + + self.backgroundImageNode.image = generateImage(CGSize(width: backgroundCornerRadius * 2.0, height: backgroundCornerRadius * 2.0), rotatedContext: { (size, context) in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor) + context.fillEllipse(in: bounds) + context.fill(CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height / 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(backgroundCornerRadius), topCapHeight: Int(backgroundCornerRadius)) + self.backgroundImageNode.cornerRadius = backgroundCornerRadius + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: 0.0) + + let initialRect = sourceNode.frame + let initialScale: CGFloat = sourceRect.width / targetRect.width + + let targetSize = CGSize(width: targetRect.size.width, height: targetRect.size.width) + self.avatarListWrapperNode.update(size: targetSize, transition: .immediate) + self.avatarListWrapperNode.frame = CGRect(x: targetRect.minX, y: targetRect.minY, width: targetRect.width, height: targetRect.width + backgroundCornerRadius) + + self.avatarListContainerNode.frame = CGRect(origin: CGPoint(), size: targetSize) + self.avatarListContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.avatarListContainerNode.cornerRadius = targetRect.width / 2.0 + + if let videoNode = sourceNode.videoNode { + videoNode.updateLayout(size: targetSize, isLandscape: true, transition: transition) + transition.updateFrame(node: videoNode, frame: CGRect(origin: CGPoint(), size: targetSize)) + transition.updateFrame(node: sourceNode.videoContainerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: targetSize.width, height: targetSize.height + backgroundCornerRadius))) + sourceNode.videoContainerNode.cornerRadius = backgroundCornerRadius + } + self.insertSubnode(sourceNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + + if let snapshotView = sourceNode.infoNode.view.snapshotView(afterScreenUpdates: false) { + self.videoFadeNode.image = sourceNode.fadeNode.image + self.videoFadeNode.frame = CGRect(x: 0.0, y: sourceRect.height - sourceNode.fadeNode.frame.height, width: sourceRect.width, height: sourceNode.fadeNode.frame.height) + + self.insertSubnode(self.videoFadeNode, aboveSubnode: sourceNode.videoContainerNode) + self.view.insertSubview(snapshotView, aboveSubview: sourceNode.videoContainerNode.view) + snapshotView.frame = sourceRect + transition.updateFrame(view: snapshotView, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - snapshotView.frame.size.height), size: snapshotView.frame.size)) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetSize.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetSize.width, height: self.videoFadeNode.frame.height))) + self.videoFadeNode.alpha = 0.0 + self.videoFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + + self.avatarListWrapperNode.layer.animateSpring(from: initialScale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + self.avatarListWrapperNode.layer.animateSpring(from: NSValue(cgPoint: initialRect.center), to: NSValue(cgPoint: self.avatarListWrapperNode.position), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.avatarListNode.currentItemNode?.addSubnode(sourceNode.videoContainerNode) + } + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: 0.0) + + self.avatarListWrapperNode.contentNode.clipsToBounds = true + + self.avatarListNode.frame = CGRect(x: targetRect.width / 2.0, y: targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingNode.frame = CGRect(x: -targetRect.width / 2.0, y: -targetRect.width / 2.0, width: targetRect.width, height: targetRect.width) + self.avatarListNode.controlsClippingOffsetNode.frame = CGRect(origin: CGPoint(x: targetRect.width / 2.0, y: targetRect.width / 2.0), size: CGSize()) + self.avatarListNode.stripContainerNode.frame = CGRect(x: 0.0, y: 13.0, width: targetRect.width, height: 2.0) + + self.avatarListNode.update(size: targetSize, peer: self.peer, customNode: self.customNode, additionalEntry: self.additionalEntry, isExpanded: true, transition: .immediate) + + let backgroundTargetRect = CGRect(x: 0.0, y: targetSize.height - backgroundCornerRadius * 2.0, width: targetRect.width, height: targetRect.height - targetSize.height + backgroundCornerRadius * 2.0) + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + } else if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode { + + } + self.appeared = true + } + + func animateOut(to targetNode: ASDisplayNode, targetRect: CGRect, transition: ContainedViewLayoutTransition) { + let radiusTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + let springDuration: Double = 0.3 + let springDamping: CGFloat = 1000.0 + if let targetNode = targetNode as? VoiceChatTileItemNode { + let initialSize = self.bounds + self.updateInfo(size: targetRect.size, animate: true) + + transition.updateCornerRadius(node: self.backgroundImageNode, cornerRadius: backgroundCornerRadius) + + let targetScale = targetRect.width / avatarListContainerNode.frame.width + + self.insertSubnode(targetNode.videoContainerNode, belowSubnode: self.avatarListWrapperNode) + self.insertSubnode(self.videoFadeNode, aboveSubnode: targetNode.videoContainerNode) + self.avatarListWrapperNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + self.avatarListWrapperNode.layer.animate(from: 1.0 as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) + self.avatarListWrapperNode.layer.animate(from: NSValue(cgPoint: self.avatarListWrapperNode.position), to: NSValue(cgPoint: targetRect.center), keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false, completion: { [weak self, weak targetNode] _ in + if let targetNode = targetNode { + targetNode.contentNode.insertSubnode(targetNode.videoContainerNode, aboveSubnode: targetNode.backgroundNode) + } + self?.removeFromSupernode() + }) + + radiusTransition.updateCornerRadius(node: self.avatarListContainerNode, cornerRadius: backgroundCornerRadius) + + if let snapshotView = targetNode.infoNode.view.snapshotView(afterScreenUpdates: false) { + self.view.insertSubview(snapshotView, aboveSubview: targetNode.videoContainerNode.view) + let snapshotFrame = snapshotView.frame + snapshotView.frame = CGRect(origin: CGPoint(x: 0.0, y: initialSize.width - snapshotView.frame.size.height), size: snapshotView.frame.size) + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + transition.updateFrame(node: self.videoFadeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetRect.height - self.videoFadeNode.frame.size.height), size: CGSize(width: targetRect.width, height: self.videoFadeNode.frame.height))) + self.videoFadeNode.alpha = 1.0 + self.videoFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + if let videoNode = targetNode.videoNode { + videoNode.updateLayout(size: targetRect.size, isLandscape: true, transition: transition) + transition.updateFrame(node: videoNode, frame: targetRect) + transition.updateFrame(node: targetNode.videoContainerNode, frame: targetRect) + } + + let backgroundTargetRect = targetRect + let initialBackgroundPosition = self.backgroundImageNode.position + self.backgroundImageNode.layer.position = backgroundTargetRect.center + let initialBackgroundBounds = self.backgroundImageNode.bounds + self.backgroundImageNode.layer.bounds = CGRect(origin: CGPoint(), size: backgroundTargetRect.size) + + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgPoint: initialBackgroundPosition), to: NSValue(cgPoint: self.backgroundImageNode.position), keyPath: "position", duration: springDuration, delay: 0.0, initialVelocity: 0.0, damping: springDamping) + self.backgroundImageNode.layer.animateSpring(from: NSValue(cgRect: initialBackgroundBounds), to: NSValue(cgRect: self.backgroundImageNode.bounds), keyPath: "bounds", duration: springDuration, initialVelocity: 0.0, damping: springDamping) + + self.avatarListNode.stripContainerNode.alpha = 0.0 + self.avatarListNode.stripContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + + self.infoNode.alpha = 0.0 + self.infoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } else if let targetNode = targetNode as? VoiceChatFullscreenParticipantItemNode { + + } + } +} diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift index dc51ff4a7b..c45ac1049c 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileGridNode.swift @@ -48,12 +48,12 @@ final class VoiceChatTileGridNode: ASDisplayNode { var wasAdded = false if let current = self.itemNodes[item.id] { itemNode = current - current.update(size: itemSize, item: item, transition: transition) + current.update(size: itemSize, availableWidth: size.width, item: item, transition: transition) } else { wasAdded = true let addedItemNode = VoiceChatTileItemNode(context: self.context) itemNode = addedItemNode - addedItemNode.update(size: itemSize, item: item, transition: .immediate) + addedItemNode.update(size: itemSize, availableWidth: size.width, item: item, transition: .immediate) self.itemNodes[self.items[i].id] = addedItemNode self.addSubnode(addedItemNode) } @@ -197,15 +197,12 @@ final class VoiceChatTilesGridItemNode: ListViewItemNode { strongSelf.tileGridNode = tileGridNode } - let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.3, curve: .spring) + let transition: ContainedViewLayoutTransition = currentItem == nil ? .immediate : .animated(duration: 0.4, curve: .spring) let tileGridSize = tileGridNode.update(size: CGSize(width: params.width - params.leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), items: item.tiles, transition: transition) if currentItem == nil { let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut) tileGridNode.frame = CGRect(x: params.leftInset, y: 0.0, width: tileGridSize.width, height: 0.0) -// transition.updateFrame(node: tileGridNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) - strongSelf.backgroundNode.frame = tileGridNode.frame -// transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) } else { transition.updateFrame(node: tileGridNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: tileGridSize)) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift index 9f915e4b43..8da3e5ffcb 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatTileItemNode.swift @@ -35,11 +35,13 @@ final class VoiceChatTileItem: Equatable { let peer: Peer let videoEndpointId: String - let icon: Icon let strings: PresentationStrings let nameDisplayOrder: PresentationPersonNameOrder + let icon: Icon + let text: VoiceChatParticipantItem.ParticipantText let speaking: Bool let action: () -> Void + let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? let getVideo: () -> GroupVideoNode? let getAudioLevel: (() -> Signal)? @@ -47,14 +49,16 @@ final class VoiceChatTileItem: Equatable { return self.videoEndpointId } - init(peer: Peer, videoEndpointId: String, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, icon: Icon, action: @escaping () -> Void, getVideo: @escaping () -> GroupVideoNode?, getAudioLevel: (() -> Signal)?) { + init(peer: Peer, videoEndpointId: String, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, speaking: Bool, icon: Icon, text: VoiceChatParticipantItem.ParticipantText, action: @escaping () -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?, getVideo: @escaping () -> GroupVideoNode?, getAudioLevel: (() -> Signal)?) { self.peer = peer self.videoEndpointId = videoEndpointId self.strings = strings self.nameDisplayOrder = nameDisplayOrder self.icon = icon + self.text = text self.speaking = speaking self.action = action + self.contextAction = contextAction self.getVideo = getVideo self.getAudioLevel = getAudioLevel } @@ -93,16 +97,26 @@ final class VoiceChatTileItemNode: ASDisplayNode { let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode - private let backgroundNode: ASDisplayNode + let contentNode: ASDisplayNode + let backgroundNode: ASDisplayNode + var videoContainerNode: ASDisplayNode var videoNode: GroupVideoNode? - private let fadeNode: ASImageNode + let infoNode: ASDisplayNode + let fadeNode: ASImageNode private let titleNode: ImmediateTextNode private let iconNode: ASImageNode private var animationNode: VoiceChatMicrophoneNode? private var highlightNode: ASImageNode + private let statusNode: VoiceChatParticipantStatusNode - private var validLayout: CGSize? + private var profileNode: VoiceChatPeerProfileNode? + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + private var extractedVerticalOffset: CGFloat? + + private var validLayout: (CGSize, CGFloat)? var item: VoiceChatTileItem? + private var isExtracted = false private let audioLevelDisposable = MetaDisposable() @@ -111,10 +125,19 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() - + + self.contentNode = ASDisplayNode() + self.contentNode.clipsToBounds = true + self.contentNode.cornerRadius = 11.0 + self.backgroundNode = ASDisplayNode() self.backgroundNode.backgroundColor = panelBackgroundColor + self.videoContainerNode = ASDisplayNode() + self.videoContainerNode.clipsToBounds = true + + self.infoNode = ASDisplayNode() + self.fadeNode = ASImageNode() self.fadeNode.displaysAsynchronously = false self.fadeNode.displayWithoutProcessing = true @@ -122,6 +145,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.fadeNode.image = fadeImage self.titleNode = ImmediateTextNode() + self.statusNode = VoiceChatParticipantStatusNode() self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false @@ -136,18 +160,38 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.clipsToBounds = true - self.contextSourceNode.contentNode.clipsToBounds = true - self.contextSourceNode.contentNode.cornerRadius = 11.0 - self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode self.addSubnode(self.containerNode) - self.contextSourceNode.contentNode.addSubnode(self.backgroundNode) - self.contextSourceNode.contentNode.addSubnode(self.fadeNode) - self.contextSourceNode.contentNode.addSubnode(self.titleNode) - self.contextSourceNode.contentNode.addSubnode(self.iconNode) - self.contextSourceNode.contentNode.addSubnode(self.highlightNode) + self.contextSourceNode.contentNode.addSubnode(self.contentNode) + self.contentNode.addSubnode(self.backgroundNode) + self.contentNode.addSubnode(self.videoContainerNode) + self.contentNode.addSubnode(self.fadeNode) + self.contentNode.addSubnode(self.infoNode) + self.infoNode.addSubnode(self.titleNode) + self.infoNode.addSubnode(self.iconNode) + self.contentNode.addSubnode(self.highlightNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let _ = self else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else { + gesture.cancel() + return + } + contextAction(strongSelf.contextSourceNode, gesture) + } + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let _ = strongSelf.item else { + return + } + strongSelf.updateIsExtracted(isExtracted, transition: transition) + } } deinit { @@ -164,10 +208,44 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.item?.action() } - func update(size: CGSize, item: VoiceChatTileItem, transition: ContainedViewLayoutTransition) { - guard self.validLayout != size || self.item != item else { + private func updateIsExtracted(_ isExtracted: Bool, transition: ContainedViewLayoutTransition) { + guard self.isExtracted != isExtracted, let extractedRect = self.extractedRect, let nonExtractedRect = self.nonExtractedRect, let item = self.item else { return } + self.isExtracted = isExtracted + + if isExtracted { + let profileNode = VoiceChatPeerProfileNode(context: self.context, size: extractedRect.size, peer: item.peer, text: item.text, customNode: self.videoContainerNode, additionalEntry: .single(nil), requestDismiss: { [weak self] in + self?.contextSourceNode.requestDismiss?() + }) + profileNode.frame = CGRect(origin: CGPoint(), size: extractedRect.size) + self.profileNode = profileNode + self.contextSourceNode.contentNode.addSubnode(profileNode) + + profileNode.animateIn(from: self, targetRect: extractedRect, transition: transition) + + self.contextSourceNode.contentNode.customHitTest = { [weak self] point in + if let strongSelf = self, let profileNode = strongSelf.profileNode { + if profileNode.avatarListWrapperNode.frame.contains(point) { + return profileNode.avatarListNode.view + } + } + return nil + } + } else if let profileNode = self.profileNode { + self.profileNode = nil + profileNode.animateOut(to: self, targetRect: nonExtractedRect, transition: transition) + + self.contextSourceNode.contentNode.customHitTest = nil + } + } + + func update(size: CGSize, availableWidth: CGFloat, item: VoiceChatTileItem, transition: ContainedViewLayoutTransition) { + guard self.validLayout?.0 != size || self.validLayout?.1 != availableWidth || self.item != item else { + return + } + + self.validLayout = (size, availableWidth) var itemTransition = transition if self.item != item { @@ -206,7 +284,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { if let videoNode = item.getVideo() { itemTransition = .immediate self.videoNode = videoNode - self.contextSourceNode.contentNode.insertSubnode(videoNode, at: 1) + self.videoContainerNode.addSubnode(videoNode) } } @@ -248,7 +326,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { } else { animationNode = VoiceChatMicrophoneNode() self.animationNode = animationNode - self.contextSourceNode.contentNode.addSubnode(animationNode) + self.infoNode.addSubnode(animationNode) } animationNode.alpha = 1.0 animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: UIColor.white), animated: true) @@ -259,34 +337,56 @@ final class VoiceChatTileItemNode: ASDisplayNode { } let bounds = CGRect(origin: CGPoint(), size: size) + self.contentNode.frame = bounds self.containerNode.frame = bounds self.contextSourceNode.frame = bounds self.contextSourceNode.contentNode.frame = bounds - if let videoNode = self.videoNode { - transition.updateFrame(node: videoNode, frame: bounds) - videoNode.updateLayout(size: size, isLandscape: true, transition: itemTransition) + let extractedWidth = availableWidth + let makeStatusLayout = self.statusNode.asyncLayout() + let (statusLayout, _) = makeStatusLayout(CGSize(width: availableWidth - 30.0, height: CGFloat.greatestFiniteMagnitude), item.text, true) + + let extractedRect = CGRect(x: 0.0, y: 0.0, width: extractedWidth, height: extractedWidth + statusLayout.height + 39.0) + let nonExtractedRect = bounds + self.extractedRect = extractedRect + self.nonExtractedRect = nonExtractedRect + + self.contextSourceNode.contentRect = extractedRect + + if self.videoContainerNode.supernode === self.contentNode { + if let videoNode = self.videoNode { + transition.updateFrame(node: videoNode, frame: bounds) + videoNode.updateLayout(size: size, isLandscape: true, transition: itemTransition) + } + transition.updateFrame(node: self.videoContainerNode, frame: bounds) } transition.updateFrame(node: self.backgroundNode, frame: bounds) transition.updateFrame(node: self.highlightNode, frame: bounds) + 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)) let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 50.0, height: size.height)) - self.titleNode.frame = CGRect(origin: CGPoint(x: 11.0, y: size.height - titleSize.height - 8.0), size: titleSize) + self.titleNode.frame = CGRect(origin: CGPoint(x: 30.0, y: size.height - titleSize.height - 8.0), size: titleSize) if let animationNode = self.animationNode { let animationSize = CGSize(width: 36.0, height: 36.0) animationNode.bounds = CGRect(origin: CGPoint(), size: animationSize) animationNode.transform = CATransform3DMakeScale(0.66667, 0.66667, 1.0) - transition.updatePosition(node: animationNode, position: CGPoint(x: size.width - 19.0, y: size.height - 15.0)) + transition.updatePosition(node: animationNode, position: CGPoint(x: 16.0, y: size.height - 15.0)) } } - func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, animate: Bool = true) { + func animateTransitionIn(from sourceNode: ASDisplayNode, containerNode: ASDisplayNode, transition: ContainedViewLayoutTransition, animate: Bool = true) { guard let _ = self.item else { return } + var duration: Double = 0.2 + var timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue + if case let .animated(transitionDuration, curve) = transition { + duration = transitionDuration + timingFunction = curve.timingFunction + } if let sourceNode = sourceNode as? VoiceChatFullscreenParticipantItemNode, let _ = sourceNode.item { let initialAnimate = animate @@ -301,7 +401,7 @@ final class VoiceChatTileItemNode: ASDisplayNode { sourceNode.videoNode = nil videoNode.alpha = 1.0 self.videoNode = videoNode - self.contextSourceNode.contentNode.insertSubnode(videoNode, at: 1) + self.videoContainerNode.addSubnode(videoNode) if animate { // self.videoContainerNode.layer.animateScale(from: sourceNode.bounds.width / videoSize.width, to: tileSize.width / videoSize.width, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) @@ -322,15 +422,14 @@ final class VoiceChatTileItemNode: ASDisplayNode { self.contextSourceNode.position = targetContainerPosition containerNode.addSubnode(self.contextSourceNode) - self.contextSourceNode.layer.animateScale(from: 0.467, to: 1.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in + self.contextSourceNode.layer.animateScale(from: 0.467, to: 1.0, duration: duration, timingFunction: timingFunction) + self.contextSourceNode.layer.animatePosition(from: startContainerPosition, to: targetContainerPosition, duration: duration, timingFunction: timingFunction, completion: { [weak self] _ in if let strongSelf = self { strongSelf.contextSourceNode.position = initialPosition strongSelf.containerNode.addSubnode(strongSelf.contextSourceNode) } }) - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) self.videoNode?.updateLayout(size: self.bounds.size, isLandscape: true, transition: transition) self.videoNode?.frame = self.bounds } else if !initialAnimate { diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json new file mode 100644 index 0000000000..ed8e0a0bf5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf new file mode 100644 index 0000000000..b9d4123329 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Pin.imageset/pin.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json new file mode 100644 index 0000000000..9e8d864ed6 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unpin.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf new file mode 100644 index 0000000000..fd3566cc37 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Call/Unpin.imageset/unpin.pdf differ