diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index b5771d1346..79f36bd3de 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -66,14 +66,6 @@ final class CameraDeviceContext { self.output.configureVideoStabilization() } - func switchOutputWith(_ otherContext: CameraDeviceContext) { -// guard let session = self.session else { -// return -// } -// self.output.reconfigure(for: session, device: self.device, input: self.input, otherPreviewView: otherContext.previewView, otherOutput: otherContext.output) -// otherContext.output.reconfigure(for: session, device: otherContext.device, input: otherContext.input, otherPreviewView: self.previewView, otherOutput: self.output) - } - func invalidate() { guard let session = self.session else { return @@ -142,7 +134,7 @@ private final class CameraContext { private var lastSnapshotTimestamp: Double = CACurrentMediaTime() private var lastAdditionalSnapshotTimestamp: Double = CACurrentMediaTime() - private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool, additional: Bool) { + private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) { Queue.concurrentDefaultQueue().async { var ciImage = CIImage(cvImageBuffer: pixelBuffer) let size = ciImage.extent.size @@ -154,10 +146,10 @@ private final class CameraContext { ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) - if additional { - CameraSimplePreviewView.saveAdditionalLastStateImage(uiImage) + if mirror { + CameraSimplePreviewView.saveLastFrontImage(uiImage) } else { - CameraSimplePreviewView.saveLastStateImage(uiImage) + CameraSimplePreviewView.saveLastBackImage(uiImage) } } } @@ -171,6 +163,8 @@ private final class CameraContext { self.simplePreviewView = previewView self.secondaryPreviewView = secondaryPreviewView + self.dualPosition = configuration.position + self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true, additional: false) self.configure { self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata) @@ -188,7 +182,7 @@ private final class CameraContext { if #available(iOS 13.0, *) { mirror = connection.inputPorts.first?.sourceDevicePosition == .front } - self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) self.lastSnapshotTimestamp = timestamp } } @@ -256,17 +250,19 @@ private final class CameraContext { return self._positionPromise.get() } - private var tmpPosition: Camera.Position = .back + private var dualPosition: Camera.Position = .back func togglePosition() { if self.isDualCamEnabled { -// let targetPosition: Camera.Position -// if case .back = self.tmpPosition { -// targetPosition = .front -// } else { -// targetPosition = .back -// } -// self.tmpPosition = targetPosition -// self._positionPromise.set(targetPosition) + let targetPosition: Camera.Position + if case .back = self.dualPosition { + targetPosition = .front + } else { + targetPosition = .back + } + self.dualPosition = targetPosition + self._positionPromise.set(targetPosition) + + self.mainDeviceContext.output.markPositionChange(position: targetPosition) } else { self.configure { self.mainDeviceContext.invalidate() @@ -277,6 +273,7 @@ private final class CameraContext { } else { targetPosition = .back } + self.dualPosition = targetPosition self._positionPromise.set(targetPosition) self.modeChange = .position @@ -294,6 +291,7 @@ private final class CameraContext { self.mainDeviceContext.invalidate() self._positionPromise.set(position) + self.dualPosition = position self.modeChange = .position self.mainDeviceContext.configure(position: position, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) @@ -333,7 +331,7 @@ private final class CameraContext { if #available(iOS 13.0, *) { mirror = connection.inputPorts.first?.sourceDevicePosition == .front } - self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) self.lastSnapshotTimestamp = timestamp } } @@ -347,7 +345,7 @@ private final class CameraContext { if #available(iOS 13.0, *) { mirror = connection.inputPorts.first?.sourceDevicePosition == .front } - self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: true) + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) self.lastAdditionalSnapshotTimestamp = timestamp } } @@ -358,7 +356,7 @@ private final class CameraContext { self.additionalDeviceContext = nil self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false) - self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) + self.mainDeviceContext.configure(position: self.dualPosition, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) } self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self else { @@ -372,7 +370,7 @@ private final class CameraContext { if #available(iOS 13.0, *) { mirror = connection.inputPorts.first?.sourceDevicePosition == .front } - self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) self.lastSnapshotTimestamp = timestamp } } @@ -448,12 +446,17 @@ private final class CameraContext { func takePhoto() -> Signal { let orientation = self.videoOrientation ?? .portrait if let additionalDeviceContext = self.additionalDeviceContext { + let dualPosition = self.dualPosition return combineLatest( self.mainDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode), additionalDeviceContext.output.takePhoto(orientation: orientation, flashMode: self._flashMode) ) |> map { main, additional in if case let .finished(mainImage, _, _) = main, case let .finished(additionalImage, _, _) = additional { - return .finished(mainImage, additionalImage, CACurrentMediaTime()) + if dualPosition == .front { + return .finished(additionalImage, mainImage, CACurrentMediaTime()) + } else { + return .finished(mainImage, additionalImage, CACurrentMediaTime()) + } } else { return .began } @@ -482,8 +485,8 @@ private final class CameraContext { self.mainDeviceContext.output.stopRecording(), additionalDeviceContext.output.stopRecording() ) |> mapToSignal { main, additional in - if case let .finished(mainResult, _, _) = main, case let .finished(additionalResult, _, _) = additional { - return .single(.finished(mainResult, additionalResult, CACurrentMediaTime())) + if case let .finished(mainResult, _, duration, positionChangeTimestamps, _) = main, case let .finished(additionalResult, _, _, _, _) = additional { + return .single(.finished(mainResult, additionalResult, duration, positionChangeTimestamps, CACurrentMediaTime())) } else { return .complete() } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index ee623b2c4f..f77f6d97a0 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -7,7 +7,7 @@ import Vision import VideoToolbox public enum VideoCaptureResult: Equatable { - case finished((String, UIImage), (String, UIImage)?, Double) + case finished((String, UIImage), (String, UIImage)?, Double, [(Bool, Double)], Double) case failed public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool { @@ -18,8 +18,11 @@ public enum VideoCaptureResult: Equatable { } else { return false } - case let .finished(_, _, lhsTime): - if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime { + case let .finished(_, _, lhsDuration, lhsChangeTimestamps, lhsTime): + if case let .finished(_, _, rhsDuration, rhsChangeTimestamps, rhsTime) = rhs, lhsDuration == rhsDuration, lhsTime == rhsTime { + if lhsChangeTimestamps.count != rhsChangeTimestamps.count { + return false + } return true } else { return false @@ -85,6 +88,7 @@ final class CameraOutput: NSObject { private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:] private var videoRecorder: VideoRecorder? + weak var overrideOutput: CameraOutput? var activeFilter: CameraFilter? var faceLandmarks: Bool = false @@ -333,8 +337,8 @@ final class CameraOutput: NSObject { let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFileURL = URL(fileURLWithPath: outputFilePath) let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in - if case let .success(transitionImage) = result { - self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, CACurrentMediaTime())) + if case let .success(transitionImage, duration, positionChangeTimestamps) = result { + self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, duration, positionChangeTimestamps.map { ($0 == .front, $1) }, CACurrentMediaTime())) } else { self?.recordingCompletionPipe.putNext(.failed) } @@ -364,6 +368,12 @@ final class CameraOutput: NSObject { self.videoRecorder = nil } } + + func markPositionChange(position: Camera.Position) { + if let videoRecorder = self.videoRecorder { + videoRecorder.markPositionChange(position: position) + } + } } extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate { diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 66ce27f886..68ecdd4f8d 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -10,8 +10,8 @@ import Vision import ImageBlur public class CameraSimplePreviewView: UIView { - static func lastStateImage() -> UIImage { - let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" + static func lastBackImage() -> UIImage { + let imagePath = NSTemporaryDirectory() + "backCameraImage.jpg" if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) { return image } else { @@ -19,15 +19,15 @@ public class CameraSimplePreviewView: UIView { } } - static func saveLastStateImage(_ image: UIImage) { - let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" + static func saveLastBackImage(_ image: UIImage) { + let imagePath = NSTemporaryDirectory() + "backCameraImage.jpg" if let data = image.jpegData(compressionQuality: 0.6) { try? data.write(to: URL(fileURLWithPath: imagePath)) } } - static func lastAdditionalStateImage() -> UIImage { - let imagePath = NSTemporaryDirectory() + "cameraImage2.jpg" + static func lastFrontImage() -> UIImage { + let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg" if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)), let image = UIImage(data: data) { return image } else { @@ -35,30 +35,25 @@ public class CameraSimplePreviewView: UIView { } } - static func saveAdditionalLastStateImage(_ image: UIImage) { - let imagePath = NSTemporaryDirectory() + "cameraImage2.jpg" + static func saveLastFrontImage(_ image: UIImage) { + let imagePath = NSTemporaryDirectory() + "frontCameraImage.jpg" if let data = image.jpegData(compressionQuality: 0.6) { try? data.write(to: URL(fileURLWithPath: imagePath)) } } - - private let additional: Bool - + private var previewingDisposable: Disposable? private let placeholderView = UIImageView() - public init(frame: CGRect, additional: Bool) { - self.additional = additional - + public init(frame: CGRect, main: Bool) { super.init(frame: frame) self.videoPreviewLayer.videoGravity = .resizeAspectFill self.placeholderView.contentMode = .scaleAspectFill - self.placeholderView.image = additional ? CameraSimplePreviewView.lastAdditionalStateImage() : CameraSimplePreviewView.lastStateImage() self.addSubview(self.placeholderView) - if !additional { + if main { if #available(iOS 13.0, *) { self.previewingDisposable = (self.isPreviewing |> filter { $0 } @@ -94,14 +89,11 @@ public class CameraSimplePreviewView: UIView { } } - public func resetPlaceholder() { - guard self.placeholderView.alpha == 0.0 else { - return - } - self.placeholderView.image = self.additional ? CameraSimplePreviewView.lastAdditionalStateImage() : CameraSimplePreviewView.lastStateImage() + public func resetPlaceholder(front: Bool) { + self.placeholderView.image = front ? CameraSimplePreviewView.lastFrontImage() : CameraSimplePreviewView.lastBackImage() self.placeholderView.alpha = 1.0 } - + private var _videoPreviewLayer: AVCaptureVideoPreviewLayer? var videoPreviewLayer: AVCaptureVideoPreviewLayer { if let layer = self._videoPreviewLayer { diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 8c131661e7..0847574472 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -49,10 +49,12 @@ private final class VideoRecorderImpl { private var recordingStartSampleTime: CMTime = .invalid private var recordingStopSampleTime: CMTime = .invalid + private var positionChangeTimestamps: [(Camera.Position, CMTime)] = [] + private let configuration: VideoRecorder.Configuration private let videoTransform: CGAffineTransform private let url: URL - fileprivate var completion: (Bool, UIImage?) -> Void = { _, _ in } + fileprivate var completion: (Bool, UIImage?, [(Camera.Position, CMTime)]?) -> Void = { _, _, _ in } private let error = Atomic(value: nil) @@ -83,6 +85,17 @@ private final class VideoRecorderImpl { self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) } } + + public func markPositionChange(position: Camera.Position) { + self.queue.async { + guard self.recordingStartSampleTime.isValid else { + return + } + let currentTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + let delta = currentTime - self.recordingStartSampleTime + self.positionChangeTimestamps.append((position, delta)) + } + } public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) { if let _ = self.hasError() { @@ -291,21 +304,21 @@ private final class VideoRecorderImpl { let completion = self.completion if self.recordingStopSampleTime == .invalid { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } return } if let _ = self.error.with({ $0 }) { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } return } if !self.tryAppendingPendingAudioBuffers() { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } return } @@ -314,21 +327,21 @@ private final class VideoRecorderImpl { self.assetWriter.finishWriting { if let _ = self.assetWriter.error { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } } else { DispatchQueue.main.async { - completion(true, self.transitionImage) + completion(true, self.transitionImage, self.positionChangeTimestamps) } } } } else if let _ = self.assetWriter.error { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } } else { DispatchQueue.main.async { - completion(false, nil) + completion(false, nil, nil) } } } @@ -407,7 +420,7 @@ public final class VideoRecorder { case generic } - case success(UIImage?) + case success(UIImage?, Double, [(Camera.Position, Double)]) case initError(Error) case writeError(Error) case finishError(Error) @@ -448,10 +461,17 @@ public final class VideoRecorder { return nil } self.impl = impl - impl.completion = { [weak self] result, transitionImage in + impl.completion = { [weak self] result, transitionImage, positionChangeTimestamps in if let self { + let duration = self.duration ?? 0.0 if result { - self.completion(.success(transitionImage)) + var timestamps: [(Camera.Position, Double)] = [] + if let positionChangeTimestamps { + for (position, time) in positionChangeTimestamps { + timestamps.append((position, time.seconds)) + } + } + self.completion(.success(transitionImage, duration, timestamps)) } else { self.completion(.finishError(.generic)) } @@ -467,6 +487,10 @@ public final class VideoRecorder { self.impl.stopRecording() } + func markPositionChange(position: Camera.Position) { + self.impl.markPositionChange(position: position) + } + func appendSampleBuffer(_ sampleBuffer: CMSampleBuffer) { guard let formatDescriptor = CMSampleBufferGetFormatDescription(sampleBuffer) else { return diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 23516bfeb4..32a607fe0b 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1126,6 +1126,9 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele guard let strongSelf = self, strongSelf.availableFilters.count > 1 || strongSelf.controller?.isStoryPostingAvailable == true else { return [] } + guard case .chatList(.root) = strongSelf.location else { + return [] + } switch strongSelf.currentItemNode.visibleContentOffset() { case let .known(value): if value < -strongSelf.currentItemNode.tempTopInset { diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 0230a7b3eb..fa96873a56 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1141,7 +1141,7 @@ private final class DrawingScreenComponent: CombinedComponent { } let previewSize: CGSize - let previewTopInset: CGFloat = environment.statusBarHeight + 12.0 + let previewTopInset: CGFloat = environment.statusBarHeight + 5.0 if case .regular = environment.metrics.widthClass { let previewHeight = context.availableSize.height - previewTopInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) diff --git a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift index 58194b2174..2347ee190a 100644 --- a/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift +++ b/submodules/FeaturedStickersScreen/Sources/FeaturedStickersScreen.swift @@ -226,7 +226,11 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { init(context: AccountContext, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { self.context = context - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme = controller.forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } + self.presentationData = presentationData self.controller = controller self.sendSticker = sendSticker diff --git a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift index a2435d4512..fc0526554a 100644 --- a/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift +++ b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift @@ -124,15 +124,24 @@ public final class CameraButton: Component { func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { if let currentId = self.component?.content.id, currentId != component.content.id { - self.contentView.removeFromSuperview() + let previousContentView = self.contentView self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false self.contentView.layer.allowsGroupOpacity = true self.addSubview(self.contentView) + + if transition.animation.isImmediate { + previousContentView.removeFromSuperview() + } else { + self.addSubview(previousContentView) + previousContentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousContentView] _ in + previousContentView?.removeFromSuperview() + }) + } } let contentSize = self.contentView.update( - transition: transition, + transition: .immediate, component: component.content.component, environment: {}, containerSize: availableSize @@ -149,7 +158,7 @@ public final class CameraButton: Component { self.updateScale(transition: transition) self.isEnabled = component.isEnabled - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize), completion: nil) + self.contentView.frame = CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: floor((size.height - contentSize.height) / 2.0)), size: contentSize) return size } diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 8e9359b72a..04fe9cd525 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -75,6 +75,7 @@ swift_library( "//submodules/TelegramUI/Components/MediaEditor", "//submodules/Components/MetalImageView", "//submodules/TelegramUI/Components/CameraButtonComponent", + "//submodules/Utils/VolumeButtons" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 12ef5eb5a8..97d0a472c4 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -19,6 +19,7 @@ import TooltipUI import MediaEditor import BundleIconComponent import CameraButtonComponent +import VolumeButtons let videoRedColor = UIColor(rgb: 0xff3b30) @@ -87,6 +88,7 @@ private final class CameraScreenComponent: CombinedComponent { let camera: Camera let updateState: ActionSlot let hasAppeared: Bool + let isVisible: Bool let panelWidth: CGFloat let flipAnimationAction: ActionSlot let animateShutter: () -> Void @@ -99,6 +101,7 @@ private final class CameraScreenComponent: CombinedComponent { camera: Camera, updateState: ActionSlot, hasAppeared: Bool, + isVisible: Bool, panelWidth: CGFloat, flipAnimationAction: ActionSlot, animateShutter: @escaping () -> Void, @@ -110,6 +113,7 @@ private final class CameraScreenComponent: CombinedComponent { self.camera = camera self.updateState = updateState self.hasAppeared = hasAppeared + self.isVisible = isVisible self.panelWidth = panelWidth self.flipAnimationAction = flipAnimationAction self.animateShutter = animateShutter @@ -125,6 +129,9 @@ private final class CameraScreenComponent: CombinedComponent { if lhs.hasAppeared != rhs.hasAppeared { return false } + if lhs.isVisible != rhs.isVisible { + return false + } if lhs.panelWidth != rhs.panelWidth { return false } @@ -159,6 +166,8 @@ private final class CameraScreenComponent: CombinedComponent { private let completion: ActionSlot> private let updateState: ActionSlot + private let animateShutter: () -> Void + private var cameraStateDisposable: Disposable? private var resultDisposable = MetaDisposable() @@ -166,6 +175,9 @@ private final class CameraScreenComponent: CombinedComponent { fileprivate var lastGalleryAsset: PHAsset? private var lastGalleryAssetsDisposable: Disposable? + private var volumeButtonsListener: VolumeButtonsListener? + private let volumeButtonsListenerShouldBeActive = ValuePromise(false, ignoreRepeated: true) + var cameraState = CameraState(mode: .photo, position: .unspecified, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0, isDualCamEnabled: false) { didSet { self.updateState.invoke(self.cameraState) @@ -176,12 +188,20 @@ private final class CameraScreenComponent: CombinedComponent { private let hapticFeedback = HapticFeedback() - init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>, updateState: ActionSlot) { + init( + context: AccountContext, + camera: Camera, + present: @escaping (ViewController) -> Void, + completion: ActionSlot>, + updateState: ActionSlot, + animateShutter: @escaping () -> Void = {} + ) { self.context = context self.camera = camera self.present = present self.completion = completion self.updateState = updateState + self.animateShutter = animateShutter super.init() @@ -202,6 +222,8 @@ private final class CameraScreenComponent: CombinedComponent { Queue.concurrentDefaultQueue().async { self.setupRecentAssetSubscription() } + + self.setupVolumeButtonsHandler() } deinit { @@ -226,6 +248,77 @@ private final class CameraScreenComponent: CombinedComponent { }) } + func setupVolumeButtonsHandler() { + guard self.volumeButtonsListener == nil else { + return + } + + self.volumeButtonsListener = VolumeButtonsListener( + shouldBeActive: self.volumeButtonsListenerShouldBeActive.get(), + upPressed: { [weak self] in + if let self { + self.handleVolumePressed() + } + }, + upReleased: { [weak self] in + if let self { + self.handleVolumeReleased() + } + }, + downPressed: { [weak self] in + if let self { + self.handleVolumePressed() + } + }, + downReleased: { [weak self] in + if let self { + self.handleVolumeReleased() + } + } + ) + } + + var volumeButtonsListenerActive = false { + didSet { + self.volumeButtonsListenerShouldBeActive.set(self.volumeButtonsListenerActive) + } + } + + private var buttonPressTimestamp: Double? + private var buttonPressTimer: SwiftSignalKit.Timer? + + private func handleVolumePressed() { + self.buttonPressTimestamp = CACurrentMediaTime() + + self.buttonPressTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in + if let self, let _ = self.buttonPressTimestamp { + if case .none = self.cameraState.recording { + self.startVideoRecording(pressing: true) + } + self.buttonPressTimestamp = nil + } + }, queue: Queue.mainQueue()) + self.buttonPressTimer?.start() + } + + private func handleVolumeReleased() { + if case .none = self.cameraState.recording { + switch self.cameraState.mode { + case .photo: + self.animateShutter() + self.takePhoto() + case .video: + self.startVideoRecording(pressing: false) + } + } else { + self.stopVideoRecording() + } + + self.buttonPressTimer?.invalidate() + self.buttonPressTimer = nil + self.buttonPressTimestamp = nil + } + func updateCameraMode(_ mode: CameraMode) { self.cameraState = self.cameraState.updatedMode(mode) self.updated(transition: .spring(duration: 0.3)) @@ -305,8 +398,8 @@ private final class CameraScreenComponent: CombinedComponent { self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0) self.resultDisposable.set((self.camera.stopRecording() |> deliverOnMainQueue).start(next: { [weak self] result in - if let self, case let .finished(mainResult, additionalResult, _) = result { - self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), .bottomRight))) + if let self, case let .finished(mainResult, additionalResult, duration, positionChangeTimestamps, _) = result { + self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), duration, positionChangeTimestamps, .bottomRight))) } })) self.isTransitioning = true @@ -329,7 +422,7 @@ private final class CameraScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion, updateState: self.updateState) + return State(context: self.context, camera: self.camera, present: self.present, completion: self.completion, updateState: self.updateState, animateShutter: self.animateShutter) } static var body: Body { @@ -351,6 +444,8 @@ private final class CameraScreenComponent: CombinedComponent { let state = context.state let controller = environment.controller let availableSize = context.availableSize + + state.volumeButtonsListenerActive = component.hasAppeared && component.isVisible let isTablet: Bool if case .regular = environment.metrics.widthClass { @@ -741,8 +836,6 @@ private final class CameraScreenComponent: CombinedComponent { } } -private let useSimplePreviewView = true - private class BlurView: UIVisualEffectView { private func setup() { for subview in self.subviews { @@ -803,7 +896,7 @@ public class CameraScreen: ViewController { public enum Result { case pendingImage case image(UIImage, UIImage?, CameraScreen.PIPPosition) - case video(String, UIImage?, String?, UIImage?, PixelDimensions, CameraScreen.PIPPosition) + case video(String, UIImage?, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], CameraScreen.PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft) @@ -811,8 +904,8 @@ public class CameraScreen: ViewController { switch self { case let .image(mainImage, additionalImage, _): return .image(mainImage, additionalImage, position) - case let .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, _): - return .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, position) + case let .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, duration, positionChangeTimestamps, _): + return .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, duration, positionChangeTimestamps, position) default: return self } @@ -860,73 +953,39 @@ public class CameraScreen: ViewController { fileprivate let containerView: UIView fileprivate let componentHost: ComponentView private let previewContainerView: UIView - fileprivate let previewView: CameraPreviewView? - fileprivate let simplePreviewView: CameraSimplePreviewView? - fileprivate var additionalPreviewView: CameraSimplePreviewView? + + private let mainPreviewContainerView: UIView + fileprivate var mainPreviewView: CameraSimplePreviewView + + private let additionalPreviewContainerView: UIView + fileprivate var additionalPreviewView: CameraSimplePreviewView + fileprivate let previewBlurView: BlurView - private var previewSnapshotView: UIView? + private var mainPreviewSnapshotView: UIView? private var additionalPreviewSnapshotView: UIView? fileprivate let previewFrameLeftDimView: UIView fileprivate let previewFrameRightDimView: UIView fileprivate let transitionDimView: UIView fileprivate let transitionCornersView: UIImageView fileprivate let camera: Camera - - private var presentationData: PresentationData - private var validLayout: ContainerViewLayout? private var changingPositionDisposable: Disposable? private var isDualCamEnabled = false private var appliedDualCam = false private var cameraPosition: Camera.Position = .back - private let completion = ActionSlot>() - - private var effectivePreviewView: UIView { - if let simplePreviewView = self.simplePreviewView { - return simplePreviewView - } else if let previewView = self.previewView { - return previewView - } else { - fatalError() - } - } - - private var currentPreviewView: UIView { - if let simplePreviewView = self.simplePreviewView { - if let additionalPreviewView = self.additionalPreviewView { - if self.isDualCamEnabled && cameraPosition == .front { - return additionalPreviewView - } else { - return simplePreviewView - } - } else { - return simplePreviewView - } - } else if let previewView = self.previewView { - return previewView - } else { - fatalError() - } - } - - private var currentAdditionalPreviewView: UIView? { - if let additionalPreviewView = self.additionalPreviewView { - if self.isDualCamEnabled && cameraPosition == .front { - return self.simplePreviewView - } else { - return additionalPreviewView - } - } else { - return nil - } - } + private var pipPosition: PIPPosition = .bottomRight fileprivate var previewBlurPromise = ValuePromise(false) - private let flipAnimationAction = ActionSlot() - private var pipPosition: PIPPosition = .bottomRight + fileprivate var cameraIsActive = true + fileprivate var hasGallery = false + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + private let completion = ActionSlot>() init(controller: CameraScreen) { self.controller = controller @@ -953,36 +1012,22 @@ public class CameraScreen: ViewController { self.previewBlurView = BlurView() self.previewBlurView.isUserInteractionEnabled = false - if let holder = controller.holder { - self.simplePreviewView = nil - self.previewView = holder.previewView - self.camera = holder.camera - } else { - if useSimplePreviewView { - self.simplePreviewView = CameraSimplePreviewView(frame: .zero, additional: false) - self.previewView = nil - - self.additionalPreviewView = CameraSimplePreviewView(frame: .zero, additional: true) - self.additionalPreviewView?.clipsToBounds = true - } else { - self.previewView = CameraPreviewView(test: false)! - self.simplePreviewView = nil - } - - var cameraFrontPosition = false - if let useCameraFrontPosition = UserDefaults.standard.object(forKey: "TelegramStoryCameraUseFrontPosition") as? NSNumber, useCameraFrontPosition.boolValue { - cameraFrontPosition = true - } - - self.cameraPosition = cameraFrontPosition ? .front : .back - self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: self.cameraPosition, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.simplePreviewView, secondaryPreviewView: self.additionalPreviewView) - if !useSimplePreviewView { -#if targetEnvironment(simulator) -#else - self.camera.attachPreviewView(self.previewView!) -#endif - } + self.mainPreviewContainerView = UIView() + self.mainPreviewContainerView.clipsToBounds = true + self.mainPreviewView = CameraSimplePreviewView(frame: .zero, main: true) + + self.additionalPreviewContainerView = UIView() + self.additionalPreviewContainerView.clipsToBounds = true + self.additionalPreviewView = CameraSimplePreviewView(frame: .zero, main: false) + + var cameraFrontPosition = false + if let useCameraFrontPosition = UserDefaults.standard.object(forKey: "TelegramStoryCameraUseFrontPosition") as? NSNumber, useCameraFrontPosition.boolValue { + cameraFrontPosition = true } + self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition) + + self.cameraPosition = cameraFrontPosition ? .front : .back + self.camera = Camera(configuration: Camera.Configuration(preset: .hd1920x1080, position: self.cameraPosition, audio: true, photo: true, metadata: false, preferredFps: 60.0), previewView: self.mainPreviewView, secondaryPreviewView: self.additionalPreviewView) self.previewFrameLeftDimView = UIView() self.previewFrameLeftDimView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) @@ -1006,17 +1051,17 @@ public class CameraScreen: ViewController { self.view.addSubview(self.containerView) self.containerView.addSubview(self.previewContainerView) - self.previewContainerView.addSubview(self.effectivePreviewView) + self.previewContainerView.addSubview(self.mainPreviewContainerView) + self.previewContainerView.addSubview(self.additionalPreviewContainerView) self.previewContainerView.addSubview(self.previewBlurView) self.previewContainerView.addSubview(self.previewFrameLeftDimView) self.previewContainerView.addSubview(self.previewFrameRightDimView) self.containerView.addSubview(self.transitionDimView) self.view.addSubview(self.transitionCornersView) - if let additionalPreviewView = self.additionalPreviewView { - self.previewContainerView.insertSubview(additionalPreviewView, at: 1) - } - + self.mainPreviewContainerView.addSubview(self.mainPreviewView) + self.additionalPreviewContainerView.addSubview(self.additionalPreviewView) + self.changingPositionDisposable = combineLatest( queue: Queue.mainQueue(), self.camera.modeChange, @@ -1024,18 +1069,21 @@ public class CameraScreen: ViewController { ).start(next: { [weak self] modeChange, forceBlur in if let self { if modeChange != .none { - if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { - self.simplePreviewView?.addSubview(snapshot) - self.previewSnapshotView = snapshot + if case .dualCamera = modeChange, self.cameraPosition == .front { + + } else { + if let snapshot = self.mainPreviewView.snapshotView(afterScreenUpdates: false) { + self.mainPreviewView.addSubview(snapshot) + self.mainPreviewSnapshotView = snapshot + } } if case .position = modeChange { UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { self.previewBlurView.effect = UIBlurEffect(style: .dark) }) } else { - if let additionalPreviewView = self.additionalPreviewView { - self.previewContainerView.insertSubview(self.previewBlurView, belowSubview: additionalPreviewView) - } + self.previewContainerView.insertSubview(self.previewBlurView, belowSubview: self.additionalPreviewContainerView) + UIView.animate(withDuration: 0.4) { self.previewBlurView.effect = UIBlurEffect(style: .dark) } @@ -1045,16 +1093,16 @@ public class CameraScreen: ViewController { self.previewBlurView.effect = UIBlurEffect(style: .dark) } } else { - UIView.animate(withDuration: 0.4, animations: { - self.previewBlurView.effect = nil - }, completion: { _ in - if let additionalPreviewView = self.additionalPreviewView { - self.previewContainerView.insertSubview(self.previewBlurView, aboveSubview: additionalPreviewView) - } - }) + if self.previewBlurView.effect != nil { + UIView.animate(withDuration: 0.4, animations: { + self.previewBlurView.effect = nil + }, completion: { _ in + self.previewContainerView.insertSubview(self.previewBlurView, aboveSubview: self.additionalPreviewContainerView) + }) + } - if let previewSnapshotView = self.previewSnapshotView { - self.previewSnapshotView = nil + if let previewSnapshotView = self.mainPreviewSnapshotView { + self.mainPreviewSnapshotView = nil UIView.animate(withDuration: 0.25, animations: { previewSnapshotView.alpha = 0.0 }, completion: { _ in @@ -1072,7 +1120,8 @@ public class CameraScreen: ViewController { } if self.isDualCamEnabled { - self.additionalPreviewView?.removePlaceholder() + self.mainPreviewView.removePlaceholder() + self.additionalPreviewView.removePlaceholder() } } } @@ -1093,9 +1142,9 @@ public class CameraScreen: ViewController { } if case .pendingImage = value { Queue.mainQueue().async { - self.simplePreviewView?.isEnabled = false + self.mainPreviewView.isEnabled = false - self.additionalPreviewView?.isEnabled = false + self.additionalPreviewView.isEnabled = false } } else { Queue.mainQueue().async { @@ -1104,8 +1153,8 @@ public class CameraScreen: ViewController { self.previewBlurPromise.set(true) } } - self.simplePreviewView?.isEnabled = false - self.additionalPreviewView?.isEnabled = false + self.mainPreviewView.isEnabled = false + self.additionalPreviewView.isEnabled = false self.camera.stopCapture() } } @@ -1119,25 +1168,32 @@ public class CameraScreen: ViewController { self.updateState.connect { [weak self] state in if let self { let previousPosition = self.cameraPosition - self.cameraPosition = state.position + let newPosition = state.position != .unspecified ? state.position : previousPosition + self.cameraPosition = newPosition let dualCamWasEnabled = self.isDualCamEnabled self.isDualCamEnabled = state.isDualCamEnabled - if self.isDualCamEnabled && previousPosition != state.position, let additionalPreviewView = self.additionalPreviewView { - if state.position == .front { - additionalPreviewView.superview?.sendSubviewToBack(additionalPreviewView) + if self.isDualCamEnabled != dualCamWasEnabled && newPosition == .front { + if self.isDualCamEnabled { + if let cloneView = self.mainPreviewView.snapshotView(afterScreenUpdates: false) { + self.mainPreviewSnapshotView = cloneView + self.mainPreviewContainerView.addSubview(cloneView) + } } else { - additionalPreviewView.superview?.insertSubview(additionalPreviewView, aboveSubview: self.simplePreviewView!) + if let cloneView = self.additionalPreviewView.snapshotView(afterScreenUpdates: false) { + self.mainPreviewSnapshotView = cloneView + self.mainPreviewContainerView.addSubview(cloneView) + } } + } + + if self.isDualCamEnabled && previousPosition != newPosition { CATransaction.begin() CATransaction.setDisableActions(true) self.requestUpdateLayout(hasAppeared: false, transition: .immediate) CATransaction.commit() - } else { - if !dualCamWasEnabled && self.isDualCamEnabled { - - } + } else if dualCamWasEnabled != self.isDualCamEnabled { self.requestUpdateLayout(hasAppeared: false, transition: .spring(duration: 0.4)) } } @@ -1155,17 +1211,17 @@ public class CameraScreen: ViewController { self.view.disablesInteractiveKeyboardGestureRecognizer = true let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) - self.effectivePreviewView.addGestureRecognizer(pinchGestureRecognizer) + self.mainPreviewContainerView.addGestureRecognizer(pinchGestureRecognizer) let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.maximumNumberOfTouches = 1 - self.effectivePreviewView.addGestureRecognizer(panGestureRecognizer) + self.mainPreviewContainerView.addGestureRecognizer(panGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) - self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer) + self.mainPreviewContainerView.addGestureRecognizer(tapGestureRecognizer) let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:))) - self.additionalPreviewView?.addGestureRecognizer(pipPanGestureRecognizer) + self.additionalPreviewContainerView.addGestureRecognizer(pipPanGestureRecognizer) self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) self.camera.startCapture() @@ -1217,11 +1273,8 @@ public class CameraScreen: ViewController { } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let previewView = self.simplePreviewView else { - return - } - let location = gestureRecognizer.location(in: previewView) - let point = previewView.cameraPoint(for: location) + let location = gestureRecognizer.location(in: mainPreviewView) + let point = mainPreviewView.cameraPoint(for: location) self.camera.focus(at: point, autoFocus: false) } @@ -1284,7 +1337,7 @@ public class CameraScreen: ViewController { func animateOut(completion: @escaping () -> Void) { self.camera.stopCapture(invalidate: true) - + UIView.animate(withDuration: 0.25, animations: { self.backgroundView.alpha = 0.0 }) @@ -1322,6 +1375,9 @@ public class CameraScreen: ViewController { } func animateOutToEditor() { + self.cameraIsActive = false + self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) @@ -1347,30 +1403,33 @@ public class CameraScreen: ViewController { } func pauseCameraCapture() { - self.simplePreviewView?.isEnabled = false - self.additionalPreviewView?.isEnabled = false + self.mainPreviewView.isEnabled = false + self.additionalPreviewView.isEnabled = false Queue.mainQueue().after(0.3) { self.previewBlurPromise.set(true) } self.camera.stopCapture() + + self.cameraIsActive = false + self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) } func resumeCameraCapture() { - if self.simplePreviewView?.isEnabled == false { - if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { - self.simplePreviewView?.addSubview(snapshot) - self.previewSnapshotView = snapshot + if !self.mainPreviewView.isEnabled { + if let snapshot = self.mainPreviewView.snapshotView(afterScreenUpdates: false) { + self.mainPreviewView.addSubview(snapshot) + self.mainPreviewSnapshotView = snapshot } - if let snapshot = self.additionalPreviewView?.snapshotView(afterScreenUpdates: false) { - self.additionalPreviewView?.addSubview(snapshot) + if let snapshot = self.additionalPreviewView.snapshotView(afterScreenUpdates: false) { + self.additionalPreviewView.addSubview(snapshot) self.additionalPreviewSnapshotView = snapshot } - self.simplePreviewView?.isEnabled = true - self.additionalPreviewView?.isEnabled = true + self.mainPreviewView.isEnabled = true + self.additionalPreviewView.isEnabled = true self.camera.startCapture() - if #available(iOS 13.0, *), let isPreviewing = self.simplePreviewView?.isPreviewing { - let _ = (isPreviewing + if #available(iOS 13.0, *) { + let _ = (self.mainPreviewView.isPreviewing |> filter { $0 } @@ -1384,6 +1443,9 @@ public class CameraScreen: ViewController { self.previewBlurPromise.set(false) } } + + self.cameraIsActive = true + self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) } } @@ -1391,6 +1453,9 @@ public class CameraScreen: ViewController { if !toGallery { self.resumeCameraCapture() + self.cameraIsActive = true + self.requestUpdateLayout(hasAppeared: self.hasAppeared, transition: .immediate) + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) @@ -1425,7 +1490,7 @@ public class CameraScreen: ViewController { let progress = 1.0 - value let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let targetTopInset = ceil((layout.statusBarHeight ?? 0.0) - (layout.size.height - layout.size.height * maxScale) / 2.0) let deltaOffset = (targetTopInset - topInset) @@ -1467,10 +1532,10 @@ public class CameraScreen: ViewController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { - if let additionalPreviewView = self.additionalPreviewView, additionalPreviewView.bounds.contains(self.view.convert(point, to: additionalPreviewView)) { - return additionalPreviewView + if self.additionalPreviewView.bounds.contains(self.view.convert(point, to: self.additionalPreviewView)) { + return self.additionalPreviewView } else { - return self.effectivePreviewView + return self.mainPreviewView } } return result @@ -1510,7 +1575,7 @@ public class CameraScreen: ViewController { } else { previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) } - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let bottomInset = layout.size.height - previewSize.height - topInset let panelWidth: CGFloat @@ -1566,10 +1631,11 @@ public class CameraScreen: ViewController { camera: self.camera, updateState: self.updateState, hasAppeared: self.hasAppeared, + isVisible: self.cameraIsActive && !self.hasGallery, panelWidth: panelWidth, flipAnimationAction: self.flipAnimationAction, animateShutter: { [weak self] in - self?.effectivePreviewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + self?.mainPreviewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) }, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) @@ -1605,58 +1671,80 @@ public class CameraScreen: ViewController { transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size)) transition.setFrame(view: self.previewContainerView, frame: previewFrame) - self.currentPreviewView.layer.cornerRadius = 0.0 - transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.mainPreviewContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) - if let additionalPreviewView = self.currentAdditionalPreviewView as? CameraSimplePreviewView { - let dualCamUpdated = self.appliedDualCam != self.isDualCamEnabled - self.appliedDualCam = self.isDualCamEnabled - - additionalPreviewView.layer.cornerRadius = 80.0 - - var origin: CGPoint - switch self.pipPosition { - case .topLeft: - origin = CGPoint(x: 10.0, y: 110.0) - if !self.isDualCamEnabled { - origin = origin.offsetBy(dx: -180.0, dy: 0.0) - } - case .topRight: - origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: 110.0) - if !self.isDualCamEnabled { - origin = origin.offsetBy(dx: 180.0, dy: 0.0) - } - case .bottomLeft: - origin = CGPoint(x: 10.0, y: previewFrame.height - 160.0 - 110.0) - if !self.isDualCamEnabled { - origin = origin.offsetBy(dx: -180.0, dy: 0.0) - } - case .bottomRight: - origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: previewFrame.height - 160.0 - 110.0) - if !self.isDualCamEnabled { - origin = origin.offsetBy(dx: 180.0, dy: 0.0) - } + let dualCamUpdated = self.appliedDualCam != self.isDualCamEnabled + self.appliedDualCam = self.isDualCamEnabled + + let circleSide = floorToScreenPixels(previewSize.width * 160.0 / 430.0) + let circleOffset = CGPoint(x: previewSize.width * 224.0 / 1080.0, y: previewSize.width * 477.0 / 1080.0) + + var origin: CGPoint + switch self.pipPosition { + case .topLeft: + origin = CGPoint(x: circleOffset.x, y: circleOffset.y) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: -180.0, dy: 0.0) } - - if let pipTranslation = self.pipTranslation { - origin = origin.offsetBy(dx: pipTranslation.x, dy: pipTranslation.y) + case .topRight: + origin = CGPoint(x: previewFrame.width - circleOffset.x, y: circleOffset.y) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: 180.0, dy: 0.0) } - - let additionalPreviewFrame = CGRect(origin: origin, size: CGSize(width: 160.0, height: 160.0)) - transition.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center) - transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size)) - - transition.setScale(view: additionalPreviewView, scale: self.isDualCamEnabled ? 1.0 : 0.1) - transition.setAlpha(view: additionalPreviewView, alpha: self.isDualCamEnabled ? 1.0 : 0.0) - - if dualCamUpdated && !self.isDualCamEnabled { - Queue.mainQueue().after(0.5) { - additionalPreviewView.resetPlaceholder() - } + case .bottomLeft: + origin = CGPoint(x: circleOffset.x, y: previewFrame.height - circleOffset.y) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: -180.0, dy: 0.0) + } + case .bottomRight: + origin = CGPoint(x: previewFrame.width - circleOffset.x, y: previewFrame.height - circleOffset.y) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: 180.0, dy: 0.0) } } - + + if let pipTranslation = self.pipTranslation { + origin = origin.offsetBy(dx: pipTranslation.x, dy: pipTranslation.y) + } + + let additionalPreviewFrame = CGRect(origin: CGPoint(x: origin.x - circleSide / 2.0, y: origin.y - circleSide / 2.0), size: CGSize(width: circleSide, height: circleSide)) + transition.setPosition(view: self.additionalPreviewContainerView, position: additionalPreviewFrame.center) + transition.setBounds(view: self.additionalPreviewContainerView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size)) + self.additionalPreviewContainerView.layer.cornerRadius = additionalPreviewFrame.width / 2.0 + + transition.setScale(view: self.additionalPreviewContainerView, scale: self.isDualCamEnabled ? 1.0 : 0.1) + transition.setAlpha(view: self.additionalPreviewContainerView, alpha: self.isDualCamEnabled ? 1.0 : 0.0) + + if dualCamUpdated && self.isDualCamEnabled { + if self.cameraPosition == .back { + self.additionalPreviewView.resetPlaceholder(front: true) + } else { + self.mainPreviewView.resetPlaceholder(front: false) + } + } + + let mainPreviewView: CameraSimplePreviewView + let additionalPreviewView: CameraSimplePreviewView + if self.cameraPosition == .front && self.isDualCamEnabled { + mainPreviewView = self.additionalPreviewView + additionalPreviewView = self.mainPreviewView + } else { + mainPreviewView = self.mainPreviewView + additionalPreviewView = self.additionalPreviewView + } + + if mainPreviewView.superview != self.mainPreviewContainerView { + self.mainPreviewContainerView.insertSubview(mainPreviewView, at: 0) + } + if additionalPreviewView.superview != self.additionalPreviewContainerView { + self.additionalPreviewContainerView.insertSubview(additionalPreviewView, at: 0) + } + + mainPreviewView.frame = CGRect(origin: .zero, size: previewFrame.size) + additionalPreviewView.frame = CGRect(origin: .zero, size: additionalPreviewFrame.size) + self.previewFrameLeftDimView.isHidden = !isTablet transition.setFrame(view: self.previewFrameLeftDimView, frame: CGRect(origin: .zero, size: CGSize(width: viewfinderFrame.minX, height: viewfinderFrame.height))) @@ -1780,6 +1868,7 @@ public class CameraScreen: ViewController { self.node.animateInFromEditor(toGallery: self.galleryController?.displayNode.supernode != nil) } + private var didStopCameraCapture = false func presentGallery(fromGesture: Bool = false) { if !fromGesture { self.hapticFeedback.impact(.light) @@ -1787,20 +1876,22 @@ public class CameraScreen: ViewController { self.dismissAllTooltips() - var didStopCameraCapture = false + self.node.hasGallery = true + + self.didStopCameraCapture = false let stopCameraCapture = { [weak self] in - guard !didStopCameraCapture, let self else { + guard let self, !self.didStopCameraCapture else { return } - didStopCameraCapture = true + self.didStopCameraCapture = true self.node.pauseCameraCapture() } let resumeCameraCapture = { [weak self] in - guard didStopCameraCapture, let self else { + guard let self, self.didStopCameraCapture else { return } - didStopCameraCapture = false + self.didStopCameraCapture = false self.node.resumeCameraCapture() } @@ -1834,8 +1925,12 @@ public class CameraScreen: ViewController { self.completion(.single(.draft(draft)), resultTransition, dismissed) } } - }, dismissed: { + }, dismissed: { [weak self] in resumeCameraCapture() + if let self { + self.node.hasGallery = false + self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: .immediate) + } }) self.galleryController = controller } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index 06d3a04303..92be146087 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -516,7 +516,9 @@ final class CaptureControlsComponent: Component { var lastGalleryAsset: PHAsset? { didSet { if self.cachedAssetImage?.0 != self.lastGalleryAsset?.localIdentifier { - self.cachedAssetImage = nil + if self.cachedAssetImage?.0 != "" { + self.cachedAssetImage = nil + } if let lastGalleryAsset = self.lastGalleryAsset { self.assetDisposable.set((fetchPhotoLibraryImage(localIdentifier: lastGalleryAsset.localIdentifier, thumbnail: true) |> deliverOnMainQueue).start(next: { [weak self] imageAndDegraded in @@ -890,12 +892,18 @@ final class CaptureControlsComponent: Component { gallerySize = CGSize(width: 50.0, height: 50.0) galleryCornerRadius = 10.0 } + let galleryButtonId: String + if let (identifier, _) = state.cachedAssetImage, identifier == "" { + galleryButtonId = "placeholder" + } else { + galleryButtonId = "gallery" + } let galleryButtonSize = self.galleryButtonView.update( - transition: .immediate, + transition: transition, component: AnyComponent( CameraButton( content: AnyComponentWithIdentity( - id: "gallery", + id: galleryButtonId, component: AnyComponent( Image( image: state.cachedAssetImage?.1, diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 7a547b81cc..1edbcc6490 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -184,18 +184,18 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return hasPremium } - public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal { + public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasTrending: Bool = true, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal { let animationCache = context.animationCache let animationRenderer = context.animationRenderer - let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: true, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, hideBackground: hideBackground) + let emojiItems = EmojiPagerContentComponent.emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, isStatusSelection: false, isReactionSelection: false, isEmojiSelection: true, hasTrending: hasTrending, topReactionItems: [], areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId, hasSearch: hasSearch, hideBackground: hideBackground) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers] let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: true, forceHasPremium: false, hideBackground: hideBackground) + let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: hasTrending, forceHasPremium: false, hideBackground: hideBackground) let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index a744953427..ede1abe5df 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -25,13 +25,13 @@ public struct MediaEditorPlayerState { public final class MediaEditor { public enum Subject { case image(UIImage, PixelDimensions) - case video(String, UIImage?, PixelDimensions) + case video(String, UIImage?, PixelDimensions, Double) case asset(PHAsset) case draft(MediaEditorDraft) var dimensions: PixelDimensions { switch self { - case let .image(_, dimensions), let .video(_, _, dimensions): + case let .image(_, dimensions), let .video(_, _, dimensions, _): return dimensions case let .asset(asset): return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) @@ -116,6 +116,12 @@ public final class MediaEditor { } private let playerPlaybackStatePromise = Promise<(Double, Double, Bool, Bool)>((0.0, 0.0, false, false)) + public var position: Signal { + return self.playerPlaybackStatePromise.get() + |> map { _, position, _, _ -> Double in + return position + } + } public var duration: Double? { if let _ = self.player { if let trimRange = self.values.videoTrimRange { @@ -275,6 +281,9 @@ public final class MediaEditor { if case let .asset(asset) = subject { self.playerPlaybackState = (asset.duration, 0.0, false, false) self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) + } else if case let .video(_, _, _, duration) = subject { + self.playerPlaybackState = (duration, 0.0, false, true) + self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState)) } } @@ -350,7 +359,7 @@ public final class MediaEditor { } textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1)) } - case let .video(path, transitionImage, _): + case let .video(path, transitionImage, _, _): textureSource = Signal { subscriber in let url = URL(fileURLWithPath: path) let asset = AVURLAsset(url: url) @@ -458,6 +467,10 @@ public final class MediaEditor { } if let player { + if let initialSeekPosition = self.initialSeekPosition { + self.initialSeekPosition = nil + player.seek(to: CMTime(seconds: initialSeekPosition, preferredTimescale: CMTimeScale(1000)), toleranceBefore: .zero, toleranceAfter: .zero) + } self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in guard let self, let duration = player.currentItem?.duration.seconds else { return @@ -552,11 +565,16 @@ public final class MediaEditor { public var onPlaybackAction: (PlaybackAction) -> Void = { _ in } + private var initialSeekPosition: Double? private var targetTimePosition: (CMTime, Bool)? private var updatingTimePosition = false public func seek(_ position: Double, andPlay play: Bool) { + guard let player = self.player else { + self.initialSeekPosition = position + return + } if !play { - self.player?.pause() + player.pause() self.onPlaybackAction(.pause) } let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)) @@ -567,7 +585,7 @@ public final class MediaEditor { } } if play { - self.player?.play() + player.play() self.onPlaybackAction(.play) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index 5ef2bea493..f137bb7442 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -148,8 +148,8 @@ final class MediaEditorComposer { var pixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - if let pixelBuffer { - makeEditorImageFrameComposition(inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in + if let pixelBuffer, let context = self.ciContext { + makeEditorImageFrameComposition(context: context, inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in if var compositedImage { let scale = self.outputDimensions.width / self.dimensions.width compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) @@ -167,11 +167,14 @@ final class MediaEditorComposer { } func processImage(inputImage: CIImage, time: CMTime, completion: @escaping (CIImage?) -> Void) { - makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: completion) + guard let context = self.ciContext else { + return + } + makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: completion) } } -public func makeEditorImageComposition(account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { +public func makeEditorImageComposition(context: CIContext, account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { let colorSpace = CGColorSpaceCreateDeviceRGB() let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! let gradientImage: CIImage @@ -191,9 +194,8 @@ public func makeEditorImageComposition(account: Account, inputImage: UIImage, di entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, entity: entity.entity, colorSpace: colorSpace)) } - makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in + makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in if let ciImage { - let context = CIContext(options: [.workingColorSpace : NSNull()]) if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { Queue.mainQueue().async { completion(UIImage(cgImage: cgImage)) @@ -205,7 +207,7 @@ public func makeEditorImageComposition(account: Account, inputImage: UIImage, di }) } -private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, completion: @escaping (CIImage?) -> Void) { +private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, completion: @escaping (CIImage?) -> Void) { var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) resultImage = gradientImage.composited(over: resultImage) @@ -253,7 +255,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: return current + 1 } let index = i - entity.image(for: time, frameRate: frameRate, completion: { image in + entity.image(for: time, frameRate: frameRate, context: context, completion: { image in if var image = image?.samplingLinear() { let resetTransform = CGAffineTransform(translationX: -image.extent.width / 2.0, y: -image.extent.height / 2.0) image = image.transformed(by: resetTransform) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 197b6f5390..260ba977cf 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -62,7 +62,7 @@ private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity { self.mirrored = mirrored } - func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { + func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) { completion(self.image) } } @@ -215,7 +215,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } private var circleMaskFilter: CIFilter? - func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { + func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) { if case .video = self.content { if let videoOutput = self.videoOutput { if let sampleBuffer = videoOutput.copyNextSampleBuffer(), let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { @@ -264,7 +264,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { var tintColor: UIColor? if let file = self.content.file, file.isCustomTemplateEmoji { - tintColor = .white + tintColor = UIColor(rgb: 0xffffff) } let processFrame: (Double?, Int?, Int?, (Int) -> AnimatedStickerFrame?) -> Void = { [weak self] duration, frameCount, frameRate, takeFrame in @@ -341,7 +341,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } if let imagePixelBuffer { - let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor) + let image = render(context: context, width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor) strongSelf.image = image } completion(strongSelf.image) @@ -426,10 +426,27 @@ protocol MediaEditorComposerEntity { var baseSize: CGSize? { get } var mirrored: Bool { get } - func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) + func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) } -private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? { +extension CIImage { + func tinted(with color: UIColor) -> CIImage? { + guard let colorMatrix = CIFilter(name: "CIColorMatrix") else { + return self + } + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + color.getRed(&r, green: &g, blue: &b, alpha: &a) + colorMatrix.setDefaults() + colorMatrix.setValue(self, forKey: "inputImage") + colorMatrix.setValue(CIVector(x: r, y: 0, z: 0, w: 0), forKey: "inputRVector") + colorMatrix.setValue(CIVector(x: 0, y: g, z: 0, w: 0), forKey: "inputGVector") + colorMatrix.setValue(CIVector(x: 0, y: 0, z: b, w: 0), forKey: "inputBVector") + colorMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: a), forKey: "inputAVector") + return colorMatrix.outputImage + } +} + +private func render(context: CIContext, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? { //let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31) //assert(bytesPerRow == calculatedBytesPerRow) @@ -458,7 +475,15 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace]) + let image = CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace]) + if let tintColor { + if let cgImage = context.createCGImage(image, from: CGRect(origin: .zero, size: image.extent.size)) { + if let tintedImage = generateTintedImage(image: UIImage(cgImage: cgImage), color: tintColor) { + return CIImage(image: tintedImage) + } + } + } + return image } private extension UIImage.Orientation { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 3f39b22d10..3857724e51 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -378,6 +378,7 @@ final class MediaEditorScreenComponent: Component { self.endEditing(true) } + private var animatingButtons = false enum TransitionAnimationSource { case camera case gallery @@ -401,6 +402,7 @@ final class MediaEditorScreenComponent: Component { } if case .camera = source { + self.animatingButtons = true var delay: Double = 0.0 for button in buttons { if let view = button.view { @@ -412,10 +414,12 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) }) delay += 0.03 - - Queue.mainQueue().after(0.45, completion) } } + Queue.mainQueue().after(0.45, { + self.animatingButtons = false + completion() + }) if let view = self.saveButton.view { view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) @@ -470,7 +474,7 @@ final class MediaEditorScreenComponent: Component { for button in buttons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } @@ -634,7 +638,7 @@ final class MediaEditorScreenComponent: Component { let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 let previewSize: CGSize - let topInset: CGFloat = environment.statusBarHeight + 12.0 + let topInset: CGFloat = environment.statusBarHeight + 5.0 if isTablet { let previewHeight = availableSize.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) @@ -745,7 +749,9 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: drawButtonView, position: drawButtonFrame.center) transition.setBounds(view: drawButtonView, bounds: CGRect(origin: .zero, size: drawButtonFrame.size)) - transition.setAlpha(view: drawButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + if !self.animatingButtons { + transition.setAlpha(view: drawButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } } let textButtonSize = self.textButton.update( @@ -772,7 +778,9 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: textButtonView, position: textButtonFrame.center) transition.setBounds(view: textButtonView, bounds: CGRect(origin: .zero, size: textButtonFrame.size)) - transition.setAlpha(view: textButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + if !self.animatingButtons { + transition.setAlpha(view: textButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } } let stickerButtonSize = self.stickerButton.update( @@ -799,7 +807,9 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: stickerButtonView, position: stickerButtonFrame.center) transition.setBounds(view: stickerButtonView, bounds: CGRect(origin: .zero, size: stickerButtonFrame.size)) - transition.setAlpha(view: stickerButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + if !self.animatingButtons { + transition.setAlpha(view: stickerButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } } let toolsButtonSize = self.toolsButton.update( @@ -826,7 +836,9 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: toolsButtonView, position: toolsButtonFrame.center) transition.setBounds(view: toolsButtonView, bounds: CGRect(origin: .zero, size: toolsButtonFrame.size)) - transition.setAlpha(view: toolsButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + if !self.animatingButtons { + transition.setAlpha(view: toolsButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } } var mediaEditor: MediaEditor? @@ -876,7 +888,9 @@ final class MediaEditorScreenComponent: Component { self.addSubview(scrubberView) } transition.setFrame(view: scrubberView, frame: scrubberFrame) - transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + if !self.animatingButtons { + transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } } scrubberBottomInset = scrubberSize.height + 10.0 @@ -1045,7 +1059,9 @@ final class MediaEditorScreenComponent: Component { } let isVisible = inputHeight > 44.0 transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize)) - transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) + if !self.animatingButtons { + transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) + } } var isEditingTextEntity = false @@ -1262,6 +1278,11 @@ final class MediaEditorScreenComponent: Component { muteButtonView.layer.shadowColor = UIColor.black.cgColor muteButtonView.layer.shadowOpacity = 0.35 self.addSubview(muteButtonView) + + if self.animatingButtons { + muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: 0.1) + muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: 0.1) + } } transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) @@ -1566,6 +1587,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate var mediaEditor: MediaEditor? fileprivate var mediaEditorPromise = Promise() + fileprivate let ciContext = CIContext(options: [.workingColorSpace : NSNull()]) + private let stickerPickerInputData = Promise() private var dismissPanGestureRecognizer: UIPanGestureRecognizer? @@ -1730,19 +1753,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if case let .draft(draft, _) = subject, let privacy = draft.privacy { controller.state.privacy = privacy - } else if !controller.isEditingStory { - let _ = combineLatest( - queue: Queue.mainQueue(), - mediaEditorStoredState(engine: controller.context.engine), - controller.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.context.account.peerId)) - ).start(next: { [weak controller] state, peer in - if let controller, var privacy = state?.privacy { - if case let .user(user) = peer, !user.isPremium && privacy.timeout != 86400 { - privacy = MediaEditorResultPrivacy(privacy: privacy.privacy, timeout: 86400, archive: false) - } - controller.state.privacy = privacy - } - }) } let isSavingAvailable: Bool @@ -1783,7 +1793,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate imageEntity.scale = 1.49 imageEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(imageEntity, announce: false) - } else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, position) = subject, let additionalVideoPath { + } else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, _, _, position) = subject, let additionalVideoPath { let videoEntity = DrawingStickerEntity(content: .video(additionalVideoPath, additionalVideoImage)) videoEntity.referenceDrawingSize = storyDimensions videoEntity.scale = 1.49 @@ -1820,6 +1830,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate initialValues = nil } let mediaEditor = MediaEditor(subject: subject.editorSubject, values: initialValues, hasHistogram: true) + if let initialVideoPosition = self.controller?.initialVideoPosition { + mediaEditor.seek(initialVideoPosition, andPlay: true) + } mediaEditor.attachPreviewView(self.previewView) mediaEditor.valuesUpdated = { [weak self] values in if let self, let controller = self.controller, values.gradientColors != nil, controller.previousSavedValues != values { @@ -1841,15 +1854,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate Queue.mainQueue().async { self.gradientView.image = gradientImage - self.previewContainerView.alpha = 1.0 - if CACurrentMediaTime() - self.initializationTimestamp > 0.2 { - self.previewContainerView.layer.allowsGroupOpacity = true - self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in - self.previewContainerView.layer.allowsGroupOpacity = false - self.backgroundDimView.isHidden = false - }) + if self.controller?.isEditingStory == true && subject.isVideo { + } else { - self.backgroundDimView.isHidden = false + self.previewContainerView.alpha = 1.0 + if CACurrentMediaTime() - self.initializationTimestamp > 0.2 { + self.previewContainerView.layer.allowsGroupOpacity = true + self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in + self.previewContainerView.layer.allowsGroupOpacity = false + self.previewContainerView.alpha = 1.0 + }) + } else { + self.backgroundDimView.isHidden = false + } } } } @@ -1857,6 +1874,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.mediaEditor = mediaEditor self.mediaEditorPromise.set(.single(mediaEditor)) + if self.controller?.isEditingStory == true && subject.isVideo { + mediaEditor.onFirstDisplay = { [weak self] in + if let self { + self.previewContainerView.alpha = 1.0 + self.backgroundDimView.isHidden = false + } + } + } + mediaEditor.onPlaybackAction = { [weak self] action in if let self { switch action { @@ -2128,7 +2154,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .camera, completion: completion) } - if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _) = subject, let transitionImage { + if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _, _, _) = subject, let transitionImage { self.setupTransitionImage(transitionImage) } case let .gallery(transitionIn): @@ -2312,6 +2338,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) } else { if controller.isEditingStory { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOut(to: .gallery) + } + + self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in completion() }) @@ -2507,7 +2538,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let progress = 1.0 - value let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let targetTopInset = ceil((layout.statusBarHeight ?? 0.0) - (layout.size.height - layout.size.height * maxScale) / 2.0) let deltaOffset = (targetTopInset - topInset) @@ -2548,7 +2579,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isTablet = false } - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 let previewSize: CGSize if isTablet { let previewHeight = layout.size.height - topInset - 75.0 @@ -2830,13 +2861,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public enum Subject { case image(UIImage, PixelDimensions, UIImage?, PIPPosition) - case video(String, UIImage?, String?, UIImage?, PixelDimensions, PIPPosition) + case video(String, UIImage?, String?, UIImage?, PixelDimensions, Double, [(Bool, Double)], PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft, Int64?) var dimensions: PixelDimensions { switch self { - case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _): + case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _, _, _): return dimensions case let .asset(asset): return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) @@ -2849,8 +2880,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch self { case let .image(image, dimensions, _, _): return .image(image, dimensions) - case let .video(videoPath, transitionImage, _, _, dimensions, _): - return .video(videoPath, transitionImage, dimensions) + case let .video(videoPath, transitionImage, _, _, dimensions, duration, _, _): + return .video(videoPath, transitionImage, dimensions, duration) case let .asset(asset): return .asset(asset) case let .draft(draft, _): @@ -2862,7 +2893,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch self { case let .image(image, dimensions, _, _): return .image(image, dimensions) - case let .video(videoPath, _, _, _, dimensions, _): + case let .video(videoPath, _, _, _, dimensions, _, _, _): return .video(videoPath, dimensions) case let .asset(asset): return .asset(asset) @@ -2870,6 +2901,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return .image(draft.thumbnail, draft.dimensions) } } + + var isVideo: Bool { + switch self { + case .image: + return false + case .video: + return true + case let .asset(asset): + return asset.mediaType == .video + case let .draft(draft, _): + return draft.isVideo + } + } } public enum Result { @@ -2888,6 +2932,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let initialCaption: NSAttributedString? fileprivate let initialPrivacy: EngineStoryPrivacy? + fileprivate let initialVideoPosition: Double? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? @@ -2905,6 +2950,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isEditing: Bool, initialCaption: NSAttributedString? = nil, initialPrivacy: EngineStoryPrivacy? = nil, + initialVideoPosition: Double? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, completion: @escaping (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void @@ -2914,6 +2960,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isEditingStory = isEditing self.initialCaption = initialCaption self.initialPrivacy = initialPrivacy + self.initialVideoPosition = initialVideoPosition self.transitionIn = transitionIn self.transitionOut = transitionOut self.completion = completion @@ -2934,6 +2981,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let initialPrivacy { self.state.privacy = MediaEditorResultPrivacy(privacy: initialPrivacy, timeout: 86400, archive: false) } + } else { + let _ = combineLatest( + queue: Queue.mainQueue(), + mediaEditorStoredState(engine: self.context.engine), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + ).start(next: { [weak self] state, peer in + if let self, var privacy = state?.privacy { + if case let .user(user) = peer, !user.isPremium && privacy.timeout != 86400 { + privacy = MediaEditorResultPrivacy(privacy: privacy.privacy, timeout: 86400, archive: false) + } + self.state.privacy = privacy + } + }) } } @@ -3235,7 +3295,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let resultImage = mediaEditor.resultImage { mediaEditor.seek(0.0, andPlay: false) - makeEditorImageComposition(account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in guard let resultImage else { return } @@ -3272,7 +3332,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch subject { case let .image(image, dimensions, _, _): saveImageDraft(image, dimensions) - case let .video(path, _, _, _, dimensions, _): + case let .video(path, _, _, _, dimensions, _, _, _): saveVideoDraft(path, dimensions, duration) case let .asset(asset): if asset.mediaType == .video { @@ -3365,7 +3425,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate duration = 5.0 firstFrame = .single(image) - case let .video(path, _, _, _, _, _): + case let .video(path, _, _, _, _, _, _, _): videoResult = .videoFile(path: path) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -3462,7 +3522,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let _ = (firstFrame |> deliverOnMainQueue).start(next: { [weak self] image in if let self { - makeEditorImageComposition(account: self.context.account, inputImage: image ?? UIImage(), dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image ?? UIImage(), dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in if let self { self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in @@ -3484,7 +3544,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let image = mediaEditor.resultImage { self.saveDraft(id: randomId) - makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in if let self, let resultImage { self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in @@ -3553,7 +3613,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let exportSubject: Signal switch subject { - case let .video(path, _, _, _, _, _): + case let .video(path, _, _, _, _, _, _, _): let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) exportSubject = .single(.video(asset)) case let .image(image, _, _, _): @@ -3631,7 +3691,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { if let image = mediaEditor.resultImage { Queue.concurrentDefaultQueue().async { - makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in if let data = resultImage?.jpegData(compressionQuality: 0.8) { let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" try? data.write(to: URL(fileURLWithPath: outputPath)) @@ -3858,7 +3918,6 @@ private final class ToolValueComponent: Component { } func update(component: ToolValueComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - let previousValue = self.component?.value self.component = component self.state = state diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index 087f543e9f..210f9cb65b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -337,7 +337,7 @@ private final class MediaToolsScreenComponent: Component { let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 let previewSize: CGSize - let topInset: CGFloat = environment.statusBarHeight + 12.0 + let topInset: CGFloat = environment.statusBarHeight + 5.0 if isTablet { let previewHeight = availableSize.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) @@ -995,7 +995,7 @@ public final class MediaToolsScreen: ViewController { } let previewSize: CGSize - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 if isTablet { let previewHeight = layout.size.height - topInset - 75.0 previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 6b0c80d7bf..5df7cb1bb2 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -382,10 +382,10 @@ final class ShareWithPeersScreenComponent: Component { @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { + guard let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } - controller.dismiss() + controller.requestDismiss() } } @@ -1045,10 +1045,10 @@ final class ShareWithPeersScreenComponent: Component { component: AnyComponent(Button( content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() else { + guard let self, let environment = self.environment, let controller = environment.controller() as? ShareWithPeersScreen else { return } - controller.dismiss() + controller.requestDismiss() } ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, @@ -1428,6 +1428,8 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { private var isDismissed: Bool = false + public var dismissed: () -> Void = {} + public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, timeout: Int, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void = {}) { self.context = context @@ -1521,6 +1523,11 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } } + func requestDismiss() { + self.dismissed() + self.dismiss() + } + override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 499fa9d476..c0adfdb3bf 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -175,7 +175,7 @@ private final class StoryContainerScreenComponent: Component { private var transitionCloneMasterView: UIView private var volumeButtonsListener: VolumeButtonsListener? - private let volumeButtonsListenerShouldBeActvie = ValuePromise(false, ignoreRepeated: true) + private let volumeButtonsListenerShouldBeActive = ValuePromise(false, ignoreRepeated: true) private var isAnimatingOut: Bool = false private var didAnimateOut: Bool = false @@ -589,19 +589,24 @@ private final class StoryContainerScreenComponent: Component { private func updateVolumeButtonMonitoring() { if self.volumeButtonsListener == nil { - self.volumeButtonsListener = VolumeButtonsListener(shouldBeActive: self.volumeButtonsListenerShouldBeActvie.get(), valueChanged: { [weak self] in + let buttonAction = { [weak self] in guard let self, self.storyItemSharedState.useAmbientMode else { return } self.storyItemSharedState.useAmbientMode = false - self.volumeButtonsListenerShouldBeActvie.set(false) + self.volumeButtonsListenerShouldBeActive.set(false) for (_, itemSetView) in self.visibleItemSetViews { if let componentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { componentView.leaveAmbientMode() } } - }) + } + self.volumeButtonsListener = VolumeButtonsListener( + shouldBeActive: self.volumeButtonsListenerShouldBeActive.get(), + upPressed: buttonAction, + downPressed: buttonAction + ) } } @@ -633,7 +638,7 @@ private final class StoryContainerScreenComponent: Component { self.focusedItem.set(focusedItemId) if self.storyItemSharedState.useAmbientMode { - self.volumeButtonsListenerShouldBeActvie.set(isVideo) + self.volumeButtonsListenerShouldBeActive.set(isVideo) if isVideo { self.updateVolumeButtonMonitoring() } @@ -762,7 +767,7 @@ private final class StoryContainerScreenComponent: Component { } var itemSetContainerSize = availableSize - var itemSetContainerInsets = UIEdgeInsets(top: environment.statusBarHeight + 12.0, left: 0.0, bottom: 0.0, right: 0.0) + var itemSetContainerInsets = UIEdgeInsets(top: environment.statusBarHeight + 5.0, left: 0.0, bottom: 0.0, right: 0.0) var itemSetContainerSafeInsets = environment.safeInsets if case .regular = environment.metrics.widthClass { let availableHeight = min(1080.0, availableSize.height - max(45.0, environment.safeInsets.bottom) * 2.0) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 124c748f53..4992da47cc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -29,6 +29,10 @@ public final class StoryContentItem: Equatable { open func leaveAmbientMode() { } + + open var videoPlaybackPosition: Double? { + return nil + } } public final class Environment: Equatable { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 6913877b39..0f90d00d73 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -72,6 +72,9 @@ final class StoryItemContentComponent: Component { private var contentLoaded: Bool = false private var videoPlaybackStatus: MediaPlayerStatus? + override var videoPlaybackPosition: Double? { + return self.videoPlaybackStatus?.timestamp + } private let hierarchyTrackingLayer: HierarchyTrackingLayer @@ -380,6 +383,11 @@ final class StoryItemContentComponent: Component { if self.currentMessageMedia?.id != messageMedia?.id { self.currentMessageMedia = messageMedia reloadMedia = true + + if let videoNode = self.videoNode { + self.videoNode = nil + videoNode.view.removeFromSuperview() + } } if reloadMedia, let messageMedia, let peerReference { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 21d8d7712b..8e64d151f2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -735,7 +735,7 @@ public final class StoryItemSetContainerComponent: Component { } if let navigationController = component.controller()?.navigationController as? NavigationController { let topViewController = navigationController.topViewController - if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) { + if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) { return true } } @@ -1702,7 +1702,6 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.updateInputMediaNode(inputPanel: self.inputPanel, availableSize: availableSize, bottomInset: component.safeInsets.bottom, inputHeight: component.inputHeight, effectiveInputHeight: inputHeight, metrics: component.metrics, deviceMetrics: component.deviceMetrics, transition: transition) - //let bottomContentInsetWithoutInput = bottomContentInset var viewListInset: CGFloat = 0.0 var inputPanelBottomInset: CGFloat @@ -2037,13 +2036,14 @@ public final class StoryItemSetContainerComponent: Component { } } + let itemSize = CGSize(width: availableSize.width, height: ceil(availableSize.width * 1.77778)) let contentDefaultBottomInset: CGFloat = bottomContentInset - let contentSize = CGSize(width: availableSize.width, height: availableSize.height - component.containerInsets.top - contentDefaultBottomInset) + let contentSize = itemSize let contentVisualBottomInset: CGFloat = max(contentDefaultBottomInset, viewListInset) - let contentVisualHeight = availableSize.height - component.containerInsets.top - contentVisualBottomInset - let contentVisualScale = contentVisualHeight / contentSize.height + let contentVisualHeight = min(contentSize.height, availableSize.height - component.containerInsets.top - contentVisualBottomInset) + let contentVisualScale = min(1.0, contentVisualHeight / contentSize.height) let contentFrame = CGRect(origin: CGPoint(x: 0.0, y: component.containerInsets.top - (contentSize.height - contentVisualHeight) * 0.5), size: contentSize) @@ -2681,6 +2681,7 @@ public final class StoryItemSetContainerComponent: Component { } let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start() + self.privacyController = nil self.updateIsProgressPaused() }, editCategory: { [weak self] privacy in @@ -2695,6 +2696,12 @@ public final class StoryItemSetContainerComponent: Component { }) } ) + controller.dismissed = { [weak self] in + if let self { + self.privacyController = nil + self.updateIsProgressPaused() + } + } self.component?.controller()?.push(controller) self.privacyController = controller @@ -2727,6 +2734,12 @@ public final class StoryItemSetContainerComponent: Component { }, editCategory: { _ in } ) + controller.dismissed = { [weak self] in + if let self { + self.privacyController = nil + self.updateIsProgressPaused() + } + } self.component?.controller()?.push(controller) self.privacyController = controller @@ -2762,21 +2775,33 @@ public final class StoryItemSetContainerComponent: Component { } private func openStoryEditing() { - guard let context = self.component?.context, let item = self.component?.slice.item.storyItem else { + guard let component = self.component, let peerReference = PeerReference(component.slice.peer._asPeer()) else { return } + let context = component.context + let item = component.slice.item.storyItem let id = item.id - + self.isEditingStory = true self.updateIsProgressPaused() self.state?.updated(transition: .easeInOut(duration: 0.2)) - + + var videoPlaybackPosition: Double? + if let visibleItem = self.visibleItems[component.slice.item.id], let view = visibleItem.view.view as? StoryContentItem.View { + videoPlaybackPosition = view.videoPlaybackPosition + } + let subject: Signal // if let source { // subject = .single(.draft(source, Int64(id))) // } else { + + var duration: Double? let media = item.media._asMedia() - subject = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .standalone(media: media)) + if let file = media as? TelegramMediaFile { + duration = file.duration + } + subject = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .story(peer: peerReference, id: item.id, media: media)) |> mapToSignal { (value, isImage) -> Signal in guard case let .data(data) = value, data.complete else { return .complete() @@ -2792,7 +2817,11 @@ public final class StoryItemSetContainerComponent: Component { if fileSize(symlinkPath) == nil { let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath) } - return .single(.video(symlinkPath, nil, nil, nil, PixelDimensions(width: 720, height: 1280), .bottomRight)) + return .single(nil) + |> then( + .single(.video(symlinkPath, nil, nil, nil, PixelDimensions(width: 720, height: 1280), duration ?? 0.0, [], .bottomRight)) + |> delay(0.1, queue: Queue.mainQueue()) + ) } } @@ -2803,6 +2832,7 @@ public final class StoryItemSetContainerComponent: Component { isEditing: true, initialCaption: chatInputStateStringWithAppliedEntities(item.text, entities: item.entities), initialPrivacy: item.privacy, + initialVideoPosition: videoPlaybackPosition, transitionIn: nil, transitionOut: { _, _ in return nil }, completion: { [weak self] _, mediaResult, caption, privacy, commit in @@ -2926,149 +2956,6 @@ public final class StoryItemSetContainerComponent: Component { updateProgressImpl = { [weak controller] progress in controller?.updateEditProgress(progress) } - -// } - -// let _ = (getStorySource(engine: context.engine, id: Int64(id)) -// |> deliverOnMainQueue).start(next: { [weak self] source in -// guard let self else { -// return -// } -// -// self.isEditingStory = true -// self.updateIsProgressPaused() -// -// let subject: Signal -// if let source { -// subject = .single(.draft(source, Int64(id))) -// } else { -// let media = item.media._asMedia() -// subject = fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: .standalone(media: media)) -// |> mapToSignal { (value, isImage) -> Signal in -// guard case let .data(data) = value, data.complete else { -// return .complete() -// } -// if let image = UIImage(contentsOfFile: data.path) { -// return .single(.image(image, PixelDimensions(image.size), nil, .bottomRight)) -// } else { -// return .single(.video(data.path, nil, nil, nil, PixelDimensions(width: 720, height: 1280), .bottomRight)) -// } -// } -// } -// -// var updateProgressImpl: ((Float) -> Void)? -// let controller = MediaEditorScreen( -// context: context, -// subject: subject, -// isEditing: true, -// transitionIn: nil, -// transitionOut: { _, _ in return nil }, -// completion: { [weak self] randomId, mediaResult, caption, privacy, commit in -// let entities = generateChatInputTextEntities(caption) -// var updatedText: String? -// var updatedEntities: [MessageTextEntity]? -// var updatedPrivacy: EngineStoryPrivacy? -// if caption.string != item.text || entities != item.entities { -// updatedText = caption.string -// updatedEntities = entities -// } -// if privacy.privacy != item.privacy { -// updatedPrivacy = privacy.privacy -// } -// -// if let mediaResult { -// switch mediaResult { -// case let .image(image, dimensions, caption): -// if let imageData = compressImageToJPEG(image, quality: 0.7) { -// let _ = (context.engine.messages.editStory(media: .image(dimensions: dimensions, data: imageData), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) -// |> deliverOnMainQueue).start(next: { [weak self] result in -// switch result { -// case let .progress(progress): -// updateProgressImpl?(progress) -// case .completed: -// Queue.mainQueue().after(0.1) { -// if let self { -// self.isEditingStory = false -// self.rewindCurrentItem() -// self.updateIsProgressPaused() -// } -// commit({}) -// } -// } -// }) -// } -// case let .video(content, firstFrameImage, values, duration, dimensions, caption): -// let adjustments: VideoMediaResourceAdjustments -// if let valuesData = try? JSONEncoder().encode(values) { -// let data = MemoryBuffer(data: valuesData) -// let digest = MemoryBuffer(data: data.md5Digest()) -// adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) -// -// let resource: TelegramMediaResource -// switch content { -// case let .imageFile(path): -// resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) -// case let .videoFile(path): -// resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) -// case let .asset(localIdentifier): -// resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) -// } -// let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6) } -// -// let _ = (context.engine.messages.editStory(media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameImageData: imageData), id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) -// |> deliverOnMainQueue).start(next: { [weak self] result in -// switch result { -// case let .progress(progress): -// updateProgressImpl?(progress) -// case .completed: -// Queue.mainQueue().after(0.1) { -// if let self { -// self.isEditingStory = false -// self.rewindCurrentItem() -// self.updateIsProgressPaused() -// } -// commit({}) -// } -// } -// }) -// } -// } -// } else if updatedText != nil || updatedPrivacy != nil { -// let _ = (context.engine.messages.editStory(media: nil, id: id, text: updatedText, entities: updatedEntities, privacy: updatedPrivacy) -// |> deliverOnMainQueue).start(next: { [weak self] result in -// switch result { -// case .completed: -// Queue.mainQueue().after(0.1) { -// if let self { -// self.isEditingStory = false -// self.rewindCurrentItem() -// self.updateIsProgressPaused() -// } -// commit({}) -// } -// default: -// break -// } -// }) -// } else { -// if let self { -// self.isEditingStory = false -// self.rewindCurrentItem() -// self.updateIsProgressPaused() -// } -// commit({}) -// } -// } -// ) -// controller.dismissed = { [weak self] in -// self?.isEditingStory = false -// self?.updateIsProgressPaused() -// } -// self.component?.controller()?.push(controller) -// updateProgressImpl = { [weak controller] progress in -// controller?.updateEditProgress(progress) -// } -// }) } private func requestSave() { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index d5ebc78cb3..00552c3bc0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -104,6 +104,7 @@ final class StoryItemSetContainerSendMessage { context: context, chatPeerId: nil, areCustomEmojiEnabled: true, + hasTrending: false, hasSearch: false, hideBackground: true, sendGif: nil diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf index 0d14e5b8fb..619f866622 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf @@ -10,7 +10,7 @@ stream /DeviceRGB CS /DeviceRGB cs q -1.000000 0.000000 -0.000000 1.000000 5.170449 5.169796 cm +1.000000 0.000000 -0.000000 1.000000 5.170449 4.169796 cm 1.000000 1.000000 1.000000 scn 9.830000 21.659878 m 10.288396 21.659878 10.660000 21.288275 10.660000 20.829878 c @@ -28,15 +28,15 @@ q 9.000000 21.288275 9.371604 21.659878 9.830000 21.659878 c h 1.660000 5.830000 m -1.660000 6.288396 1.288396 6.660000 0.830000 6.660000 c -0.371604 6.660000 0.000000 6.288396 0.000000 5.830000 c +1.660000 6.288397 1.288396 6.660000 0.830000 6.660000 c +0.371604 6.660000 0.000000 6.288397 0.000000 5.830000 c 0.000000 4.580000 l 0.000000 2.050535 2.050535 0.000000 4.579999 0.000000 c 15.080000 0.000000 l 17.609465 0.000000 19.660000 2.050535 19.660000 4.580000 c 19.660000 5.830000 l -19.660000 6.288396 19.288397 6.660000 18.830000 6.660000 c -18.371603 6.660000 18.000000 6.288396 18.000000 5.830000 c +19.660000 6.288397 19.288397 6.660000 18.830000 6.660000 c +18.371603 6.660000 18.000000 6.288397 18.000000 5.830000 c 18.000000 4.580000 l 18.000000 2.967329 16.692673 1.660000 15.080000 1.660000 c 4.579999 1.660000 l diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a9b7c5dbda..67c30eca03 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -10966,15 +10966,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - self.volumeButtonsListener = VolumeButtonsListener(shouldBeActive: shouldBeActive, valueChanged: { [weak self] in - guard let strongSelf = self, strongSelf.traceVisibility() && isTopmostChatController(strongSelf) else { + let buttonAction = { [weak self] in + guard let self, self.traceVisibility() && isTopmostChatController(self) else { return } - strongSelf.videoUnmuteTooltipController?.dismiss() + self.videoUnmuteTooltipController?.dismiss() var actions: [(Bool, (Double?) -> Void)] = [] var hasUnconsumed = false - strongSelf.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let (action, _, _, isUnconsumed, _) = itemNode.playMediaWithSound() { if case let .visible(fraction, _) = itemNode.visibility, fraction > 0.7 { actions.insert((isUnconsumed, action), at: 0) @@ -10990,7 +10990,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - }) + } + self.volumeButtonsListener = VolumeButtonsListener( + shouldBeActive: shouldBeActive, + upPressed: buttonAction, + downPressed: buttonAction + ) self.chatDisplayNode.historyNode.openNextChannelToRead = { [weak self] peer, location in guard let strongSelf = self else { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 260109549c..473dc3374f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -305,8 +305,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil case let .image(image, additionalImage, pipPosition): return .image(image, PixelDimensions(image.size), additionalImage, editorPIPPosition(pipPosition)) - case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, pipPosition): - return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, editorPIPPosition(pipPosition)) + case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, duration, positionChangeTimestamps, pipPosition): + return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, duration, positionChangeTimestamps, editorPIPPosition(pipPosition)) case let .asset(asset): return .asset(asset) case let .draft(draft): diff --git a/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift index 693235f365..58e5d866a8 100644 --- a/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift +++ b/submodules/Utils/VolumeButtons/Sources/VolumeButtons.swift @@ -10,21 +10,25 @@ public class VolumeButtonsListener: NSObject { private var disposable: Disposable? - public init(shouldBeActive: Signal, valueChanged: @escaping () -> Void) { - var impl: (() -> Void)? - + public init( + shouldBeActive: Signal, + upPressed: @escaping () -> Void, + upReleased: @escaping () -> Void = {}, + downPressed: @escaping () -> Void, + downReleased: @escaping () -> Void = {} + ) { self.handler = PGCameraVolumeButtonHandler(upButtonPressedBlock: { - impl?() - }, upButtonReleasedBlock: {}, downButtonPressedBlock: { - impl?() - }, downButtonReleasedBlock: {}) + upPressed() + }, upButtonReleasedBlock: { + upReleased() + }, downButtonPressedBlock: { + downPressed() + }, downButtonReleasedBlock: { + downReleased() + }) super.init() - - impl = { - valueChanged() - } - + self.disposable = (shouldBeActive |> deliverOnMainQueue).start(next: { [weak self] value in guard let strongSelf = self else {