diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 3ac78d7fe3..341740e7ac 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -10,6 +10,7 @@ private final class CameraContext { private let device: CameraDevice private let input = CameraInput() private let output = CameraOutput() + private let cameraImageContext = CIContext() private let initialConfiguration: Camera.Configuration private var invalidated = false @@ -40,20 +41,24 @@ private final class CameraContext { } } - private let previewSnapshotContext = CIContext() private var lastSnapshotTimestamp: Double = CACurrentMediaTime() - private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer) { + private func savePreviewSnapshot(pixelBuffer: CVPixelBuffer, mirror: Bool) { Queue.concurrentDefaultQueue().async { var ciImage = CIImage(cvImageBuffer: pixelBuffer) let size = ciImage.extent.size + if mirror { + var transform = CGAffineTransformMakeScale(-1.0, 1.0) + transform = CGAffineTransformTranslate(transform, size.width, 0.0) + ciImage = ciImage.transformed(by: transform) + } ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) - if let cgImage = self.previewSnapshotContext.createCGImage(ciImage, from: ciImage.extent) { + if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) CameraSimplePreviewView.saveLastStateImage(uiImage) } } } - + private var videoOrientation: AVCaptureVideoOrientation? init(queue: Queue, session: AVCaptureSession, configuration: Camera.Configuration, metrics: Camera.Metrics, previewView: CameraSimplePreviewView?) { self.queue = queue @@ -77,28 +82,14 @@ private final class CameraContext { } let timestamp = CACurrentMediaTime() - if timestamp > self.lastSnapshotTimestamp + 5.0 { - self.savePreviewSnapshot(pixelBuffer: pixelBuffer) + if timestamp > self.lastSnapshotTimestamp + 2.5 { + var mirror = false + if #available(iOS 13.0, *) { + mirror = connection.inputPorts.first?.sourceDevicePosition == .front + } + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror) self.lastSnapshotTimestamp = timestamp } -// if let previewView = self.previewView, !self.changingPosition { -// let videoOrientation = connection.videoOrientation -// if #available(iOS 13.0, *) { -// previewView.mirroring = connection.inputPorts.first?.sourceDevicePosition == .front -// } -// if let rotation = CameraPreviewView.Rotation(with: .portrait, videoOrientation: videoOrientation, cameraPosition: self.device.position) { -// previewView.rotation = rotation -// } -// if #available(iOS 13.0, *), connection.inputPorts.first?.sourceDevicePosition == .front { -// let width = CVPixelBufferGetWidth(pixelBuffer) -// let height = CVPixelBufferGetHeight(pixelBuffer) -// previewView.captureDeviceResolution = CGSize(width: width, height: height) -// } -// previewView.pixelBuffer = pixelBuffer -// Queue.mainQueue().async { -// self.videoOrientation = videoOrientation -// } -// } } self.output.processFaceLandmarks = { [weak self] observations in @@ -232,6 +223,10 @@ private final class CameraContext { self.device.setZoomLevel(zoomLevel) } + func setZoomDelta(_ zoomDelta: CGFloat) { + self.device.setZoomDelta(zoomDelta) + } + func takePhoto() -> Signal { return self.output.takePhoto(orientation: self.videoOrientation ?? .portrait, flashMode: self._flashMode) } @@ -240,7 +235,7 @@ private final class CameraContext { return self.output.startRecording() } - public func stopRecording() -> Signal { + public func stopRecording() -> Signal<(String, UIImage?)?, NoError> { return self.output.stopRecording() } @@ -306,19 +301,25 @@ public final class Camera { } public func startCapture() { +#if targetEnvironment(simulator) +#else self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { context.startCapture() } } +#endif } public func stopCapture(invalidate: Bool = false) { +#if targetEnvironment(simulator) +#else self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { context.stopCapture(invalidate: invalidate) } } +#endif } public func togglePosition() { @@ -369,7 +370,7 @@ public final class Camera { } } - public func stopRecording() -> Signal { + public func stopRecording() -> Signal<(String, UIImage?)?, NoError> { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { @@ -417,6 +418,15 @@ public final class Camera { } } + + public func setZoomDelta(_ zoomDelta: CGFloat) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setZoomDelta(zoomDelta) + } + } + } + public func setTorchActive(_ active: Bool) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 880e4f35b4..3df0b04f8a 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -220,4 +220,13 @@ final class CameraDevice { device.videoZoomFactor = max(1.0, min(10.0, zoomLevel)) } } + + func setZoomDelta(_ zoomDelta: CGFloat) { + guard let device = self.videoDevice else { + return + } + self.transaction(device) { device in + device.videoZoomFactor = max(1.0, min(10.0, device.videoZoomFactor * zoomDelta)) + } + } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index ea51c2702c..0227267561 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -1,5 +1,8 @@ +import Foundation import AVFoundation +import UIKit import SwiftSignalKit +import CoreImage import Vision import VideoToolbox @@ -160,7 +163,7 @@ final class CameraOutput: NSObject { } } - private var recordingCompletionPipe = ValuePipe() + private var recordingCompletionPipe = ValuePipe<(String, UIImage?)?>() func startRecording() -> Signal { guard self.videoRecorder == nil else { return .complete() @@ -184,14 +187,13 @@ 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 .success = result { - self?.recordingCompletionPipe.putNext(outputFilePath) + if case let .success(transitionImage) = result { + self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage)) } else { self?.recordingCompletionPipe.putNext(nil) } }) - videoRecorder?.start() self.videoRecorder = videoRecorder @@ -207,7 +209,7 @@ final class CameraOutput: NSObject { } } - func stopRecording() -> Signal { + func stopRecording() -> Signal<(String, UIImage?)?, NoError> { self.videoRecorder?.stop() return self.recordingCompletionPipe.signal() diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 74ae7b8b3b..b605e922a8 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -63,7 +63,7 @@ public class CameraSimplePreviewView: UIView { public override func layoutSubviews() { super.layoutSubviews() - self.placeholderView.frame = self.bounds + self.placeholderView.frame = self.bounds.insetBy(dx: -1.0, dy: -1.0) } var videoPreviewLayer: AVCaptureVideoPreviewLayer { diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index cd67763227..8c131661e7 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -1,5 +1,7 @@ import Foundation import AVFoundation +import UIKit +import CoreImage import SwiftSignalKit import TelegramCore @@ -32,6 +34,10 @@ private final class VideoRecorderImpl { private var videoInput: AVAssetWriterInput? private var audioInput: AVAssetWriterInput? + private let imageContext: CIContext + private var transitionImage: UIImage? + private var savedTransitionImage = false + private var pendingAudioSampleBuffers: [CMSampleBuffer] = [] private var _duration: CMTime = .zero @@ -46,7 +52,7 @@ private final class VideoRecorderImpl { private let configuration: VideoRecorder.Configuration private let videoTransform: CGAffineTransform private let url: URL - fileprivate var completion: (Bool) -> Void = { _ in } + fileprivate var completion: (Bool, UIImage?) -> Void = { _, _ in } private let error = Atomic(value: nil) @@ -58,6 +64,7 @@ private final class VideoRecorderImpl { self.configuration = configuration self.videoTransform = videoTransform self.url = fileUrl + self.imageContext = CIContext() try? FileManager.default.removeItem(at: url) guard let assetWriter = try? AVAssetWriter(url: url, fileType: .mp4) else { @@ -76,7 +83,7 @@ private final class VideoRecorderImpl { self.recordingStartSampleTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) } } - + public func appendVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) { if let _ = self.hasError() { return @@ -91,7 +98,6 @@ private final class VideoRecorderImpl { guard !self.stopped && self.error.with({ $0 }) == nil else { return } - var failed = false if self.videoInput == nil { let videoSettings = self.configuration.videoSettings @@ -139,6 +145,17 @@ private final class VideoRecorderImpl { } if let videoInput = self.videoInput, videoInput.isReadyForMoreMediaData { + if !self.savedTransitionImage, let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + self.savedTransitionImage = true + + let ciImage = CIImage(cvPixelBuffer: pixelBuffer) + if let cgImage = self.imageContext.createCGImage(ciImage, from: ciImage.extent) { + self.transitionImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) + } else { + self.savedTransitionImage = false + } + } + if videoInput.append(sampleBuffer) { self.lastVideoSampleTime = presentationTime let startTime = self.recordingStartSampleTime @@ -274,21 +291,21 @@ private final class VideoRecorderImpl { let completion = self.completion if self.recordingStopSampleTime == .invalid { DispatchQueue.main.async { - completion(false) + completion(false, nil) } return } if let _ = self.error.with({ $0 }) { DispatchQueue.main.async { - completion(false) + completion(false, nil) } return } if !self.tryAppendingPendingAudioBuffers() { DispatchQueue.main.async { - completion(false) + completion(false, nil) } return } @@ -297,21 +314,21 @@ private final class VideoRecorderImpl { self.assetWriter.finishWriting { if let _ = self.assetWriter.error { DispatchQueue.main.async { - completion(false) + completion(false, nil) } } else { DispatchQueue.main.async { - completion(true) + completion(true, self.transitionImage) } } } } else if let _ = self.assetWriter.error { DispatchQueue.main.async { - completion(true) + completion(false, nil) } } else { DispatchQueue.main.async { - completion(true) + completion(false, nil) } } } @@ -390,7 +407,7 @@ public final class VideoRecorder { case generic } - case success + case success(UIImage?) case initError(Error) case writeError(Error) case finishError(Error) @@ -431,10 +448,10 @@ public final class VideoRecorder { return nil } self.impl = impl - impl.completion = { [weak self] success in + impl.completion = { [weak self] result, transitionImage in if let self { - if success { - self.completion(.success) + if result { + self.completion(.success(transitionImage)) } else { self.completion(.finishError(.generic)) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d0a80689ee..7c5717ab87 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2387,40 +2387,54 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var items: [ContextMenuItem] = [] - + //TODO:localize - items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] c, _ in - c.dismiss(completion: { - guard let self else { - return - } - - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in + if peer.id == self.context.account.peerId { + items.append(.action(ContextMenuActionItem(text: "Add Story", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c.dismiss(completion: { guard let self else { return } - guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + + self.openStoryCamera() + }) + }))) + } else { + items.append(.action(ContextMenuActionItem(text: "View Profile", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c.dismiss(completion: { + guard let self else { return } - (self.navigationController as? NavigationController)?.pushViewController(controller) + + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (self.navigationController as? NavigationController)?.pushViewController(controller) + }) }) - }) - }))) - items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - }))) - items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) - }, action: { _, f in - f(.default) - }))) + }))) + items.append(.action(ContextMenuActionItem(text: "Mute", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + }))) + items.append(.action(ContextMenuActionItem(text: "Archive", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) + }, action: { _, f in + f(.default) + }))) + } let controller = ContextController(account: self.context.account, presentationData: self.presentationData, source: .extracted(ChatListHeaderBarContextExtractedContentSource(controller: self, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture) self.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) @@ -2451,7 +2465,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.fullScreenEffectView = nil fullScreenEffectView.removeFromSuperview() } - + if let value = RippleEffectView(centerLocation: centerLocation, completion: { [weak self] in guard let self else { return @@ -2468,6 +2482,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + private(set) var storyUploadProgress: Float? + public func updateStoryUploadProgress(_ progress: Float?) { + self.storyUploadProgress = progress.flatMap { max(0.027, min(0.99, $0)) } + self.chatListDisplayNode.requestNavigationBarLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) + } + + public func scrollToStories() { + + } + private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { var tabContainerOffset: CGFloat = 0.0 if !self.displayNavigationBar { @@ -5194,6 +5218,7 @@ private final class ChatListLocationContext { if stateAndFilterId.state.editing { if case .chatList(.root) = self.location { self.rightButton = nil + self.storyButton = nil } let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle @@ -5208,6 +5233,7 @@ private final class ChatListLocationContext { } else if isReorderingTabs { if case .chatList(.root) = self.location { self.rightButton = nil + self.storyButton = nil } self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( content: .text(title: presentationData.strings.Common_Done, isBold: true), @@ -5278,6 +5304,16 @@ private final class ChatListLocationContext { ))) } } + + self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent( + content: .icon(imageName: "Chat List/AddStoryIcon"), + pressed: { [weak self] _ in + guard let self, let parentController = self.parentController else { + return + } + parentController.openStoryCamera() + } + ))) } else { self.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( content: .text(title: presentationData.strings.Common_Edit, isBold: false), @@ -5332,16 +5368,6 @@ private final class ChatListLocationContext { self.proxyButton = nil } - self.storyButton = AnyComponentWithIdentity(id: "story", component: AnyComponent(NavigationButtonComponent( - content: .icon(imageName: "Chat List/AddStoryIcon"), - pressed: { [weak self] _ in - guard let self, let parentController = self.parentController else { - return - } - parentController.openStoryCamera() - } - ))) - self.chatListTitle = titleContent if case .chatList(.root) = self.location, checkProxy { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 598b95f6dd..c949ad3061 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1852,6 +1852,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, storySubscriptions: self.controller?.storySubscriptions, + uploadProgress: self.controller?.storyUploadProgress, tabsNode: tabsNode, tabsNodeIsSearch: tabsNodeIsSearch, activateSearch: { [weak self] searchContentNode in @@ -1933,6 +1934,13 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } + func requestNavigationBarLayout(transition: ContainedViewLayoutTransition) { + guard let (layout, _, _, _, _) = self.containerLayout else { + return + } + let _ = self.updateNavigationBar(layout: layout, transition: transition) + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) { var navigationBarHeight = navigationBarHeight var visualNavigationHeight = visualNavigationHeight diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 5ddc68bff8..769f7b45e3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1683,6 +1683,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var currentCredibilityIconContent: EmojiStatusComponent.Content? var currentSecretIconImage: UIImage? var currentForwardedIcon: UIImage? + var currentStoryIcon: UIImage? var selectableControlSizeAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -1805,6 +1806,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var forumThread: (id: Int64, title: String, iconId: Int64?, iconColor: Int32, isUnread: Bool)? var displayForwardedIcon = false + var displayStoryReplyIcon = false switch contentData { case let .chat(itemPeer, _, _, _, text, spoilers, customEmojiRanges): @@ -1983,6 +1985,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if let forwardInfo = message.forwardInfo, !forwardInfo.flags.contains(.isImported) { displayForwardedIcon = true + } else if let _ = message.attributes.first(where: { $0 is ReplyStoryAttribute }) { + displayStoryReplyIcon = true } var displayMediaPreviews = true @@ -2070,6 +2074,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { currentForwardedIcon = PresentationResourcesChatList.forwardedIcon(item.presentationData.theme) } + if displayStoryReplyIcon { + currentStoryIcon = PresentationResourcesChatList.storyReplyIcon(item.presentationData.theme) + } + if let currentForwardedIcon { textLeftCutout += currentForwardedIcon.size.width if !contentImageSpecs.isEmpty { @@ -2079,6 +2087,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + if let currentStoryIcon { + textLeftCutout += currentStoryIcon.size.width + if !contentImageSpecs.isEmpty { + textLeftCutout += forwardedIconSpacing + } else { + textLeftCutout += contentImageTrailingSpace + } + } + for i in 0 ..< contentImageSpecs.count { if i != 0 { textLeftCutout += contentImageSpacing @@ -3337,13 +3354,22 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var mediaPreviewOffset = textNodeFrame.origin.offsetBy(dx: 1.0, dy: floor((measureLayout.size.height - contentImageSize.height) / 2.0)) - if let currentForwardedIcon = currentForwardedIcon { - strongSelf.forwardedIconNode.image = currentForwardedIcon + var messageTypeIcon: UIImage? + var messageTypeIconOffset = mediaPreviewOffset + if let currentForwardedIcon { + messageTypeIcon = currentForwardedIcon + messageTypeIconOffset.y += 3.0 + } else if let currentStoryIcon { + messageTypeIcon = currentStoryIcon + } + + if let messageTypeIcon { + strongSelf.forwardedIconNode.image = messageTypeIcon if strongSelf.forwardedIconNode.supernode == nil { strongSelf.mainContentContainerNode.addSubnode(strongSelf.forwardedIconNode) } - transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: CGPoint(x: mediaPreviewOffset.x, y: mediaPreviewOffset.y + 3.0), size: currentForwardedIcon.size)) - mediaPreviewOffset.x += currentForwardedIcon.size.width + forwardedIconSpacing + transition.updateFrame(node: strongSelf.forwardedIconNode, frame: CGRect(origin: messageTypeIconOffset, size: messageTypeIcon.size)) + mediaPreviewOffset.x += messageTypeIcon.size.width + forwardedIconSpacing } else if strongSelf.forwardedIconNode.supernode != nil { strongSelf.forwardedIconNode.removeFromSupernode() } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 6441f10783..35f280a78b 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -881,6 +881,52 @@ public struct Transition { } } + public func setShapeLayerStrokeStart(layer: CAShapeLayer, strokeStart: CGFloat, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.strokeStart = strokeStart + completion?(true) + case let .curve(duration, curve): + let previousStrokeStart = layer.strokeStart + layer.strokeStart = strokeStart + + layer.animate( + from: previousStrokeStart as NSNumber, + to: strokeStart as NSNumber, + keyPath: "strokeStart", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + + public func setShapeLayerStrokeEnd(layer: CAShapeLayer, strokeEnd: CGFloat, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.strokeEnd = strokeEnd + completion?(true) + case let .curve(duration, curve): + let previousStrokeEnd = layer.strokeEnd + layer.strokeEnd = strokeEnd + + layer.animate( + from: previousStrokeEnd as NSNumber, + to: strokeEnd as NSNumber, + keyPath: "strokeEnd", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + public func setShapeLayerFillColor(layer: CAShapeLayer, color: UIColor, completion: ((Bool) -> Void)? = nil) { if let current = layer.layerTintColor, current == color.cgColor { completion?(true) diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 3d1f8a3e1f..3f51ef98bf 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -58,7 +58,7 @@ public final class RoundedRectangle: Component { } self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius)) UIGraphicsEndImageContext() - } else if component.colors.count > 1{ + } else if component.colors.count > 1 { let imageSize = availableSize UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0) if let context = UIGraphicsGetCurrentContext() { diff --git a/submodules/Components/LottieAnimationComponent/BUILD b/submodules/Components/LottieAnimationComponent/BUILD index dc859a23d0..b582942411 100644 --- a/submodules/Components/LottieAnimationComponent/BUILD +++ b/submodules/Components/LottieAnimationComponent/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/Display:Display", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", + "//submodules/GZip:GZip", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index e43bbb486d..30eee8d303 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -4,6 +4,7 @@ import Lottie import AppBundle import HierarchyTrackingLayer import Display +import GZip public final class LottieAnimationComponent: Component { public struct AnimationItem: Equatable { @@ -176,7 +177,14 @@ public final class LottieAnimationComponent: Component { self.didPlayToCompletion = false self.currentCompletion = nil - if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let animation = Animation.filepath(url.path) { + var animation: Animation? + if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) { + animation = maybeAnimation + } else if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) { + animation = try? Animation.from(data: unpackedData, strategy: .codable) + } + + if let animation { let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable)) switch component.animation.mode { case .still, .animateTransitionFromPrevious: diff --git a/submodules/Display/Source/Font.swift b/submodules/Display/Source/Font.swift index 8d209eea97..722baf2618 100644 --- a/submodules/Display/Source/Font.swift +++ b/submodules/Display/Source/Font.swift @@ -42,6 +42,7 @@ public struct Font { public enum Weight { case regular + case thin case light case medium case semibold @@ -59,6 +60,8 @@ public struct Font { var weight: UIFont.Weight { switch self { + case .thin: + return .thin case .light: return .light case .medium: @@ -78,6 +81,8 @@ public struct Font { switch self { case .regular: return "regular" + case .thin: + return "thin" case .light: return "light" case .medium: diff --git a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift index 756afbdc43..585b913720 100644 --- a/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingBubbleEntity.swift @@ -176,21 +176,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe self.addGestureRecognizer(panGestureRecognizer) self.panGestureRecognizer = panGestureRecognizer - self.snapTool.onSnapXUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) - } - } - - self.snapTool.onSnapYUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToYAxis(snapped) - } - } - - self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToAngle(snappedAngle) + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) } } } @@ -230,11 +218,13 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe for layer in sublayers { if layer.frame.contains(location) { self.currentHandle = layer + entityView.onInteractionUpdated(true) return } } } self.currentHandle = self.layer + entityView.onInteractionUpdated(true) case .changed: let delta = gestureRecognizer.translation(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview) @@ -283,7 +273,7 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe updatedPosition.x += delta.x updatedPosition.y += delta.y - updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) } entity.size = updatedSize @@ -292,10 +282,9 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe entityView.update(animated: false) gestureRecognizer.setTranslation(.zero, in: entityView) - case .ended: - self.snapTool.reset() - case .cancelled: + case .ended, .cancelled: self.snapTool.reset() + entityView.onInteractionUpdated(false) default: break } @@ -310,11 +299,16 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe switch gestureRecognizer.state { case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } let scale = gestureRecognizer.scale entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) entityView.update() gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) default: break } @@ -332,12 +326,14 @@ final class DrawingBubbleEntititySelectionView: DrawingEntitySelectionView, UIGe switch gestureRecognizer.state { case .began: self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation gestureRecognizer.rotation = 0.0 case .ended, .cancelled: + entityView.onInteractionUpdated(false) self.snapTool.rotationReset() default: break diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 50223ffebb..fd0ebbf2d0 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -56,6 +56,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private var tapGestureRecognizer: UITapGestureRecognizer! public private(set) var selectedEntityView: DrawingEntityView? + public var getEntityEdgePositions: () -> UIEdgeInsets? = { return nil } public var getEntityCenterPosition: () -> CGPoint = { return .zero } public var getEntityInitialRotation: () -> CGFloat = { return 0.0 } public var getEntityAdditionalScale: () -> CGFloat = { return 1.0 } @@ -66,10 +67,18 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { var entityAdded: (DrawingEntity) -> Void = { _ in } var entityRemoved: (DrawingEntity) -> Void = { _ in } - + + private let topEdgeView = UIView() + private let leftEdgeView = UIView() + private let rightEdgeView = UIView() + private let bottomEdgeView = UIView() private let xAxisView = UIView() private let yAxisView = UIView() private let angleLayer = SimpleShapeLayer() + + public var onInteractionUpdated: (Bool) -> Void = { _ in } + public var edgePreviewUpdated: (Bool) -> Void = { _ in } + private let hapticFeedback = HapticFeedback() public init(context: AccountContext, size: CGSize) { @@ -82,6 +91,22 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.addGestureRecognizer(tapGestureRecognizer) self.tapGestureRecognizer = tapGestureRecognizer + self.topEdgeView.alpha = 0.0 + self.topEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.topEdgeView.isUserInteractionEnabled = false + + self.leftEdgeView.alpha = 0.0 + self.leftEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.leftEdgeView.isUserInteractionEnabled = false + + self.rightEdgeView.alpha = 0.0 + self.rightEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.rightEdgeView.isUserInteractionEnabled = false + + self.bottomEdgeView.alpha = 0.0 + self.bottomEdgeView.backgroundColor = UIColor(rgb: 0x5fc1f0) + self.bottomEdgeView.isUserInteractionEnabled = false + self.xAxisView.alpha = 0.0 self.xAxisView.backgroundColor = UIColor(rgb: 0x5fc1f0) self.xAxisView.isUserInteractionEnabled = false @@ -94,6 +119,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.opacity = 0.0 self.angleLayer.lineDashPattern = [12, 12] as [NSNumber] + self.addSubview(self.topEdgeView) + self.addSubview(self.leftEdgeView) + self.addSubview(self.rightEdgeView) + self.addSubview(self.bottomEdgeView) + self.addSubview(self.xAxisView) self.addSubview(self.yAxisView) self.layer.addSublayer(self.angleLayer) @@ -103,28 +133,41 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { fatalError("init(coder:) has not been implemented") } - deinit { - print() - } - public override func layoutSubviews() { super.layoutSubviews() + let referenceSize = self.convert(CGRect(origin: .zero, size: CGSize(width: 1.0 + UIScreenPixel, height: 1.0)), from: nil) + let width = ceil(referenceSize.width) + + if let edges = self.getEntityEdgePositions() { + self.topEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) + self.topEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.top) + + self.bottomEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) + self.bottomEdgeView.center = CGPoint(x: self.bounds.width / 2.0, y: edges.bottom) + + self.leftEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0)) + self.leftEdgeView.center = CGPoint(x: edges.left, y: self.bounds.height / 2.0) + + self.rightEdgeView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0)) + self.rightEdgeView.center = CGPoint(x: edges.right, y: self.bounds.height / 2.0) + } + let point = self.getEntityCenterPosition() - self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 6.0, height: 3000.0)) + self.xAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: width, height: 3000.0)) self.xAxisView.center = point self.xAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation()) - self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0)) + self.yAxisView.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) self.yAxisView.center = point self.yAxisView.transform = CGAffineTransform(rotationAngle: self.getEntityInitialRotation()) let anglePath = CGMutablePath() - anglePath.move(to: CGPoint(x: 0.0, y: 3.0)) - anglePath.addLine(to: CGPoint(x: 3000.0, y: 3.0)) + anglePath.move(to: CGPoint(x: 0.0, y: width / 2.0)) + anglePath.addLine(to: CGPoint(x: 3000.0, y: width / 2.0)) self.angleLayer.path = anglePath - self.angleLayer.lineWidth = 6.0 - self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: 6.0)) + self.angleLayer.lineWidth = width + self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) } public var entities: [DrawingEntity] { @@ -286,57 +329,66 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } view.containerView = self - view.onSnapToXAxis = { [weak self, weak view] snappedToX in - guard let strongSelf = self, let strongView = view else { + let processSnap: (Bool, UIView) -> Void = { [weak self] snapped, snapView in + guard let self else { return } let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - if snappedToX { - strongSelf.insertSubview(strongSelf.xAxisView, belowSubview: strongView) - if strongSelf.xAxisView.alpha < 1.0 { - strongSelf.hapticFeedback.impact(.light) + if snapped { + self.insertSubview(snapView, belowSubview: view) + if snapView.alpha < 1.0 { + self.hapticFeedback.impact(.light) } - transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 1.0) + transition.updateAlpha(layer: snapView.layer, alpha: 1.0) } else { - transition.updateAlpha(layer: strongSelf.xAxisView.layer, alpha: 0.0) + transition.updateAlpha(layer: snapView.layer, alpha: 0.0) } } - view.onSnapToYAxis = { [weak self, weak view] snappedToY in - guard let strongSelf = self, let strongView = view else { + + view.onSnapUpdated = { [weak self, weak view] type, snapped in + guard let self else { return } - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - if snappedToY { - strongSelf.insertSubview(strongSelf.yAxisView, belowSubview: strongView) - if strongSelf.yAxisView.alpha < 1.0 { - strongSelf.hapticFeedback.impact(.light) + switch type { + case .centerX: + processSnap(snapped, self.xAxisView) + case .centerY: + processSnap(snapped, self.yAxisView) + case .top: + processSnap(snapped, self.topEdgeView) + self.edgePreviewUpdated(snapped) + case .left: + processSnap(snapped, self.leftEdgeView) + self.edgePreviewUpdated(snapped) + case .right: + processSnap(snapped, self.rightEdgeView) + self.edgePreviewUpdated(snapped) + case .bottom: + processSnap(snapped, self.bottomEdgeView) + self.edgePreviewUpdated(snapped) + case let .rotation(angle): + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if let angle, let view { + self.layer.insertSublayer(self.angleLayer, below: view.layer) + self.angleLayer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0) + if self.angleLayer.opacity < 1.0 { + self.hapticFeedback.impact(.light) + } + transition.updateAlpha(layer: self.angleLayer, alpha: 1.0) + } else { + transition.updateAlpha(layer: self.angleLayer, alpha: 0.0) } - transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 1.0) - } else { - transition.updateAlpha(layer: strongSelf.yAxisView.layer, alpha: 0.0) - } - } - view.onSnapToAngle = { [weak self, weak view] snappedToAngle in - guard let strongSelf = self, let strongView = view else { - return - } - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - if let snappedToAngle { - strongSelf.layer.insertSublayer(strongSelf.angleLayer, below: strongView.layer) - strongSelf.angleLayer.transform = CATransform3DMakeRotation(snappedToAngle, 0.0, 0.0, 1.0) - if strongSelf.angleLayer.opacity < 1.0 { - strongSelf.hapticFeedback.impact(.light) - } - transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 1.0) - } else { - transition.updateAlpha(layer: strongSelf.angleLayer, alpha: 0.0) } } view.onPositionUpdated = { [weak self] position in - guard let strongSelf = self else { - return + if let self { + self.angleLayer.position = position + } + } + view.onInteractionUpdated = { [weak self] interacting in + if let self { + self.onInteractionUpdated(interacting) } - strongSelf.angleLayer.position = position } view.update() @@ -603,10 +655,9 @@ public class DrawingEntityView: UIView { public weak var selectionView: DrawingEntitySelectionView? weak var containerView: DrawingEntitiesView? - var onSnapToXAxis: (Bool) -> Void = { _ in } - var onSnapToYAxis: (Bool) -> Void = { _ in } - var onSnapToAngle: (CGFloat?) -> Void = { _ in } + var onSnapUpdated: (DrawingEntitySnapTool.SnapType, Bool) -> Void = { _, _ in } var onPositionUpdated: (CGPoint) -> Void = { _ in } + var onInteractionUpdated: (Bool) -> Void = { _ in } init(context: AccountContext, entity: DrawingEntity) { self.context = context diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index bc73d07eb8..a21d50d6a3 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -145,20 +145,26 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia } } + private var beganRotating = false @objc func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { var updatedRotation = self.mediaEntity.rotation var rotation: CGFloat = 0.0 switch gestureRecognizer.state { case .began: - break + self.beganRotating = false case .changed: rotation = gestureRecognizer.rotation - updatedRotation += rotation - - gestureRecognizer.rotation = 0.0 + if self.beganRotating || abs(rotation) >= 0.08 * .pi || abs(self.mediaEntity.rotation) >= 0.03 { + if !self.beganRotating { + self.beganRotating = true + } else { + updatedRotation += rotation + } + gestureRecognizer.rotation = 0.0 + } case .ended, .cancelled: - break + self.beganRotating = false default: break } diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index d2d931bf30..555862c2a7 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -1137,10 +1137,18 @@ private final class DrawingScreenComponent: CombinedComponent { presentColorPicker(state.currentColor) } } + + var controlsVisible = true + if state.drawingViewState.isDrawing { + controlsVisible = false + } + let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778)) + let previewTopInset: CGFloat = floorToScreenPixels(context.availableSize.height - previewSize.height) / 2.0 + var topInset = environment.safeInsets.top + 31.0 if component.sourceHint == .storyEditor { - topInset += 75.0 + topInset = previewTopInset + 31.0 } let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0 @@ -1178,6 +1186,7 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(bottomGradient .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomGradient.size.height / 2.0)) + .opacity(controlsVisible ? 1.0 : 0.0) ) if let textEntity = state.selectedEntity as? DrawingTextEntity { @@ -1318,6 +1327,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1345,6 +1355,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1372,6 +1383,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1399,6 +1411,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1426,6 +1439,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta delay += 0.025 @@ -1454,6 +1468,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1481,6 +1496,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) offsetX += delta @@ -1508,13 +1524,14 @@ private final class DrawingScreenComponent: CombinedComponent { completion() }) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) if state.selectedEntity is DrawingStickerEntity || state.selectedEntity is DrawingTextEntity { } else { let tools = tools.update( component: ToolsComponent( - state: component.isVideo ? state.drawingState.forVideo() : state.drawingState, + state: component.isVideo || component.sourceHint == .storyEditor ? state.drawingState.forVideo() : state.drawingState, isFocused: false, tag: toolsTag, toolPressed: { [weak state] tool in @@ -1551,6 +1568,7 @@ private final class DrawingScreenComponent: CombinedComponent { completion() } })) + .opacity(controlsVisible ? 1.0 : 0.0) ) } @@ -1725,7 +1743,7 @@ private final class DrawingScreenComponent: CombinedComponent { ) context.add(textSize .position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0)) - .opacity(sizeSliderVisible ? 1.0 : 0.0) + .opacity(sizeSliderVisible && controlsVisible ? 1.0 : 0.0) ) let undoButton = undoButton.update( @@ -1745,7 +1763,7 @@ private final class DrawingScreenComponent: CombinedComponent { context.add(undoButton .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) - .opacity(isEditingText ? 0.0 : 1.0) + .opacity(isEditingText || !controlsVisible ? 0.0 : 1.0) ) @@ -1765,7 +1783,7 @@ private final class DrawingScreenComponent: CombinedComponent { context.add(redoButton .position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset)) .scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01) - .opacity(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.0) + .opacity(state.drawingViewState.canRedo && !isEditingText && controlsVisible ? 1.0 : 0.0) ) let clearAllButton = clearAllButton.update( @@ -1785,9 +1803,16 @@ private final class DrawingScreenComponent: CombinedComponent { context.add(clearAllButton .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset)) .scale(isEditingText ? 0.01 : 1.0) - .opacity(isEditingText ? 0.0 : 1.0) + .opacity(isEditingText || !controlsVisible ? 0.0 : 1.0) ) + let textButtonTopInset: CGFloat + if let sourceHint = component.sourceHint, case .storyEditor = sourceHint { + textButtonTopInset = environment.statusBarHeight + } else { + textButtonTopInset = topInset + } + let textCancelButton = textCancelButton.update( component: Button( content: AnyComponent( @@ -1823,7 +1848,7 @@ private final class DrawingScreenComponent: CombinedComponent { transition: context.transition ) context.add(textDoneButton - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset)) + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: textButtonTopInset)) .scale(isEditingText ? 1.0 : 0.01) .opacity(isEditingText ? 1.0 : 0.0) ) @@ -1870,6 +1895,7 @@ private final class DrawingScreenComponent: CombinedComponent { .position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) + .opacity(controlsVisible ? 1.0 : 0.0) ) let modeRightInset: CGFloat = 57.0 @@ -1919,6 +1945,7 @@ private final class DrawingScreenComponent: CombinedComponent { .appear(.default(scale: true)) .disappear(.default(scale: true)) .cornerRadius(12.0) + .opacity(controlsVisible ? 1.0 : 0.0) ) let doneButton = doneButton.update( @@ -1938,7 +1965,8 @@ private final class DrawingScreenComponent: CombinedComponent { var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { - doneButtonPosition = doneButtonPosition.offsetBy(dx: -2.0, dy: 0.0) + doneButtonPosition.x = doneButtonPosition.x - 2.0 + doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + doneButton.size.height / 2.0) } context.add(doneButton .position(doneButtonPosition) @@ -1955,6 +1983,7 @@ private final class DrawingScreenComponent: CombinedComponent { }) transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true) }) + .opacity(controlsVisible ? 1.0 : 0.0) ) let selectedIndex: Int @@ -2013,9 +2042,13 @@ private final class DrawingScreenComponent: CombinedComponent { availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height), transition: context.transition ) - let modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0) + var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0) + if component.sourceHint == .storyEditor { + modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 8.0 + modeAndSize.size.height / 2.0) + } context.add(modeAndSize .position(modeAndSizePosition) + .opacity(controlsVisible ? 1.0 : 0.0) ) var animatingOut = false @@ -2049,10 +2082,12 @@ private final class DrawingScreenComponent: CombinedComponent { ) var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { - backButtonPosition = backButtonPosition.offsetBy(dx: 2.0, dy: 0.0) + backButtonPosition.x = backButtonPosition.x + 2.0 + backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewTopInset + 3.0 + backButton.size.height / 2.0) } context.add(backButton .position(backButtonPosition) + .opacity(controlsVisible ? 1.0 : 0.0) ) return context.availableSize @@ -3019,6 +3054,23 @@ public final class DrawingToolsInteraction { } } + public func endTextEditing(reset: Bool) { + if let entityView = self.entitiesView.selectedEntityView as? DrawingTextEntityView { + entityView.endEditing(reset: reset) + } + } + + public func updateEntitySize(_ size: CGFloat) { + if let selectedEntityView = self.entitiesView.selectedEntityView { + if let textEntity = selectedEntityView.entity as? DrawingTextEntity { + textEntity.fontSize = size + } else { + selectedEntityView.entity.lineWidth = size + } + selectedEntityView.update() + } + } + func presentEyedropper(retryLaterForVideo: Bool = true, dismissed: @escaping () -> Void) { // self.entitiesView.pause() // diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift index 8bf8a45e5e..74a0c722bc 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntity.swift @@ -185,21 +185,9 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, self.addGestureRecognizer(panGestureRecognizer) self.panGestureRecognizer = panGestureRecognizer - self.snapTool.onSnapXUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) - } - } - - self.snapTool.onSnapYUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToYAxis(snapped) - } - } - - self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToAngle(snappedAngle) + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) } } } @@ -240,11 +228,13 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, for layer in sublayers { if layer.frame.contains(location) { self.currentHandle = layer + entityView.onInteractionUpdated(true) return } } } self.currentHandle = self.layer + entityView.onInteractionUpdated(true) case .changed: let delta = gestureRecognizer.translation(in: entityView.superview) let velocity = gestureRecognizer.velocity(in: entityView.superview) @@ -337,7 +327,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, updatedPosition.x += delta.x updatedPosition.y += delta.y - updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) } entity.size = updatedSize @@ -345,10 +335,9 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, entityView.update(animated: false) gestureRecognizer.setTranslation(.zero, in: entityView) - case .ended: - self.snapTool.reset() - case .cancelled: + case .ended, .cancelled: self.snapTool.reset() + entityView.onInteractionUpdated(false) default: break } @@ -363,11 +352,16 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, switch gestureRecognizer.state { case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } let scale = gestureRecognizer.scale entity.size = CGSize(width: entity.size.width * scale, height: entity.size.height * scale) entityView.update() gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) default: break } @@ -385,6 +379,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, switch gestureRecognizer.state { case .began: self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation @@ -392,6 +387,7 @@ final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView, gestureRecognizer.rotation = 0.0 case .ended, .cancelled: self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) default: break } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index ae4e248e84..3b1dfec20d 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -310,21 +310,9 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG self.addGestureRecognizer(panGestureRecognizer) self.panGestureRecognizer = panGestureRecognizer - self.snapTool.onSnapXUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) - } - } - - self.snapTool.onSnapYUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToYAxis(snapped) - } - } - - self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToAngle(snappedAngle) + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) } } } @@ -365,11 +353,13 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG if layer.frame.contains(location) { self.currentHandle = layer self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) return } } } self.currentHandle = self.layer + entityView.onInteractionUpdated(true) case .changed: let delta = gestureRecognizer.translation(in: entityView.superview) let parentLocation = gestureRecognizer.location(in: self.superview) @@ -399,7 +389,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG updatedPosition.x += delta.x updatedPosition.y += delta.y - updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) } entity.position = updatedPosition @@ -413,6 +403,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG if self.currentHandle != nil { self.snapTool.rotationReset() } + entityView.onInteractionUpdated(false) default: break } @@ -427,11 +418,16 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG switch gestureRecognizer.state { case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } let scale = gestureRecognizer.scale entity.scale = entity.scale * scale entityView.update() gestureRecognizer.scale = 1.0 + case .cancelled, .ended: + entityView.onInteractionUpdated(false) default: break } @@ -449,6 +445,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG switch gestureRecognizer.state { case .began: self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation @@ -456,6 +453,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG gestureRecognizer.rotation = 0.0 case .ended, .cancelled: self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) default: break } @@ -509,32 +507,77 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView, UIG private let snapTimeout = 1.0 class DrawingEntitySnapTool { - private var xState: (skipped: CGFloat, waitForLeave: Bool)? - private var yState: (skipped: CGFloat, waitForLeave: Bool)? + enum SnapType { + case centerX + case centerY + case top + case left + case right + case bottom + case rotation(CGFloat?) + + static var allPositionTypes: [SnapType] { + return [ + .centerX, + .centerY, + .top, + .left, + .right, + .bottom + ] + } + } + + struct SnapState { + let skipped: CGFloat + let waitForLeave: Bool + } + + private var topEdgeState: SnapState? + private var leftEdgeState: SnapState? + private var rightEdgeState: SnapState? + private var bottomEdgeState: SnapState? + + private var xState: SnapState? + private var yState: SnapState? + private var rotationState: (angle: CGFloat, skipped: CGFloat, waitForLeave: Bool)? - var onSnapXUpdated: (Bool) -> Void = { _ in } - var onSnapYUpdated: (Bool) -> Void = { _ in } - var onSnapRotationUpdated: (CGFloat?) -> Void = { _ in } + var onSnapUpdated: (SnapType, Bool) -> Void = { _, _ in } + + var previousTopEdgeSnapTimestamp: Double? + var previousLeftEdgeSnapTimestamp: Double? + var previousRightEdgeSnapTimestamp: Double? + var previousBottomEdgeSnapTimestamp: Double? var previousXSnapTimestamp: Double? var previousYSnapTimestamp: Double? var previousRotationSnapTimestamp: Double? func reset() { + self.topEdgeState = nil + self.leftEdgeState = nil + self.rightEdgeState = nil + self.bottomEdgeState = nil self.xState = nil self.yState = nil - self.onSnapXUpdated(false) - self.onSnapYUpdated(false) + for type in SnapType.allPositionTypes { + self.onSnapUpdated(type, false) + } } func rotationReset() { self.rotationState = nil - self.onSnapRotationUpdated(nil) + self.onSnapUpdated(.rotation(nil), false) } func maybeSkipFromStart(entityView: DrawingEntityView, position: CGPoint) { + self.topEdgeState = nil + self.leftEdgeState = nil + self.rightEdgeState = nil + self.bottomEdgeState = nil + self.xState = nil self.yState = nil @@ -543,94 +586,222 @@ class DrawingEntitySnapTool { if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { if position.x > snapLocation.x - snapXDelta && position.x < snapLocation.x + snapXDelta { - self.xState = (0.0, true) + self.xState = SnapState(skipped: 0.0, waitForLeave: true) } if position.y > snapLocation.y - snapYDelta && position.y < snapLocation.y + snapYDelta { - self.yState = (0.0, true) + self.yState = SnapState(skipped: 0.0, waitForLeave: true) } } } - func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint) -> CGPoint { + func update(entityView: DrawingEntityView, velocity: CGPoint, delta: CGPoint, updatedPosition: CGPoint, size: CGSize) -> CGPoint { var updatedPosition = updatedPosition + guard let snapCenterLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() else { + return updatedPosition + } + let snapEdgeLocations = (entityView.superview as? DrawingEntitiesView)?.getEntityEdgePositions() + let currentTimestamp = CACurrentMediaTime() - let snapXDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 - let snapXVelocity: CGFloat = snapXDelta * 12.0 - let snapXSkipTranslation: CGFloat = snapXDelta * 2.0 + let snapDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 + let snapVelocity: CGFloat = snapDelta * 12.0 + let snapSkipTranslation: CGFloat = snapDelta * 2.0 - if abs(velocity.x) < snapXVelocity || self.xState?.waitForLeave == true { - if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { - if let (skipped, waitForLeave) = self.xState { - if waitForLeave { - if updatedPosition.x > snapLocation.x - snapXDelta * 2.0 && updatedPosition.x < snapLocation.x + snapXDelta * 2.0 { - + let topPoint = updatedPosition.y - size.height / 2.0 + let leftPoint = updatedPosition.x - size.width / 2.0 + let rightPoint = updatedPosition.x + size.width / 2.0 + let bottomPoint = updatedPosition.y + size.height / 2.0 + + func process( + state: SnapState?, + velocity: CGFloat, + delta: CGFloat, + value: CGFloat, + snapVelocity: CGFloat, + snapToValue: CGFloat?, + snapDelta: CGFloat, + snapSkipTranslation: CGFloat, + previousSnapTimestamp: Double?, + onSnapUpdated: (Bool) -> Void + ) -> ( + value: CGFloat, + state: SnapState?, + snapTimestamp: Double? + ) { + var updatedValue = value + var updatedState = state + var updatedPreviousSnapTimestamp = previousSnapTimestamp + if abs(velocity) < snapVelocity || state?.waitForLeave == true { + if let snapToValue { + if let state { + let skipped = state.skipped + let waitForLeave = state.waitForLeave + if waitForLeave { + if value > snapToValue - snapDelta * 2.0 && value < snapToValue + snapDelta * 2.0 { + + } else { + updatedState = nil + } + } else if abs(skipped) < snapSkipTranslation { + updatedState = SnapState(skipped: skipped + delta, waitForLeave: false) + updatedValue = snapToValue } else { - self.xState = nil + updatedState = SnapState(skipped: snapSkipTranslation, waitForLeave: true) + onSnapUpdated(false) } - } else if abs(skipped) < snapXSkipTranslation { - self.xState = (skipped + delta.x, false) - updatedPosition.x = snapLocation.x } else { - self.xState = (snapXSkipTranslation, true) - self.onSnapXUpdated(false) - } - } else { - if updatedPosition.x > snapLocation.x - snapXDelta && updatedPosition.x < snapLocation.x + snapXDelta { - if let previousXSnapTimestamp, currentTimestamp - previousXSnapTimestamp < snapTimeout { - - } else { - self.previousXSnapTimestamp = currentTimestamp - self.xState = (0.0, false) - updatedPosition.x = snapLocation.x - self.onSnapXUpdated(true) + if value > snapToValue - snapDelta && value < snapToValue + snapDelta { + if let previousSnapTimestamp, currentTimestamp - previousSnapTimestamp < snapTimeout { + + } else { + updatedPreviousSnapTimestamp = currentTimestamp + updatedState = SnapState(skipped: 0.0, waitForLeave: false) + updatedValue = snapToValue + onSnapUpdated(true) + } } } } + } else { + updatedState = nil + onSnapUpdated(false) } - } else { - self.xState = nil - self.onSnapXUpdated(false) + return (updatedValue, updatedState, updatedPreviousSnapTimestamp) } - let snapYDelta: CGFloat = (entityView.superview?.frame.width ?? 0.0) * 0.02 - let snapYVelocity: CGFloat = snapYDelta * 12.0 - let snapYSkipTranslation: CGFloat = snapYDelta * 2.0 + let (updatedXValue, updatedXState, updatedXPreviousTimestamp) = process( + state: self.xState, + velocity: velocity.x, + delta: delta.x, + value: updatedPosition.x, + snapVelocity: snapVelocity, + snapToValue: snapCenterLocation.x, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousXSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.centerX, snapped) + } + ) + self.xState = updatedXState + self.previousXSnapTimestamp = updatedXPreviousTimestamp - if abs(velocity.y) < snapYVelocity || self.yState?.waitForLeave == true { - if let snapLocation = (entityView.superview as? DrawingEntitiesView)?.getEntityCenterPosition() { - if let (skipped, waitForLeave) = self.yState { - if waitForLeave { - if updatedPosition.y > snapLocation.y - snapYDelta * 2.0 && updatedPosition.y < snapLocation.y + snapYDelta * 2.0 { - - } else { - self.yState = nil - } - } else if abs(skipped) < snapYSkipTranslation { - self.yState = (skipped + delta.y, false) - updatedPosition.y = snapLocation.y - } else { - self.yState = (snapYSkipTranslation, true) - self.onSnapYUpdated(false) + let (updatedYValue, updatedYState, updatedYPreviousTimestamp) = process( + state: self.yState, + velocity: velocity.y, + delta: delta.y, + value: updatedPosition.y, + snapVelocity: snapVelocity, + snapToValue: snapCenterLocation.y, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousYSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.centerY, snapped) + } + ) + self.yState = updatedYState + self.previousYSnapTimestamp = updatedYPreviousTimestamp + + if let snapEdgeLocations { + if updatedXState == nil { + let (updatedXLeftEdgeValue, updatedLeftEdgeState, updatedLeftEdgePreviousTimestamp) = process( + state: self.leftEdgeState, + velocity: velocity.x, + delta: delta.x, + value: leftPoint, + snapVelocity: snapVelocity, + snapToValue: snapEdgeLocations.left, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousLeftEdgeSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.left, snapped) } + ) + self.leftEdgeState = updatedLeftEdgeState + self.previousLeftEdgeSnapTimestamp = updatedLeftEdgePreviousTimestamp + + if updatedLeftEdgeState != nil { + updatedPosition.x = updatedXLeftEdgeValue + size.width / 2.0 + + self.rightEdgeState = nil + self.previousRightEdgeSnapTimestamp = nil } else { - if updatedPosition.y > snapLocation.y - snapYDelta && updatedPosition.y < snapLocation.y + snapYDelta { - if let previousYSnapTimestamp, currentTimestamp - previousYSnapTimestamp < snapTimeout { - - } else { - self.previousYSnapTimestamp = currentTimestamp - self.yState = (0.0, false) - updatedPosition.y = snapLocation.y - self.onSnapYUpdated(true) + let (updatedXRightEdgeValue, updatedRightEdgeState, updatedRightEdgePreviousTimestamp) = process( + state: self.rightEdgeState, + velocity: velocity.x, + delta: delta.x, + value: rightPoint, + snapVelocity: snapVelocity, + snapToValue: snapEdgeLocations.right, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousRightEdgeSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.right, snapped) } - } + ) + self.rightEdgeState = updatedRightEdgeState + self.previousRightEdgeSnapTimestamp = updatedRightEdgePreviousTimestamp + + updatedPosition.x = updatedXRightEdgeValue - size.width / 2.0 } + } else { + updatedPosition.x = updatedXValue + } + + if updatedYState == nil { + let (updatedYTopEdgeValue, updatedTopEdgeState, updatedTopEdgePreviousTimestamp) = process( + state: self.topEdgeState, + velocity: velocity.y, + delta: delta.y, + value: topPoint, + snapVelocity: snapVelocity, + snapToValue: snapEdgeLocations.top, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousTopEdgeSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.top, snapped) + } + ) + self.topEdgeState = updatedTopEdgeState + self.previousTopEdgeSnapTimestamp = updatedTopEdgePreviousTimestamp + + if updatedTopEdgeState != nil { + updatedPosition.y = updatedYTopEdgeValue + size.height / 2.0 + + self.bottomEdgeState = nil + self.previousBottomEdgeSnapTimestamp = nil + } else { + let (updatedYBottomEdgeValue, updatedBottomEdgeState, updatedBottomEdgePreviousTimestamp) = process( + state: self.bottomEdgeState, + velocity: velocity.y, + delta: delta.y, + value: bottomPoint, + snapVelocity: snapVelocity, + snapToValue: snapEdgeLocations.bottom, + snapDelta: snapDelta, + snapSkipTranslation: snapSkipTranslation, + previousSnapTimestamp: self.previousBottomEdgeSnapTimestamp, + onSnapUpdated: { [weak self] snapped in + self?.onSnapUpdated(.bottom, snapped) + } + ) + self.bottomEdgeState = updatedBottomEdgeState + self.previousBottomEdgeSnapTimestamp = updatedBottomEdgePreviousTimestamp + + updatedPosition.y = updatedYBottomEdgeValue - size.height / 2.0 + } + } else { + updatedPosition.y = updatedYValue } } else { - self.yState = nil - self.onSnapYUpdated(false) + updatedPosition.x = updatedXValue + updatedPosition.y = updatedYValue } return updatedPosition @@ -679,7 +850,7 @@ class DrawingEntitySnapTool { updatedRotation = snapRotation } else { self.rotationState = (snapRotation, snapSkipRotation, true) - self.onSnapRotationUpdated(nil) + self.onSnapUpdated(.rotation(nil), false) } } else { for snapRotation in self.snapRotations { @@ -691,7 +862,7 @@ class DrawingEntitySnapTool { self.previousRotationSnapTimestamp = currentTimestamp self.rotationState = (snapRotation, 0.0, false) updatedRotation = snapRotation - self.onSnapRotationUpdated(snapRotation) + self.onSnapUpdated(.rotation(snapRotation), true) } break } @@ -699,7 +870,7 @@ class DrawingEntitySnapTool { } } else { self.rotationState = nil - self.onSnapRotationUpdated(nil) + self.onSnapUpdated(.rotation(nil), false) } return updatedRotation diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 1dc511135b..042d030743 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -20,7 +20,7 @@ extension DrawingTextEntity.Alignment { } } -final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { +public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { private var textEntity: DrawingTextEntity { return self.entity as! DrawingTextEntity } @@ -77,7 +77,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { private var isSuspended = false private var _isEditing = false - var isEditing: Bool { + public var isEditing: Bool { return self._isEditing || self.isSuspended } @@ -265,7 +265,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { } } - func textViewDidChange(_ textView: UITextView) { + public func textViewDidChange(_ textView: UITextView) { guard let updatedText = self.textView.attributedText.mutableCopy() as? NSMutableAttributedString else { return } @@ -301,7 +301,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { self.textView.selectedRange = NSMakeRange(previousSelectedRange.location + previousSelectedRange.length + text.length, 0) } - override func sizeThatFits(_ size: CGSize) -> CGSize { + public override func sizeThatFits(_ size: CGSize) -> CGSize { self.textView.setNeedsLayersUpdate() var result = self.textView.sizeThatFits(CGSize(width: self.textEntity.width, height: .greatestFiniteMagnitude)) result.width = max(224.0, ceil(result.width) + 20.0) @@ -309,7 +309,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { return result; } - override func sizeToFit() { + public override func sizeToFit() { let center = self.center let transform = self.transform self.transform = .identity @@ -320,7 +320,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { //entity changed } - override func layoutSubviews() { + public override func layoutSubviews() { super.layoutSubviews() self.textView.frame = self.bounds @@ -506,7 +506,7 @@ final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate { } } - override func update(animated: Bool = false) { + public override func update(animated: Bool = false) { self.update(animated: animated, afterAppendingEmoji: false) } @@ -695,21 +695,9 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest self.addGestureRecognizer(panGestureRecognizer) self.panGestureRecognizer = panGestureRecognizer - self.snapTool.onSnapXUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToXAxis(snapped) - } - } - - self.snapTool.onSnapYUpdated = { [weak self] snapped in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToYAxis(snapped) - } - } - - self.snapTool.onSnapRotationUpdated = { [weak self] snappedAngle in - if let strongSelf = self, let entityView = strongSelf.entityView { - entityView.onSnapToAngle(snappedAngle) + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) } } } @@ -753,11 +741,13 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest if layer.frame.contains(location) { self.currentHandle = layer self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) return } } } self.currentHandle = self.layer + entityView.onInteractionUpdated(true) case .changed: let delta = gestureRecognizer.translation(in: entityView.superview) let parentLocation = gestureRecognizer.location(in: self.superview) @@ -788,7 +778,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest updatedPosition.x += delta.x updatedPosition.y += delta.y - updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition) + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) } entity.scale = updatedScale @@ -802,6 +792,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest if self.currentHandle != nil { self.snapTool.rotationReset() } + entityView.onInteractionUpdated(false) default: break } @@ -816,11 +807,16 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest switch gestureRecognizer.state { case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } let scale = gestureRecognizer.scale entity.scale = max(0.1, entity.scale * scale) entityView.update() gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) default: break } @@ -838,6 +834,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest switch gestureRecognizer.state { case .began: self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) case .changed: rotation = gestureRecognizer.rotation updatedRotation += rotation @@ -845,6 +842,7 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView, UIGest gestureRecognizer.rotation = 0.0 case .ended, .cancelled: self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) default: break } diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift index a17a7daa42..14a66b24b4 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntity.swift @@ -192,6 +192,7 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe for layer in sublayers { if layer.frame.contains(location) { self.currentHandle = layer + entityView.onInteractionUpdated(true) return } } @@ -245,8 +246,8 @@ final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView, UIGe entityView.update(animated: false) gestureRecognizer.setTranslation(.zero, in: entityView) - case .ended: - break + case .ended, .cancelled: + entityView.onInteractionUpdated(false) default: break } diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index e20b53ac63..0df546eabb 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -621,7 +621,7 @@ private func generateKnobImage() -> UIImage? { return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5)) } -final class TextSizeSliderComponent: Component { +public final class TextSizeSliderComponent: Component { let value: CGFloat let tag: AnyObject? let updated: (CGFloat) -> Void @@ -646,7 +646,7 @@ final class TextSizeSliderComponent: Component { return true } - final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { + public final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView { private var validSize: CGSize? private let backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3)) @@ -739,7 +739,7 @@ final class TextSizeSliderComponent: Component { } } - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -787,11 +787,11 @@ final class TextSizeSliderComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View() } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { view.updated = self.updated view.released = self.released return view.updateLayout(size: availableSize, component: self, transition: transition) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 129c4e6265..ab70fb46d1 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -259,14 +259,14 @@ final class MediaPickerGridItemNode: GridItemNode { self.imageNode.setSignal(imageSignal) self.currentDraftState = (draft, index) - self.setNeedsLayout() - if self.typeIconNode.supernode == nil { + if self.draftNode.supernode == nil { self.draftNode.attributedText = NSAttributedString(string: "Draft", font: Font.semibold(12.0), textColor: .white) self.addSubnode(self.draftNode) - self.setNeedsLayout() } + + self.setNeedsLayout() } self.updateSelectionState() @@ -290,6 +290,11 @@ final class MediaPickerGridItemNode: GridItemNode { if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentMediaState!.1 != index { self.currentMediaState = (media.asset, index) + + if self.draftNode.supernode != nil { + self.draftNode.removeFromSupernode() + } + self.setNeedsLayout() } @@ -313,6 +318,7 @@ final class MediaPickerGridItemNode: GridItemNode { } if self.currentState == nil || self.currentState!.0 !== fetchResult || self.currentState!.1 != index { + self.backgroundNode.image = nil let editingContext = interaction.editingState let asset = fetchResult.object(at: index) diff --git a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift index 529471b013..0e354ede1f 100644 --- a/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift +++ b/submodules/Postbox/Sources/MediaBoxFileContextV2Impl.swift @@ -389,7 +389,8 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { self.fileMap.serialize(manager: self.manager, to: self.metaPath) } case let .progressUpdated(progress): - let _ = progress + self.fileMap.progressUpdated(progress) + self.updateStatusRequests() case let .replaceHeader(data, range): self.processWrite(resourceOffset: 0, data: data, dataRange: range) case let .moveLocalFile(path): @@ -576,7 +577,11 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext { updatedStatus = .Remote(progress: progress) } } else if self.pendingFetch != nil { - updatedStatus = .Fetching(isActive: true, progress: 0.0) + if let progress = self.fileMap.progress { + updatedStatus = .Fetching(isActive: true, progress: progress) + } else { + updatedStatus = .Fetching(isActive: true, progress: 0.0) + } } else { updatedStatus = .Remote(progress: 0.0) } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 8b8e33eae6..fd5e294fdb 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -53,10 +53,10 @@ enum MessageContentToUpload { } func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, message: Message) -> MessageContentToUpload { - return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media) + return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media) } -func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload { +func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload { var contextResult: OutgoingChatContextResultMessageAttribute? var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute? var autoclearMessageAttribute: AutoclearTimeoutMessageAttribute? @@ -87,14 +87,14 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .forward(forwardInfo), reuploadInfo: nil, cacheReferenceKey: nil)), .text) } else if let contextResult = contextResult { return .immediate(.content(PendingMessageUploadedContentAndReuploadInfo(content: .chatContextResult(contextResult), reuploadInfo: nil, cacheReferenceKey: nil)), .text) - } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { + } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { return .signal(mediaResult, .media) } else { return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) } } -func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal? { +func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal? { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource { return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .secretMedia(.inputEncryptedFile(id: resource.fileId, accessHash: resource.accessHash), resource.decryptedSize, resource.key), reuploadInfo: nil, cacheReferenceKey: nil))) @@ -114,7 +114,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } } } - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) } else { if forceReupload { let mediaReference: AnyMediaReference @@ -148,7 +148,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) } } else if let contact = media as? TelegramMediaContact { let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "") @@ -644,7 +644,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA return .file } -private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal { +private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal { return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload) |> mapToSignal { result -> Signal in var referenceKey: CachedSentMediaReferenceKey? @@ -694,8 +694,19 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } else { fileReference = .standalone(media: file) } - let upload = messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge) - |> mapError { _ -> PendingMessageUploadError in return .generic + let upload: Signal = .single(nil) + |> then( + messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge) + |> mapError { _ -> PendingMessageUploadError in return .generic } + |> map(Optional.init) + ) + let resourceStatus: Signal + if passFetchProgress { + resourceStatus = postbox.mediaBox.resourceStatus(file.resource) + |> castError(PendingMessageUploadError.self) + |> map(Optional.init) + } else { + resourceStatus = .single(nil) } var alreadyTransformed = false for attribute in attributes { @@ -773,11 +784,21 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } } }) - - return combineLatest(upload, transformedFileAndThumbnail) - |> mapToSignal { content, fileAndThumbnailResult -> Signal in + + return combineLatest(upload, transformedFileAndThumbnail, resourceStatus) + |> mapToSignal { content, fileAndThumbnailResult, resourceStatus -> Signal in + guard let content = content else { + if let resourceStatus = resourceStatus, case let .Fetching(_, progress) = resourceStatus { + return .single(.progress(progress * 0.33)) + } + return .complete() + } switch content { case let .progress(progress): + var progress = progress + if passFetchProgress { + progress = 0.33 + progress * 0.67 + } return .single(.progress(progress)) case let .inputFile(inputFile): if case let .done(file, thumbnail) = fileAndThumbnailResult { diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 64b6f1a2b0..29edcc91af 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -59,7 +59,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, case let .update(media): let generateUploadSignal: (Bool) -> Signal? = { forceReupload in let augmentedMedia = augmentMediaWithReference(media) - return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) + return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) } if let uploadSignal = generateUploadSignal(forceReupload) { uploadedMedia = .single(.progress(0.027)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 254368c553..0274ef19a4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -487,7 +487,12 @@ public final class EngineStorySubscriptions: Equatable { } } -func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal { +public enum StoryUploadResult { + case progress(Float) + case completed +} + +func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy) -> Signal { let originalMedia: Media let contentToUpload: MessageContentToUpload @@ -516,6 +521,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: revalidationContext: account.mediaReferenceRevalidationContext, forceReupload: true, isGrouped: false, + passFetchProgress: false, peerId: account.peerId, messageId: nil, attributes: [], @@ -548,6 +554,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: revalidationContext: account.mediaReferenceRevalidationContext, forceReupload: true, isGrouped: false, + passFetchProgress: true, peerId: account.peerId, messageId: nil, attributes: [], @@ -569,47 +576,53 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Signal in - var privacyRules: [Api.InputPrivacyRule] - switch privacy.base { - case .everyone: - privacyRules = [.inputPrivacyValueAllowAll] - case .contacts: - privacyRules = [.inputPrivacyValueAllowContacts] - case .closeFriends: - privacyRules = [.inputPrivacyValueAllowCloseFriends] - case .nobody: - privacyRules = [.inputPrivacyValueDisallowAll] - } - var privacyUsers: [Api.InputUser] = [] - var privacyChats: [Int64] = [] - for peerId in privacy.additionallyIncludePeers { - if let peer = transaction.getPeer(peerId) { - if let _ = peer as? TelegramUser { - if let inputUser = apiInputUser(peer) { - privacyUsers.append(inputUser) + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Signal in + switch result { + case let .progress(progress): + return .single(.progress(progress)) + case let .content(content): + var privacyRules: [Api.InputPrivacyRule] + switch privacy.base { + case .everyone: + privacyRules = [.inputPrivacyValueAllowAll] + case .contacts: + privacyRules = [.inputPrivacyValueAllowContacts] + case .closeFriends: + privacyRules = [.inputPrivacyValueAllowCloseFriends] + case .nobody: + privacyRules = [.inputPrivacyValueDisallowAll] + } + var privacyUsers: [Api.InputUser] = [] + var privacyChats: [Int64] = [] + for peerId in privacy.additionallyIncludePeers { + if let peer = transaction.getPeer(peerId) { + if let _ = peer as? TelegramUser { + if let inputUser = apiInputUser(peer) { + privacyUsers.append(inputUser) + } + } else if peer is TelegramGroup || peer is TelegramChannel { + privacyChats.append(peer.id.id._internalGetInt64Value()) } - } else if peer is TelegramGroup || peer is TelegramChannel { - privacyChats.append(peer.id.id._internalGetInt64Value()) } } - } - if !privacyUsers.isEmpty { - privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers)) - } - if !privacyChats.isEmpty { - privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats)) - } - - switch result { - case let .content(content): + if !privacyUsers.isEmpty { + privacyRules.append(.inputPrivacyValueAllowUsers(users: privacyUsers)) + } + if !privacyChats.isEmpty { + privacyRules.append(.inputPrivacyValueAllowChatParticipants(chats: privacyChats)) + } + switch content.content { case let .media(inputMedia, _): var flags: Int32 = 0 var apiCaption: String? var apiEntities: [Api.MessageEntity]? + if pin { + flags |= 1 << 2 + } + if !text.isEmpty { flags |= 1 << 0 apiCaption = text @@ -641,7 +654,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: |> `catch` { _ -> Signal in return .single(nil) } - |> mapToSignal { updates -> Signal in + |> mapToSignal { updates -> Signal in if let updates = updates { for update in updates.allUpdates { if case let .updateStory(_, story) = update { @@ -660,7 +673,7 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: account.stateManager.addUpdates(updates) } - return .complete() + return .single(.completed) } default: return .complete() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 96a7482eb5..3352f8fbfb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -876,8 +876,8 @@ public extension TelegramEngine { } } - public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], privacy: EngineStoryPrivacy) -> Signal { - return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, privacy: privacy) + public func uploadStory(media: EngineStoryInputMedia, text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy) -> Signal { + return _internal_uploadStory(account: self.account, media: media, text: text, entities: entities, pin: pin, privacy: privacy) } public func deleteStory(id: Int32) -> Signal { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 7bf1a7998d..852294e8de 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -107,6 +107,7 @@ public enum PresentationResourceKey: Int32 { case chatListRecentStatusVoiceChatPanelIcon case chatListForwardedIcon + case chatListStoryReplyIcon case chatListGeneralTopicIcon case chatListGeneralTopicSmallIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 8f80673a7e..c41529fc28 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -253,6 +253,12 @@ public struct PresentationResourcesChatList { }) } + public static func storyReplyIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatListStoryReplyIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/StoryReplyIcon"), color: theme.chatList.muteIconColor) + }) + } + public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 97a0ae66d8..4153994c1e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -203,12 +203,12 @@ private final class CameraScreenComponent: CombinedComponent { } else { self.camera.setFlashMode(.off) } - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) } func togglePosition() { self.camera.togglePosition() - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) } func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { @@ -252,9 +252,9 @@ private final class CameraScreenComponent: CombinedComponent { func stopVideoRecording() { self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0) self.resultDisposable.set((self.camera.stopRecording() - |> deliverOnMainQueue).start(next: { [weak self] path in - if let self, let path { - self.completion.invoke(.single(.video(path, PixelDimensions(width: 1080, height: 1920)))) + |> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in + if let self, let (path, transitionImage) = pathAndTransitionImage { + self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920)))) } })) self.updated(transition: .spring(duration: 0.4)) @@ -641,7 +641,7 @@ public class CameraScreen: ViewController { public enum Result { case pendingImage case image(UIImage) - case video(String, PixelDimensions) + case video(String, UIImage?, PixelDimensions) case asset(PHAsset) case draft(MediaEditorDraft) } @@ -787,6 +787,10 @@ public class CameraScreen: ViewController { ).start(next: { [weak self] changingPosition, forceBlur in if let self { if changingPosition { + if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { + self.simplePreviewView?.addSubview(snapshot) + self.previewSnapshotView = snapshot + } UIView.transition(with: self.previewContainerView, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: { self.previewBlurView.effect = UIBlurEffect(style: .dark) }) @@ -869,16 +873,16 @@ public class CameraScreen: ViewController { @objc private func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { switch gestureRecognizer.state { - case .began: - gestureRecognizer.scale = 1.0 case .changed: let scale = gestureRecognizer.scale - self.camera.setZoomLevel(scale) + self.camera.setZoomDelta(scale) + gestureRecognizer.scale = 1.0 default: break } } + private var isDismissing = false @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let controller = self.controller else { return @@ -891,7 +895,8 @@ public class CameraScreen: ViewController { if !"".isEmpty { } else { - if translation.x < -10.0 { + if translation.x < -10.0 || self.isDismissing { + self.isDismissing = true let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.updateTransitionProgress(transitionFraction, transition: .immediate) } else if translation.y < -10.0 { @@ -904,6 +909,8 @@ public class CameraScreen: ViewController { let velocity = gestureRecognizer.velocity(in: self.view) let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) + + self.isDismissing = false default: break } @@ -1306,7 +1313,7 @@ public class CameraScreen: ViewController { func presentGallery(fromGesture: Bool = false) { if !fromGesture { - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) } var didStopCameraCapture = false @@ -1364,8 +1371,10 @@ public class CameraScreen: ViewController { return } + self.dismissAllTooltips() + if !interactive { - self.hapticFeedback.impact(.veryLight) + self.hapticFeedback.impact(.light) } self.node.camera.stopCapture(invalidate: true) @@ -1385,6 +1394,20 @@ public class CameraScreen: ViewController { } } + private func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + return true + }) + } + public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { let offsetX = floorToScreenPixels((1.0 - transitionFraction) * self.node.frame.width * -1.0) transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index 456b798e2f..b0948b9674 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -214,7 +214,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { self.colorPixelFormat = .bgra8Unorm self.framebufferOnly = true - //self.presentsWithTransaction = true + self.isPaused = true self.delegate = self diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 6fb817522f..075ebf5671 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -134,6 +134,7 @@ public final class ChatListHeaderComponent: Component { public let networkStatus: HeaderNetworkStatusComponent.Content? public let storySubscriptions: EngineStorySubscriptions? public let storiesFraction: CGFloat + public let uploadProgress: Float? public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings @@ -149,6 +150,7 @@ public final class ChatListHeaderComponent: Component { networkStatus: HeaderNetworkStatusComponent.Content?, storySubscriptions: EngineStorySubscriptions?, storiesFraction: CGFloat, + uploadProgress: Float?, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, @@ -163,6 +165,7 @@ public final class ChatListHeaderComponent: Component { self.networkStatus = networkStatus self.storySubscriptions = storySubscriptions self.storiesFraction = storiesFraction + self.uploadProgress = uploadProgress self.theme = theme self.strings = strings self.openStatusSetup = openStatusSetup @@ -191,6 +194,9 @@ public final class ChatListHeaderComponent: Component { if lhs.storiesFraction != rhs.storiesFraction { return false } + if lhs.uploadProgress != rhs.uploadProgress { + return false + } if lhs.context !== rhs.context { return false } @@ -817,6 +823,7 @@ public final class ChatListHeaderComponent: Component { strings: component.strings, storySubscriptions: storySubscriptions, collapseFraction: 1.0 - component.storiesFraction, + uploadProgress: component.uploadProgress, peerAction: { [weak self] peer in guard let self else { return diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 990b1864e2..80a5b41ec2 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -21,6 +21,7 @@ public final class ChatListNavigationBar: Component { public let secondaryContent: ChatListHeaderComponent.Content? public let secondaryTransition: CGFloat public let storySubscriptions: EngineStorySubscriptions? + public let uploadProgress: Float? public let tabsNode: ASDisplayNode? public let tabsNodeIsSearch: Bool public let activateSearch: (NavigationBarSearchContentNode) -> Void @@ -38,6 +39,7 @@ public final class ChatListNavigationBar: Component { secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, storySubscriptions: EngineStorySubscriptions?, + uploadProgress: Float?, tabsNode: ASDisplayNode?, tabsNodeIsSearch: Bool, activateSearch: @escaping (NavigationBarSearchContentNode) -> Void, @@ -54,6 +56,7 @@ public final class ChatListNavigationBar: Component { self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition self.storySubscriptions = storySubscriptions + self.uploadProgress = uploadProgress self.tabsNode = tabsNode self.tabsNodeIsSearch = tabsNodeIsSearch self.activateSearch = activateSearch @@ -94,6 +97,9 @@ public final class ChatListNavigationBar: Component { if lhs.storySubscriptions != rhs.storySubscriptions { return false } + if lhs.uploadProgress != rhs.uploadProgress { + return false + } if lhs.tabsNode !== rhs.tabsNode { return false } @@ -176,12 +182,12 @@ public final class ChatListNavigationBar: Component { } } - public func applyScroll(offset: CGFloat, transition: Transition) { + public func applyScroll(offset: CGFloat, forceUpdate: Bool = false, transition: Transition) { let transition = transition self.rawScrollOffset = offset - if self.deferScrollApplication { + if self.deferScrollApplication && !forceUpdate { self.hasDeferredScrollOffset = true return } @@ -208,7 +214,7 @@ public final class ChatListNavigationBar: Component { } let clippedScrollOffset = min(minContentOffset, offset) - if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset { + if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate { return } self.hasDeferredScrollOffset = false @@ -304,6 +310,7 @@ public final class ChatListNavigationBar: Component { networkStatus: nil, storySubscriptions: component.storySubscriptions, storiesFraction: 1.0 - storiesOffsetFraction, + uploadProgress: component.uploadProgress, context: component.context, theme: component.theme, strings: component.strings, @@ -402,8 +409,14 @@ public final class ChatListNavigationBar: Component { let themeUpdated = self.component?.theme !== component.theme var storiesUnlockedUpdated = false - if let previousComponent = self.component, previousComponent.storiesUnlocked != component.storiesUnlocked { - storiesUnlockedUpdated = true + var uploadProgressUpdated = false + if let previousComponent = self.component { + if previousComponent.storiesUnlocked != component.storiesUnlocked { + storiesUnlockedUpdated = true + } + if previousComponent.uploadProgress != component.uploadProgress { + uploadProgressUpdated = true + } } self.component = component @@ -447,6 +460,12 @@ public final class ChatListNavigationBar: Component { self.hasDeferredScrollOffset = true + if uploadProgressUpdated { + if let rawScrollOffset = self.rawScrollOffset { + self.applyScroll(offset: rawScrollOffset, forceUpdate: true, transition: transition) + } + } + if storiesUnlockedUpdated && component.storiesUnlocked { self.applyScrollFraction = 0.0 self.applyScrollStartFraction = 0.0 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 113cce3d7e..176514f72a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -1,6 +1,10 @@ import Foundation -public enum CodableDrawingEntity { +public enum CodableDrawingEntity: Equatable { + public static func == (lhs: CodableDrawingEntity, rhs: CodableDrawingEntity) -> Bool { + return lhs.entity.isEqual(to: rhs.entity) + } + case sticker(DrawingStickerEntity) case text(DrawingTextEntity) case simpleShape(DrawingSimpleShapeEntity) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift index 843c9c890b..b8a4ed8990 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift @@ -104,4 +104,38 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable { newEntity.rotation = self.rotation return newEntity } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingBubbleEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.drawType != other.drawType { + return false + } + if self.color != other.color { + return false + } + if self.lineWidth != other.lineWidth { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.size != other.size { + return false + } + if self.rotation != other.rotation { + return false + } + if self.tailPosition != other.tailPosition { + return false + } + return true + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift index 5d79c01486..cea001e5dc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift @@ -17,4 +17,6 @@ public protocol DrawingEntity: AnyObject { var renderImage: UIImage? { get set } var renderSubEntities: [DrawingEntity]? { get set } + + func isEqual(to other: DrawingEntity) -> Bool } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift index 1164dd1093..896397cbc1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift @@ -7,7 +7,7 @@ import AccountContext import Photos public final class DrawingMediaEntity: DrawingEntity, Codable { - public enum Content { + public enum Content: Equatable { case image(UIImage, PixelDimensions) case video(String, PixelDimensions) case asset(PHAsset) @@ -20,6 +20,29 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) } } + + public static func == (lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .image(lhsImage, lhsDimensions): + if case let .image(rhsImage, rhsDimensions) = rhs { + return lhsImage === rhsImage && lhsDimensions == rhsDimensions + } else { + return false + } + case let .video(lhsPath, lhsDimensions): + if case let .video(rhsPath, rhsDimensions) = rhs { + return lhsPath == rhsPath && lhsDimensions == rhsDimensions + } else { + return false + } + case let .asset(lhsAsset): + if case let .asset(rhsAsset) = rhs { + return lhsAsset.localIdentifier == rhsAsset.localIdentifier + } else { + return false + } + } + } } private enum CodingKeys: String, CodingKey { @@ -143,4 +166,35 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { newEntity.mirrored = self.mirrored return newEntity } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingMediaEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.content != other.content { + return false + } + if self.size != other.size { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + if self.mirrored != other.mirrored { + return false + } + return true + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift index 151d56b19e..2f01566976 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift @@ -110,4 +110,38 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { newEntity.rotation = self.rotation return newEntity } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingSimpleShapeEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.shapeType != other.shapeType { + return false + } + if self.drawType != other.drawType { + return false + } + if self.color != other.color { + return false + } + if self.lineWidth != other.lineWidth { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.size != other.size { + return false + } + if self.rotation != other.rotation { + return false + } + return true + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 3d74eb7dc5..f78c8b485d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -5,9 +5,26 @@ import AccountContext import TelegramCore public final class DrawingStickerEntity: DrawingEntity, Codable { - public enum Content { + public enum Content: Equatable { case file(TelegramMediaFile) case image(UIImage) + + public static func == (lhs: Content, rhs: Content) -> Bool { + switch lhs { + case let .file(lhsFile): + if case let .file(rhsFile) = rhs { + return lhsFile.fileId == rhsFile.fileId + } else { + return false + } + case let .image(lhsImage): + if case let .image(rhsImage) = rhs { + return lhsImage === rhsImage + } else { + return false + } + } + } } private enum CodingKeys: String, CodingKey { case uuid @@ -110,4 +127,32 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { newEntity.mirrored = self.mirrored return newEntity } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingStickerEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.content != other.content { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + if self.mirrored != other.mirrored { + return false + } + return true + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift index db4c0a783e..e77f711caf 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift @@ -56,26 +56,26 @@ public final class DrawingTextEntity: DrawingEntity, Codable { case renderAnimationFrames } - public enum Style: Codable { + public enum Style: Codable, Equatable { case regular case filled case semi case stroke } - public enum Animation: Codable { + public enum Animation: Codable, Equatable { case none case typing case wiggle case zoomIn } - public enum Font: Codable { + public enum Font: Codable, Equatable { case sanFrancisco case other(String, String) } - public enum Alignment: Codable { + public enum Alignment: Codable, Equatable { case left case center case right @@ -257,6 +257,52 @@ public final class DrawingTextEntity: DrawingEntity, Codable { return newEntity } + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingTextEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.text != other.text { + return false + } + if self.style != other.style { + return false + } + if self.animation != other.animation { + return false + } + if self.font != other.font { + return false + } + if self.alignment != other.alignment { + return false + } + if self.fontSize != other.fontSize { + return false + } + if self.color != other.color { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.width != other.width { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + return true + } + // public weak var currentEntityView: DrawingEntityView? // public func makeView(context: AccountContext) -> DrawingEntityView { // let entityView = DrawingTextEntityView(context: context, entity: self) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift index 798c3cd22c..42685df5a5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift @@ -107,4 +107,38 @@ public final class DrawingVectorEntity: DrawingEntity, Codable { newEntity.end = self.end return newEntity } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingVectorEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.type != other.type { + return false + } + if self.color != other.color { + return false + } + if self.lineWidth != other.lineWidth { + return false + } + if self.drawingSize != other.drawingSize { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.start != other.start { + return false + } + if self.mid.0 != other.mid.0 || self.mid.1 != other.mid.1 { + return false + } + if self.end != other.end { + return false + } + return true + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift index 8017410b88..32b21dde64 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift @@ -53,7 +53,7 @@ final class ImageTextureSource: TextureSource { func connect(to consumer: TextureConsumer) { self.output = consumer if let texture = self.texture { - self.output?.consumeTexture(texture) + self.output?.consumeTexture(texture, render: false) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 5e6786791f..e6629b7a8f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -23,13 +23,13 @@ public struct MediaEditorPlayerState { public final class MediaEditor { public enum Subject { case image(UIImage, PixelDimensions) - case video(String, PixelDimensions) + case video(String, UIImage?, PixelDimensions) 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)) @@ -189,7 +189,7 @@ public final class MediaEditor { let duration = asset.duration.seconds let interval = duration / Double(count) for i in 0 ..< count { - timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(60.0)))) + timestamps.append(NSValue(time: CMTime(seconds: Double(i) * interval, preferredTimescale: CMTimeScale(1000)))) } var updatedFrames: [UIImage] = [] @@ -287,51 +287,59 @@ public final class MediaEditor { colors = mediaEditorGetGradientColors(from: image) } textureSource = .single((ImageTextureSource(image: image, renderTarget: renderTarget), image, nil, colors.0, colors.1)) - case let .video(path, _): + case let .video(path, transitionImage, _): textureSource = Signal { subscriber in let url = URL(fileURLWithPath: path) let asset = AVURLAsset(url: url) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.appliesPreferredTrackTransform = true - imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - if let image { - let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image)) - subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) - } else { - subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black)) + + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + + if let transitionImage { + let colors = mediaEditorGetGradientColors(from: transitionImage) + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) + subscriber.putCompletion() + + return EmptyDisposable + } else { + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.appliesPreferredTrackTransform = true + imageGenerator.maximumSize = CGSize(width: 72, height: 128) + imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: CMTime(seconds: 0, preferredTimescale: CMTimeScale(30.0)))]) { _, image, _, _, _ in + if let image { + let colors = mediaEditorGetGradientColors(from: UIImage(cgImage: image)) + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) + } else { + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, .black, .black)) + } + subscriber.putCompletion() + } + return ActionDisposable { + imageGenerator.cancelAllCGImageGeneration() } - } - return ActionDisposable { - imageGenerator.cancelAllCGImageGeneration() } } case let .asset(asset): textureSource = Signal { subscriber in if asset.mediaType == .video { - let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: nil, resultHandler: { image, info in + let options = PHImageRequestOptions() + options.deliveryMode = .fastFormat + let requestId = PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 128.0, height: 128.0), contentMode: .aspectFit, options: options, resultHandler: { image, info in if let image { - var degraded = false if let info { if let cancelled = info[PHImageCancelledKey] as? Bool, cancelled { return } - if let degradedValue = info[PHImageResultIsDegradedKey] as? Bool, degradedValue { - degraded = true + } + let colors = mediaEditorGetGradientColors(from: image) + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in + if let asset { + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) + subscriber.putCompletion() } - } - if !degraded { - let colors = mediaEditorGetGradientColors(from: image) - PHImageManager.default().requestAVAsset(forVideo: asset, options: nil, resultHandler: { asset, _, _ in - if let asset { - let playerItem = AVPlayerItem(asset: asset) - let player = AVPlayer(playerItem: playerItem) - subscriber.putNext((VideoTextureSource(player: player, renderTarget: renderTarget), nil, player, colors.0, colors.1)) - subscriber.putCompletion() - } - }) - } + }) } }) return ActionDisposable { @@ -379,6 +387,7 @@ public final class MediaEditor { self.setGradientColors([topColor, bottomColor]) if player == nil { + self.updateRenderChain() self.maybeGeneratePersonSegmentation(image) } @@ -396,7 +405,7 @@ public final class MediaEditor { self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in if let self { let start = self.values.videoTrimRange?.lowerBound ?? 0.0 - self.player?.seek(to: CMTime(seconds: start, preferredTimescale: 60)) + self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000))) self.player?.play() } }) @@ -417,10 +426,20 @@ public final class MediaEditor { } private var skipRendering = false + private func updateValues(skipRendering: Bool = false, _ f: (MediaEditorValues) -> MediaEditorValues) { + if skipRendering { + self.skipRendering = true + } + self.values = f(self.values) + if skipRendering { + self.skipRendering = false + } + } + public func setCrop(offset: CGPoint, scale: CGFloat, rotation: CGFloat, mirroring: Bool) { - self.skipRendering = true - self.values = self.values.withUpdatedCrop(offset: offset, scale: scale, rotation: rotation, mirroring: mirroring) - self.skipRendering = false + self.updateValues(skipRendering: true) { values in + return values.withUpdatedCrop(offset: offset, scale: scale, rotation: rotation, mirroring: mirroring) + } } public func getToolValue(_ key: EditorToolKey) -> Any? { @@ -428,19 +447,24 @@ public final class MediaEditor { } public func setToolValue(_ key: EditorToolKey, value: Any) { - var updatedToolValues = self.values.toolValues - updatedToolValues[key] = value - self.values = self.values.withUpdatedToolValues(updatedToolValues) - self.updateRenderChain() + self.updateValues { values in + var updatedToolValues = values.toolValues + updatedToolValues[key] = value + return values.withUpdatedToolValues(updatedToolValues) + } } public func setVideoIsMuted(_ videoIsMuted: Bool) { self.player?.isMuted = videoIsMuted - self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted) + self.updateValues(skipRendering: true) { values in + return values.withUpdatedVideoIsMuted(videoIsMuted) + } } public func setVideoIsFullHd(_ videoIsFullHd: Bool) { - self.values = self.values.withUpdatedVideoIsFullHd(videoIsFullHd) + self.updateValues(skipRendering: true) { values in + return values.withUpdatedVideoIsFullHd(videoIsFullHd) + } } private var targetTimePosition: (CMTime, Bool)? @@ -486,33 +510,51 @@ public final class MediaEditor { }) } - public func setVideoTrimStart(_ trimStart: Double) { - let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0 - let trimRange = trimStart ..< trimEnd - self.values = self.values.withUpdatedVideoTrimRange(trimRange) + public func setVideoTrimRange(_ trimRange: Range, apply: Bool) { + self.updateValues(skipRendering: true) { values in + return values.withUpdatedVideoTrimRange(trimRange) + } + + if apply { + self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) + } } - public func setVideoTrimEnd(_ trimEnd: Double) { - let trimStart = self.values.videoTrimRange?.lowerBound ?? 0.0 - let trimRange = trimStart ..< trimEnd - self.values = self.values.withUpdatedVideoTrimRange(trimRange) - - self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimEnd, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) - } - public func setDrawingAndEntities(data: Data?, image: UIImage?, entities: [CodableDrawingEntity]) { - self.values = self.values.withUpdatedDrawingAndEntities(drawing: image, entities: entities) + self.updateValues(skipRendering: true) { values in + return values.withUpdatedDrawingAndEntities(drawing: image, entities: entities) + } } public func setGradientColors(_ gradientColors: [UIColor]) { - self.values = self.values.withUpdatedGradientColors(gradientColors: gradientColors) + self.updateValues(skipRendering: true) { values in + return values.withUpdatedGradientColors(gradientColors: gradientColors) + } } + private var previousUpdateTime: Double? + private var scheduledUpdate = false private func updateRenderChain() { self.renderChain.update(values: self.values) if let player = self.player, player.rate > 0.0 { } else { - self.previewView?.scheduleFrame() + let currentTime = CACurrentMediaTime() + if !self.scheduledUpdate { + let delay = 0.03333 + if let previousUpdateTime = self.previousUpdateTime, currentTime - previousUpdateTime < delay { + self.scheduledUpdate = true + Queue.mainQueue().after(delay - (currentTime - previousUpdateTime)) { + self.scheduledUpdate = false + self.previousUpdateTime = CACurrentMediaTime() + self.renderer.willRenderFrame() + self.renderer.renderFrame() + } + } else { + self.previousUpdateTime = currentTime + self.renderer.willRenderFrame() + self.renderer.renderFrame() + } + } } } @@ -564,7 +606,8 @@ final class MediaEditorRenderChain { } func update(values: MediaEditorValues) { - for (key, value) in values.toolValues { + for key in EditorToolKey.allCases { + let value = values.toolValues[key] switch key { case .enhance: if let value = value as? Float { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index c4ce447f43..7871390813 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -101,8 +101,7 @@ final class MediaEditorComposer { } let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) - self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation) - self.renderer.renderFrame() + self.renderer.consumeVideoPixelBuffer(imageBuffer, rotation: textureRotation, timestamp: time, render: true) if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) @@ -136,8 +135,7 @@ final class MediaEditorComposer { } if self.filteredImage == nil, let device = self.device { if let texture = loadTexture(image: inputImage, device: device) { - self.renderer.consumeTexture(texture) - self.renderer.renderFrame() + self.renderer.consumeTexture(texture, render: true) if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift index 98baa40638..cde84b67fe 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -14,7 +14,19 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } var drawable: MTLDrawable? { - return self.currentDrawable + return self.nextDrawable + } + + var nextDrawable: MTLDrawable? { + if #available(iOS 13.0, *) { + if let layer = self.layer as? CAMetalLayer { + return layer.nextDrawable() + } else { + return self.currentDrawable + } + } else { + return self.currentDrawable + } } var renderPassDescriptor: MTLRenderPassDescriptor? { @@ -46,13 +58,8 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge self.colorPixelFormat = .bgra8Unorm self.isPaused = true - self.enableSetNeedsDisplay = false - } - - func scheduleFrame() { - Queue.mainQueue().justDispatch { - self.draw() - } + self.enableSetNeedsDisplay = true + self.framebufferOnly = true } public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { @@ -61,10 +68,14 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } } + public func redraw() { + self.setNeedsDisplay() + } + public func draw(in view: MTKView) { guard self.frame.width > 0.0 else { return } - self.renderer?.renderFrame() + self.renderer?.displayFrame() } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 5add4c451f..e96b10ec57 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -6,8 +6,8 @@ import Photos import SwiftSignalKit protocol TextureConsumer: AnyObject { - func consumeTexture(_ texture: MTLTexture) - func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) + func consumeTexture(_ texture: MTLTexture, render: Bool) + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, timestamp: CMTime, render: Bool) } final class RenderingContext { @@ -40,7 +40,7 @@ protocol RenderTarget: AnyObject { var drawable: MTLDrawable? { get } var renderPassDescriptor: MTLRenderPassDescriptor? { get } - func scheduleFrame() + func redraw() } final class MediaEditorRenderer: TextureConsumer { @@ -161,10 +161,12 @@ final class MediaEditorRenderer: TextureConsumer { guard let device = device, let commandQueue = self.commandQueue, let textureCache = self.textureCache else { + self.semaphore.signal() return } guard let commandBuffer = commandQueue.makeCommandBuffer() else { + self.semaphore.signal() return } @@ -174,6 +176,7 @@ final class MediaEditorRenderer: TextureConsumer { } else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) { texture = videoTexture } else { + self.semaphore.signal() return } @@ -182,61 +185,81 @@ final class MediaEditorRenderer: TextureConsumer { texture = nextTexture } } - if self.renderTarget != nil { - self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer) - } self.finalTexture = texture + commandBuffer.addCompletedHandler { [weak self] _ in + if let self { + if self.renderTarget == nil { + self.semaphore.signal() + } + } + } + commandBuffer.commit() + + if let renderTarget = self.renderTarget { + renderTarget.redraw() + } else { + commandBuffer.waitUntilCompleted() + } + } + + func displayFrame() { + guard let renderTarget = self.renderTarget, + let device = renderTarget.mtlDevice, + let commandQueue = self.commandQueue, + let commandBuffer = commandQueue.makeCommandBuffer(), + let texture = self.finalTexture + else { + self.semaphore.signal() + return + } + commandBuffer.addCompletedHandler { [weak self] _ in if let self { self.semaphore.signal() -#if targetEnvironment(simulator) if let onNextRender = self.onNextRender { self.onNextRender = nil Queue.mainQueue().async { onNextRender() } } -#endif } } -#if targetEnvironment(simulator) -#else - if let renderTarget = self.renderTarget, let drawable = renderTarget.drawable { - drawable.addPresentedHandler { [weak self] _ in - if let self, let onNextRender = self.onNextRender { - self.onNextRender = nil - Queue.mainQueue().async { - onNextRender() - } - } - } - } -#endif + self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer) - if let _ = self.renderTarget { - commandBuffer.commit() - commandBuffer.waitUntilScheduled() - } else { - commandBuffer.commit() - commandBuffer.waitUntilCompleted() - } + commandBuffer.commit() } - func consumeTexture(_ texture: MTLTexture) { - self.semaphore.wait() + func willRenderFrame() { + let _ = self.semaphore.wait(timeout: .distantFuture) + } + + func consumeTexture(_ texture: MTLTexture, render: Bool) { + if render { + let _ = self.semaphore.wait(timeout: .distantFuture) + } self.currentTexture = texture - self.renderTarget?.scheduleFrame() + if render { + self.renderFrame() + } } - func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) { - self.semaphore.wait() + var previousPresentationTimestamp: CMTime? + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, timestamp: CMTime, render: Bool) { + let _ = self.semaphore.wait(timeout: .distantFuture) self.currentPixelBuffer = (pixelBuffer, rotation) - self.renderTarget?.scheduleFrame() + if render { + if self.previousPresentationTimestamp == timestamp { + self.semaphore.signal() + } else { + self.renderFrame() + } + } + self.previousPresentationTimestamp = timestamp } func renderTargetDidChange(_ target: RenderTarget?) { @@ -245,7 +268,7 @@ final class MediaEditorRenderer: TextureConsumer { } func renderTargetDrawableSizeDidChange(_ size: CGSize) { - self.renderTarget?.scheduleFrame() + self.renderTarget?.redraw() } func finalRenderedImage() -> UIImage? { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 18d603c35e..08d4a35874 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -5,7 +5,7 @@ import TelegramCore import AVFoundation import VideoToolbox -public enum EditorToolKey: Int32 { +public enum EditorToolKey: Int32, CaseIterable { case enhance case brightness case contrast @@ -37,7 +37,50 @@ private let adjustmentToolsKeys: [EditorToolKey] = [ .sharpen ] -public final class MediaEditorValues: Codable { +public final class MediaEditorValues: Codable, Equatable { + public static func == (lhs: MediaEditorValues, rhs: MediaEditorValues) -> Bool { + if lhs.originalDimensions != rhs.originalDimensions { + return false + } + if lhs.cropOffset != rhs.cropOffset { + return false + } + if lhs.cropSize != rhs.cropSize { + return false + } + if lhs.cropScale != rhs.cropScale { + return false + } + if lhs.cropRotation != rhs.cropRotation { + return false + } + if lhs.cropMirroring != rhs.cropMirroring { + return false + } + if lhs.gradientColors != rhs.gradientColors { + return false + } + if lhs.videoTrimRange != rhs.videoTrimRange { + return false + } + if lhs.videoIsMuted != rhs.videoIsMuted { + return false + } + if lhs.videoIsFullHd != rhs.videoIsFullHd { + return false + } + if lhs.drawing !== rhs.drawing { + return false + } + if lhs.entities != rhs.entities { + return false + } +// if lhs.toolValues != rhs.toolValues { +// return false +// } + return true + } + private enum CodingKeys: String, CodingKey { case originalWidth case originalHeight @@ -211,6 +254,39 @@ public final class MediaEditorValues: Codable { func withUpdatedToolValues(_ toolValues: [EditorToolKey: Any]) -> MediaEditorValues { return MediaEditorValues(originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropSize: self.cropSize, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, drawing: self.drawing, entities: self.entities, toolValues: toolValues) } + + public var resultDimensions: PixelDimensions { + if self.videoIsFullHd { + return PixelDimensions(width: 1080, height: 1920) + } else { + return PixelDimensions(width: 720, height: 1280) + } + } + + public var hasChanges: Bool { + if self.cropOffset != .zero { + return true + } + if self.cropScale != 1.0 { + return true + } + if self.cropRotation != 0.0 { + return true + } + if self.cropMirroring { + return true + } + if self.videoTrimRange != nil { + return true + } + if !self.entities.isEmpty { + return true + } + if !self.toolValues.isEmpty { + return true + } + return false + } } public struct TintValue: Equatable, Codable { @@ -621,7 +697,7 @@ public extension MediaEditorValues { } var requiresComposing: Bool { - if self.originalDimensions.width > self.originalDimensions.height { + if self.originalDimensions.width > 0 && abs((Double(self.originalDimensions.height) / Double(self.originalDimensions.width)) - 1.7777778) > 0.001 { return true } if abs(1.0 - self.cropScale) > 0.0 { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index f48230a443..a3d45f2a0f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -198,7 +198,7 @@ public final class MediaEditorVideoExport { var timeRange: CMTimeRange? { if let videoTrimRange = self.values.videoTrimRange { - return CMTimeRange(start: CMTime(seconds: videoTrimRange.lowerBound, preferredTimescale: 1), end: CMTime(seconds: videoTrimRange.upperBound, preferredTimescale: 1)) + return CMTimeRange(start: CMTime(seconds: videoTrimRange.lowerBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), end: CMTime(seconds: videoTrimRange.upperBound, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) } else { return nil } @@ -236,7 +236,7 @@ public final class MediaEditorVideoExport { public enum ExportStatus { case unknown - case progress(Double) + case progress(Float) case completed case failed(ExportError) } @@ -259,6 +259,13 @@ public final class MediaEditorVideoExport { private var textureRotation: TextureRotation = .rotate0Degrees private let duration = ValuePromise() + private var durationValue: CMTime? { + didSet { + if let durationValue = self.durationValue { + self.duration.set(durationValue) + } + } + } private let pauseDispatchGroup = DispatchGroup() private var cancelled = false @@ -279,14 +286,14 @@ public final class MediaEditorVideoExport { private func setup() { if case let .video(asset) = self.subject { if let trimmedVideoDuration = self.configuration.timeRange?.duration { - self.duration.set(trimmedVideoDuration) + self.durationValue = trimmedVideoDuration } else { asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"]) { - self.duration.set(asset.duration) + self.durationValue = asset.duration } } } else { - self.duration.set(CMTime(seconds: 5, preferredTimescale: 1)) + self.durationValue = CMTime(seconds: 5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) } switch self.subject { @@ -325,20 +332,23 @@ public final class MediaEditorVideoExport { let videoTracks = asset.tracks(withMediaType: .video) if (videoTracks.count > 0) { var sourceFrameRate: Float = 0.0 + let colorProperties: [String: Any] = [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 + ] + let outputSettings: [String: Any] = [ - kCVPixelBufferPixelFormatTypeKey as String: [kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], - AVVideoColorPropertiesKey: [ - AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, - AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, - AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 - ] + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelBufferMetalCompatibilityKey as String: true, + AVVideoColorPropertiesKey: colorProperties ] if let videoTrack = videoTracks.first, videoTrack.preferredTransform.isIdentity && !self.configuration.values.requiresComposing { } else { self.setupComposer() } let videoOutput = AVAssetReaderTrackOutput(track: videoTracks.first!, outputSettings: outputSettings) - videoOutput.alwaysCopiesSampleData = false + videoOutput.alwaysCopiesSampleData = true if reader.canAdd(videoOutput) { reader.add(videoOutput) } else { @@ -519,8 +529,13 @@ public final class MediaEditorVideoExport { } self.pauseDispatchGroup.wait() if let buffer = output.copyNextSampleBuffer() { + let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) + if let duration = self.durationValue { + let startTimestamp = self.reader?.timeRange.start ?? .zero + let progress = (timestamp - startTimestamp).seconds / duration.seconds + self.statusValue = .progress(Float(progress)) + } if let composer = self.composer { - let timestamp = CMSampleBufferGetPresentationTimeStamp(buffer) composer.processSampleBuffer(buffer, pool: writer.pixelBufferPool, textureRotation: self.textureRotation, completion: { pixelBuffer in if let pixelBuffer { if !writer.appendPixelBuffer(pixelBuffer, at: timestamp) { @@ -595,6 +610,12 @@ public final class MediaEditorVideoExport { self.resume() } self.cancelled = true + + self.queue.async { + if let reader = self.reader, reader.status == .reading { + reader.cancelReading() + } + } } private let statusPromise = Promise(.unknown) @@ -607,7 +628,6 @@ public final class MediaEditorVideoExport { return self.statusPromise.get() } - private func startImageVideoExport() { guard self.internalStatus == .idle, let writer = self.writer else { self.statusValue = .failed(.invalid) @@ -687,7 +707,7 @@ public final class MediaEditorVideoExport { } } - public func startExport() { + public func start() { switch self.subject { case .video: self.startVideoExport() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 9882967721..8492f9162c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -1,4 +1,5 @@ import Foundation +import QuartzCore import Metal import simd @@ -133,31 +134,41 @@ final class OutputRenderPass: DefaultRenderPass { @discardableResult override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else { + guard let renderTarget = self.renderTarget else { return nil } self.setupVerticesBuffer(device: device) - let drawableSize = renderTarget.drawableSize - - let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder( - descriptor: renderPassDescriptor)! - - renderCommandEncoder.setViewport(MTLViewport( - originX: 0.0, originY: 0.0, - width: Double(drawableSize.width), height: Double(drawableSize.height), - znear: -1.0, zfar: 1.0)) - - - renderCommandEncoder.setFragmentTexture(input, index: 0) - - self.encodeDefaultCommands(using: renderCommandEncoder) - - renderCommandEncoder.endEncoding() - - if let drawable = renderTarget.drawable { + autoreleasepool { + guard let drawable = renderTarget.drawable else { + return + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].storeAction = .store + + let drawableSize = renderTarget.drawableSize + + let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder( + descriptor: renderPassDescriptor)! + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0.0, originY: 0.0, + width: Double(drawableSize.width), height: Double(drawableSize.height), + znear: -1.0, zfar: 1.0)) + + + renderCommandEncoder.setFragmentTexture(input, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + commandBuffer.present(drawable) } + return nil } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index 508e30210e..e469b2316f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -98,16 +98,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD return } + var frameRate: Int = 30 var hasVideoTrack: Bool = false for track in playerItem.asset.tracks { if track.mediaType == .video { + if track.nominalFrameRate > 0.0 { + frameRate = Int(ceil(track.nominalFrameRate)) + } hasVideoTrack = true break } } self.textureRotation = textureRotatonForAVAsset(playerItem.asset) if !hasVideoTrack { - assertionFailure("No video track found.") return } @@ -129,7 +132,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD playerItem.add(output) self.playerItemOutput = output - self.setupDisplayLink() + self.setupDisplayLink(frameRate: min(60, frameRate)) } private class DisplayLinkTarget { @@ -142,7 +145,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } } - private func setupDisplayLink() { + private func setupDisplayLink(frameRate: Int) { self.displayLink?.invalidate() self.displayLink = nil @@ -150,7 +153,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD let displayLink = CADisplayLink(target: DisplayLinkTarget({ [weak self] in self?.handleUpdate() }), selector: #selector(DisplayLinkTarget.handleDisplayLinkUpdate(sender:))) - displayLink.preferredFramesPerSecond = 60 + displayLink.preferredFramesPerSecond = frameRate displayLink.add(to: .main, forMode: .common) self.displayLink = displayLink } @@ -183,7 +186,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD var presentationTime: CMTime = .zero if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) { - self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation) + self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation, timestamp: presentationTime, render: true) } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 34cbc328b2..186ad786c2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -38,24 +38,36 @@ final class MediaEditorScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let isDisplayingTool: Bool + let isInteractingWithEntities: Bool + let isDismissing: Bool let mediaEditor: MediaEditor? let privacy: MediaEditorResultPrivacy let selectedEntity: DrawingEntity? + let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void init( context: AccountContext, + isDisplayingTool: Bool, + isInteractingWithEntities: Bool, + isDismissing: Bool, mediaEditor: MediaEditor?, privacy: MediaEditorResultPrivacy, selectedEntity: DrawingEntity?, + entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context + self.isDisplayingTool = isDisplayingTool + self.isInteractingWithEntities = isInteractingWithEntities + self.isDismissing = isDismissing self.mediaEditor = mediaEditor self.privacy = privacy self.selectedEntity = selectedEntity + self.entityViewForEntity = entityViewForEntity self.openDrawing = openDrawing self.openTools = openTools } @@ -64,6 +76,15 @@ final class MediaEditorScreenComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.isDisplayingTool != rhs.isDisplayingTool { + return false + } + if lhs.isInteractingWithEntities != rhs.isInteractingWithEntities { + return false + } + if lhs.isDismissing != rhs.isDismissing { + return false + } if lhs.privacy != rhs.privacy { return false } @@ -175,6 +196,12 @@ final class MediaEditorScreenComponent: Component { private let saveButton = ComponentView() private let settingsButton = ComponentView() + private let textCancelButton = ComponentView() + private let textDoneButton = ComponentView() + private let textSize = ComponentView() + + private var isDismissed = false + private var component: MediaEditorScreenComponent? private weak var state: State? private var environment: ViewControllerComponentContainer.Environment? @@ -249,6 +276,7 @@ final class MediaEditorScreenComponent: Component { } func animateOut(to source: TransitionAnimationSource) { + self.isDismissed = true let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) @@ -265,7 +293,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: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } @@ -277,7 +305,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.inputPanel.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } @@ -303,13 +331,12 @@ final class MediaEditorScreenComponent: Component { if let view = self.scrubber.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } - func animateOutToTool() { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + func animateOutToTool(transition: Transition) { if let view = self.cancelButton.view { view.alpha = 0.0 } @@ -324,49 +351,24 @@ final class MediaEditorScreenComponent: Component { for button in buttons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - transition.setAlpha(view: view, alpha: 0.0) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } if let view = self.doneButton.view { - transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } if let view = self.inputPanel.view { - transition.setAlpha(view: view, alpha: 0.0) - view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) - } - - if let view = self.saveButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.muteButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.settingsButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.privacyButton.view { - transition.setAlpha(view: view, alpha: 0.0) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.scrubber.view { - transition.setAlpha(view: view, alpha: 0.0) view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } - func animateInFromTool() { - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + func animateInFromTool(transition: Transition) { if let view = self.cancelButton.view { view.alpha = 1.0 } @@ -381,48 +383,27 @@ final class MediaEditorScreenComponent: Component { for button in buttons { if let view = button.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - transition.setAlpha(view: view, alpha: 1.0) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } if let view = self.doneButton.view { - transition.setAlpha(view: view, alpha: 1.0) transition.setScale(view: view, scale: 1.0) } if let view = self.inputPanel.view { - transition.setAlpha(view: view, alpha: 1.0) - view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) - } - - if let view = self.saveButton.view { - transition.setAlpha(view: view, alpha: 1.0) - transition.setScale(view: view, scale: 1.0) - } - - if let view = self.muteButton.view { - transition.setAlpha(view: view, alpha: 1.0) - transition.setScale(view: view, scale: 1.0) - } - - if let view = self.settingsButton.view { - transition.setAlpha(view: view, alpha: 1.0) - transition.setScale(view: view, scale: 1.0) - } - - if let view = self.privacyButton.view { - transition.setAlpha(view: view, alpha: 1.0) view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } if let view = self.scrubber.view { - transition.setAlpha(view: view, alpha: 1.0) view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } } func update(component: MediaEditorScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + guard !self.isDismissed else { + return availableSize + } let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment @@ -469,6 +450,7 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) + transition.setAlpha(view: cancelButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let doneButtonSize = self.doneButton.update( @@ -507,6 +489,7 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: doneButtonView, position: doneButtonFrame.center) transition.setBounds(view: doneButtonView, bounds: CGRect(origin: .zero, size: doneButtonFrame.size)) + transition.setAlpha(view: doneButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let drawButtonSize = self.drawButton.update( @@ -531,7 +514,9 @@ final class MediaEditorScreenComponent: Component { if drawButtonView.superview == nil { self.addSubview(drawButtonView) } - transition.setFrame(view: drawButtonView, frame: drawButtonFrame) + 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) } let textButtonSize = self.textButton.update( @@ -556,7 +541,9 @@ final class MediaEditorScreenComponent: Component { if textButtonView.superview == nil { self.addSubview(textButtonView) } - transition.setFrame(view: textButtonView, frame: textButtonFrame) + 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) } let stickerButtonSize = self.stickerButton.update( @@ -581,7 +568,9 @@ final class MediaEditorScreenComponent: Component { if stickerButtonView.superview == nil { self.addSubview(stickerButtonView) } - transition.setFrame(view: stickerButtonView, frame: stickerButtonFrame) + 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) } let toolsButtonSize = self.toolsButton.update( @@ -606,7 +595,9 @@ final class MediaEditorScreenComponent: Component { if toolsButtonView.superview == nil { self.addSubview(toolsButtonView) } - transition.setFrame(view: toolsButtonView, frame: toolsButtonFrame) + 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) } let mediaEditor = component.mediaEditor @@ -627,8 +618,7 @@ final class MediaEditorScreenComponent: Component { framesUpdateTimestamp: playerState.framesUpdateTimestamp, trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in if let mediaEditor { - mediaEditor.setVideoTrimStart(start) - mediaEditor.setVideoTrimEnd(end) + mediaEditor.setVideoTrimRange(start.. 0.0 { @@ -738,12 +736,7 @@ final class MediaEditorScreenComponent: Component { self.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) - - if inputPanelOffset > 0.0 && component.selectedEntity != nil { - isEditingTextEntity = true - inputPanelAlpha = 0.0 - } - transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha) + transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } let privacyText: String @@ -767,6 +760,8 @@ final class MediaEditorScreenComponent: Component { } } + let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) + let privacyButtonSize = self.privacyButton.update( transition: transition, component: AnyComponent(Button( @@ -795,8 +790,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: privacyButtonView, position: privacyButtonFrame.center) transition.setBounds(view: privacyButtonView, bounds: CGRect(origin: .zero, size: privacyButtonFrame.size)) - transition.setScale(view: privacyButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0) - transition.setAlpha(view: privacyButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0) + transition.setScale(view: privacyButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } let saveButtonSize = self.saveButton.update( @@ -839,8 +834,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) - transition.setScale(view: saveButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0) - transition.setAlpha(view: saveButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0) + transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } if let playerState = state.playerState, playerState.hasAudio { @@ -883,8 +878,8 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) - transition.setScale(view: muteButtonView, scale: self.inputPanelExternalState.isEditing ? 0.01 : 1.0) - transition.setAlpha(view: muteButtonView, alpha: self.inputPanelExternalState.isEditing ? 0.0 : 1.0) + transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } } @@ -921,11 +916,99 @@ final class MediaEditorScreenComponent: Component { } transition.setPosition(view: settingsButtonView, position: settingsButtonFrame.center) transition.setBounds(view: settingsButtonView, bounds: CGRect(origin: .zero, size: settingsButtonFrame.size)) - transition.setScale(view: settingsButtonView, scale: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.01 : 1.0) - transition.setAlpha(view: settingsButtonView, alpha: self.inputPanelExternalState.isEditing || isEditingTextEntity ? 0.0 : 1.0) + transition.setScale(view: settingsButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: settingsButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } } + let textCancelButtonSize = self.textCancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white) + ), + action: { + if let controller = environment.controller() as? MediaEditorScreen { + controller.node.interaction?.endTextEditing(reset: true) + } + } + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 30.0) + ) + let textCancelButtonFrame = CGRect( + origin: CGPoint(x: 13.0, y: environment.statusBarHeight + 20.0), + size: textCancelButtonSize + ) + if let textCancelButtonView = self.textCancelButton.view { + if textCancelButtonView.superview == nil { + self.addSubview(textCancelButtonView) + } + transition.setPosition(view: textCancelButtonView, position: textCancelButtonFrame.center) + transition.setBounds(view: textCancelButtonView, bounds: CGRect(origin: .zero, size: textCancelButtonFrame.size)) + transition.setScale(view: textCancelButtonView, scale: isEditingTextEntity ? 1.0 : 0.01) + transition.setAlpha(view: textCancelButtonView, alpha: isEditingTextEntity ? 1.0 : 0.0) + } + + let textDoneButtonSize = self.textDoneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + Text(text: environment.strings.Common_Done, font: Font.regular(17.0), color: .white) + ), + action: { + if let controller = environment.controller() as? MediaEditorScreen { + controller.node.interaction?.endTextEditing(reset: false) + } + } + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 30.0) + ) + let textDoneButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - textDoneButtonSize.width - 13.0, y: environment.statusBarHeight + 20.0), + size: textDoneButtonSize + ) + if let textDoneButtonView = self.textDoneButton.view { + if textDoneButtonView.superview == nil { + self.addSubview(textDoneButtonView) + } + transition.setPosition(view: textDoneButtonView, position: textDoneButtonFrame.center) + transition.setBounds(view: textDoneButtonView, bounds: CGRect(origin: .zero, size: textDoneButtonFrame.size)) + transition.setScale(view: textDoneButtonView, scale: isEditingTextEntity ? 1.0 : 0.01) + transition.setAlpha(view: textDoneButtonView, alpha: isEditingTextEntity ? 1.0 : 0.0) + } + + let textSizeSize = self.textSize.update( + transition: transition, + component: AnyComponent(TextSizeSliderComponent( + value: sizeValue ?? 0.5, + tag: nil, + updated: { [weak state] size in + if let controller = environment.controller() as? MediaEditorScreen { + controller.node.interaction?.updateEntitySize(size) + state?.updated() + } + }, released: { + } + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 240.0) + ) + let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : environment.safeInsets.bottom + let textSizeFrame = CGRect( + origin: CGPoint(x: 0.0, y: environment.safeInsets.top + (availableSize.height - environment.safeInsets.top - bottomInset) / 2.0 - textSizeSize.height / 2.0), + size: textSizeSize + ) + if let textSizeView = self.textSize.view { + if textSizeView.superview == nil { + self.addSubview(textSizeView) + } + transition.setPosition(view: textSizeView, position: textSizeFrame.center) + transition.setBounds(view: textSizeView, bounds: CGRect(origin: .zero, size: textSizeFrame.size)) + transition.setAlpha(view: textSizeView, alpha: sizeSliderVisible && !component.isInteractingWithEntities ? 1.0 : 0.0) + } + return availableSize } } @@ -986,7 +1069,7 @@ public final class MediaEditorScreen: ViewController { } struct State { - var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: true) + var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: false) } var state = State() { @@ -998,7 +1081,7 @@ public final class MediaEditorScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext - private var interaction: DrawingToolsInteraction? + fileprivate var interaction: DrawingToolsInteraction? private let initializationTimestamp = CACurrentMediaTime() fileprivate var subject: MediaEditorScreen.Subject? @@ -1006,6 +1089,8 @@ public final class MediaEditorScreen: ViewController { private let backgroundDimView: UIView fileprivate let componentHost: ComponentView + fileprivate let storyPreview: ComponentView + fileprivate let toolValue: ComponentView private let previewContainerView: UIView private var transitionInView: UIImageView? @@ -1022,6 +1107,15 @@ public final class MediaEditorScreen: ViewController { private let stickerPickerInputData = Promise() + private var dismissPanGestureRecognizer: UIPanGestureRecognizer? + + private var isDisplayingTool = false + private var isInteractingWithEntities = false + private var isEnhacing = false + private var isDismissing = false + private var dismissOffset: CGFloat = 0.0 + private var isDismissed = false + private var presentationData: PresentationData private var validLayout: ContainerViewLayout? @@ -1032,10 +1126,12 @@ public final class MediaEditorScreen: ViewController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.backgroundDimView = UIView() - self.backgroundDimView.alpha = 0.0 + self.backgroundDimView.isHidden = true self.backgroundDimView.backgroundColor = .black self.componentHost = ComponentView() + self.storyPreview = ComponentView() + self.toolValue = ComponentView() self.previewContainerView = UIView() self.previewContainerView.alpha = 0.0 @@ -1052,6 +1148,9 @@ public final class MediaEditorScreen: ViewController { self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } + self.entitiesView.getEntityEdgePositions = { + return UIEdgeInsets(top: 160.0, left: 36.0, bottom: storyDimensions.height - 160.0, right: storyDimensions.width - 36.0) + } self.previewView = MediaEditorPreviewView(frame: .zero) self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false @@ -1068,7 +1167,7 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) - self.previewContainerView.addSubview(self.drawingView) + self.entitiesView.addSubview(self.drawingView) self.previewContainerView.addSubview(self.selectionContainerView) self.subjectDisposable = ( @@ -1137,6 +1236,22 @@ public final class MediaEditorScreen: ViewController { stickerPickerInputData.set(signal) }) + + self.entitiesView.edgePreviewUpdated = { [weak self] preview in + if let self { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if let storyPreviewView = self.storyPreview.view { + transition.updateAlpha(layer: storyPreviewView.layer, alpha: preview ? 0.4 : 0.0) + } + } + } + + self.entitiesView.onInteractionUpdated = { [weak self] interacting in + if let self { + self.isInteractingWithEntities = interacting + self.requestUpdate(transition: .easeInOut(duration: 0.2)) + } + } } deinit { @@ -1151,7 +1266,6 @@ public final class MediaEditorScreen: ViewController { } let mediaDimensions = subject.dimensions - let maxSide: CGFloat = 1920.0 / UIScreen.main.scale let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) @@ -1168,6 +1282,7 @@ public final class MediaEditorScreen: ViewController { let initialRotation = mediaEntity.rotation if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { + self.entitiesView.sendSubviewToBack(entityView) entityView.previewView = self.previewView entityView.updated = { [weak self, weak mediaEntity] in if let self, let mediaEntity { @@ -1204,10 +1319,10 @@ public final class MediaEditorScreen: ViewController { 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.alpha = 1.0 + self.backgroundDimView.isHidden = false }) } else { - self.backgroundDimView.alpha = 1.0 + self.backgroundDimView.isHidden = false } } } @@ -1221,6 +1336,12 @@ public final class MediaEditorScreen: ViewController { self.view.disablesInteractiveModalDismiss = true self.view.disablesInteractiveKeyboardGestureRecognizer = true + let dismissPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handleDismissPan(_:))) + dismissPanGestureRecognizer.delegate = self + dismissPanGestureRecognizer.maximumNumberOfTouches = 1 + self.previewContainerView.addGestureRecognizer(dismissPanGestureRecognizer) + self.dismissPanGestureRecognizer = dismissPanGestureRecognizer + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) panGestureRecognizer.delegate = self panGestureRecognizer.minimumNumberOfTouches = 2 @@ -1245,8 +1366,10 @@ public final class MediaEditorScreen: ViewController { entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, isVideo: false, - updateSelectedEntity: { _ in - + updateSelectedEntity: { [weak self] _ in + if let self { + self.requestUpdate() + } }, updateVideoPlayback: { [weak self] isPlaying in if let self, let mediaEditor = self.mediaEditor { @@ -1286,6 +1409,76 @@ public final class MediaEditorScreen: ViewController { return true } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === self.dismissPanGestureRecognizer { + if self.isDisplayingTool || self.entitiesView.hasSelection { + return false + } + return true + } else { + return true + } + } + + @objc func handleDismissPan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let controller = self.controller, let layout = self.validLayout, (layout.inputHeight ?? 0.0).isZero else { + return + } + + var hasSwipeToDismiss = false + if let subject = self.subject { + if case .asset = subject { + hasSwipeToDismiss = true + } else if case .draft = subject { + hasSwipeToDismiss = true + } + } + + let translation = gestureRecognizer.translation(in: self.view) + let velocity = gestureRecognizer.velocity(in: self.view) + switch gestureRecognizer.state { + case .changed: + if abs(translation.y) > 10.0 && !self.isEnhacing && hasSwipeToDismiss { + if !self.isDismissing { + self.isDismissing = true + controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) + } + } else if abs(translation.x) > 10.0 && !self.isDismissing { + self.isEnhacing = true + controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + } + + if self.isDismissing { + self.dismissOffset = translation.y + controller.requestLayout(transition: .immediate) + } else if self.isEnhacing { + if let mediaEditor = self.mediaEditor { + let value = mediaEditor.getToolValue(.enhance) as? Float ?? 0.0 + let delta = Float((translation.x / self.frame.width) * 1.5) + let updatedValue = max(0.0, min(1.0, value + delta)) + mediaEditor.setToolValue(.enhance, value: updatedValue) + } + self.requestUpdate() + gestureRecognizer.setTranslation(.zero, in: self.view) + } + case .ended, .cancelled: + if self.isDismissing { + if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0 { + controller.requestDismiss(saveDraft: false, animated: true) + } else { + self.dismissOffset = 0.0 + self.isDismissing = false + controller.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) + } + } else { + self.isEnhacing = false + controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) + } + default: + break + } + } + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { self.entitiesView.handlePan(gestureRecognizer) } @@ -1312,6 +1505,29 @@ public final class MediaEditorScreen: ViewController { } } + private func setupTransitionImage(_ image: UIImage) { + self.previewContainerView.alpha = 1.0 + + let transitionInView = UIImageView(image: image) + var initialScale: CGFloat + if image.size.height > image.size.width { + initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) + } else { + initialScale = self.previewContainerView.bounds.width / image.size.width + } + transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) + transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) + self.previewContainerView.addSubview(transitionInView) + self.transitionInView = transitionInView + + self.mediaEditor?.onFirstDisplay = { [weak self] in + if let self, let transitionInView = self.transitionInView { + transitionInView.removeFromSuperview() + self.transitionInView = nil + } + } + } + func animateIn() { if let transitionIn = self.controller?.transitionIn { switch transitionIn { @@ -1319,28 +1535,12 @@ public final class MediaEditorScreen: ViewController { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { view.animateIn(from: .camera) } + if let subject = self.subject, case let .video(_, transitionImage, _) = subject, let transitionImage { + self.setupTransitionImage(transitionImage) + } case let .gallery(transitionIn): if let sourceImage = transitionIn.sourceImage { - self.previewContainerView.alpha = 1.0 - - let transitionInView = UIImageView(image: sourceImage) - var initialScale: CGFloat - if sourceImage.size.height > sourceImage.size.width { - initialScale = max(self.previewContainerView.bounds.width / sourceImage.size.width, self.previewContainerView.bounds.height / sourceImage.size.height) - } else { - initialScale = self.previewContainerView.bounds.width / sourceImage.size.width - } - transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) - transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) - self.previewContainerView.addSubview(transitionInView) - self.transitionInView = transitionInView - - self.mediaEditor?.onFirstDisplay = { [weak self] in - if let self, let transitionInView = self.transitionInView { - transitionInView.removeFromSuperview() - self.transitionInView = nil - } - } + self.setupTransitionImage(sourceImage) } if let sourceView = transitionIn.sourceView { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -1357,7 +1557,7 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - self.backgroundDimView.alpha = 1.0 + self.backgroundDimView.isHidden = false self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) if let componentView = self.componentHost.view { @@ -1378,10 +1578,12 @@ public final class MediaEditorScreen: ViewController { guard let controller = self.controller else { return } + self.isDismissed = true controller.statusBar.statusBarStyle = .Ignore + let previousDimAlpha = self.backgroundDimView.alpha self.backgroundDimView.alpha = 0.0 - self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.25) if finished, case .message = controller.state.privacy { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -1491,15 +1693,23 @@ public final class MediaEditorScreen: ViewController { } func animateOutToTool() { + self.isDisplayingTool = true + + let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToTool() + view.animateOutToTool(transition: transition) } + self.requestUpdate(transition: transition) } func animateInFromTool() { + self.isDisplayingTool = false + + let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromTool() + view.animateInFromTool(transition: transition) } + self.requestUpdate(transition: transition) } func presentPrivacyTooltip() { @@ -1517,27 +1727,62 @@ public final class MediaEditorScreen: ViewController { self.controller?.present(tooltipController, in: .current) } + private weak var saveTooltip: SaveProgressScreen? func presentSaveTooltip() { - guard let sourceView = self.componentHost.findTaggedView(tag: saveButtonTag) else { + guard let controller = self.controller else { return } - let parentFrame = self.view.convert(self.bounds, to: nil) - let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) - let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) + if let saveTooltip = self.saveTooltip { + if case .completion = saveTooltip.content { + saveTooltip.dismiss() + self.saveTooltip = nil + } + } let text: String let isVideo = self.mediaEditor?.resultIsVideo ?? false if isVideo { - text = "Video saved to Photos" + text = "Video saved to Photos." } else { - text = "Image saved to Photos" + text = "Image saved to Photos." } - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in - return .ignore - }) - self.controller?.present(tooltipController, in: .current) + if let tooltipController = self.saveTooltip { + tooltipController.content = .completion(text) + } else { + let tooltipController = SaveProgressScreen(context: self.context, content: .completion(text)) + controller.present(tooltipController, in: .current) + self.saveTooltip = tooltipController + } + } + + func updateVideoExportProgress(_ progress: Float) { + guard let controller = self.controller else { + return + } + + if let saveTooltip = self.saveTooltip { + if case .completion = saveTooltip.content { + saveTooltip.dismiss() + self.saveTooltip = nil + } + } + + let text = "Preparing video..." + + if let tooltipController = self.saveTooltip { + tooltipController.content = .progress(text, progress) + } else { + let tooltipController = SaveProgressScreen(context: self.context, content: .progress(text, 0.0)) + tooltipController.cancelled = { [weak self] in + if let self, let controller = self.controller { + controller.cancelVideoExport() + } + } + controller.present(tooltipController, in: .current) + self.saveTooltip = tooltipController + } } private weak var storyArchiveTooltip: ViewController? @@ -1553,7 +1798,7 @@ public final class MediaEditorScreen: ViewController { let parentFrame = self.view.convert(self.bounds, to: nil) let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) - let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize()) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 5.0), size: CGSize()) let text: String if archive { @@ -1562,7 +1807,7 @@ public final class MediaEditorScreen: ViewController { text = "Story will disappear in 24 hours." } - let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, cornerRadius: 9.0, shouldDismissOnTouch: { _ in return .ignore }) self.storyArchiveTooltip = tooltipController @@ -1578,15 +1823,15 @@ public final class MediaEditorScreen: ViewController { return result } - func requestUpdate() { + func requestUpdate(transition: Transition = .immediate) { if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: .immediate) + self.containerLayoutUpdated(layout: layout, transition: transition) } } private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { - guard let controller = self.controller else { + guard let controller = self.controller, !self.isDismissed else { return } let isFirstTime = self.validLayout == nil @@ -1622,9 +1867,19 @@ public final class MediaEditorScreen: ViewController { component: AnyComponent( MediaEditorScreenComponent( context: self.context, + isDisplayingTool: self.isDisplayingTool, + isInteractingWithEntities: self.isInteractingWithEntities, + isDismissing: self.isDismissing, mediaEditor: self.mediaEditor, privacy: controller.state.privacy, - selectedEntity: self.entitiesView.selectedEntityView?.entity, + selectedEntity: self.isDisplayingTool ? nil : self.entitiesView.selectedEntityView?.entity, + entityViewForEntity: { [weak self] entity in + if let self { + return self.entitiesView.getView(for: entity.uuid) + } else { + return nil + } + }, openDrawing: { [weak self] mode in if let self { if self.entitiesView.hasSelection { @@ -1659,8 +1914,8 @@ public final class MediaEditorScreen: ViewController { self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() - self?.entitiesView.selectEntity(nil) self?.interaction?.activate() + self?.entitiesView.selectEntity(nil) } controller.requestApply = { [weak controller, weak self] in self?.drawingScreen = nil @@ -1676,8 +1931,8 @@ public final class MediaEditorScreen: ViewController { self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) } - self?.entitiesView.selectEntity(nil) self?.interaction?.activate() + self?.entitiesView.selectEntity(nil) } self.controller?.present(controller, in: .current) self.animateOutToTool() @@ -1712,32 +1967,72 @@ public final class MediaEditorScreen: ViewController { self.view.insertSubview(componentView, at: 3) componentView.clipsToBounds = true } - let componentFrame = CGRect(origin: .zero, size: componentSize) - transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize)) } + let storyPreviewSize = self.storyPreview.update( + transition: transition, + component: AnyComponent( + StoryPreviewComponent( + context: self.context, + caption: "" + ) + ), + environment: {}, + forceUpdate: false, + containerSize: previewSize + ) + if let storyPreviewView = self.storyPreview.view { + if storyPreviewView.superview == nil { + storyPreviewView.alpha = 0.0 + storyPreviewView.isUserInteractionEnabled = false + self.previewContainerView.addSubview(storyPreviewView) + } + transition.setFrame(view: storyPreviewView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: storyPreviewSize)) + } + + let enhanceValue = self.mediaEditor?.getToolValue(.enhance) as? Float ?? 0.0 + let toolValueSize = self.toolValue.update( + transition: transition, + component: AnyComponent( + ToolValueComponent( + title: "Enhance", + value: "\(Int(enhanceValue * 100.0))" + ) + ), + environment: {}, + forceUpdate: false, + containerSize: CGSize(width: previewSize.width, height: 120.0) + ) + if let toolValueView = self.toolValue.view { + if toolValueView.superview == nil { + toolValueView.alpha = 0.0 + toolValueView.isUserInteractionEnabled = false + self.previewContainerView.addSubview(toolValueView) + } + transition.setFrame(view: toolValueView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - toolValueSize.width) / 2.0), y: 88.0), size: toolValueSize)) + transition.setAlpha(view: toolValueView, alpha: self.isEnhacing ? 1.0 : 0.0) + } + + transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) + transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0) + var bottomInputOffset: CGFloat = 0.0 if let inputHeight = layout.inputHeight, inputHeight > 0.0 { - if self.entitiesView.selectedEntityView != nil { + if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { bottomInputOffset = inputHeight / 2.0 } else { bottomInputOffset = inputHeight - topInset - 17.0 } } - - transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) - - var previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset), size: previewSize) - if let inputHeight = layout.inputHeight, inputHeight > 0.0, self.drawingScreen != nil { - previewFrame = previewFrame.offsetBy(dx: 0.0, dy: inputHeight / 2.0) - } + let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize) transition.setFrame(view: self.previewContainerView, frame: previewFrame) let entitiesViewScale = previewSize.width / storyDimensions.width self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) transition.setFrame(view: self.entitiesContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) - transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: self.entitiesView.bounds.size)) transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) @@ -1755,13 +2050,13 @@ public final class MediaEditorScreen: ViewController { public enum Subject { case image(UIImage, PixelDimensions) - case video(String, PixelDimensions) + case video(String, UIImage?, PixelDimensions) 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)) @@ -1774,8 +2069,8 @@ public final class MediaEditorScreen: ViewController { switch self { case let .image(image, dimensions): return .image(image, dimensions) - case let .video(videoPath, dimensions): - return .video(videoPath, dimensions) + case let .video(videoPath, transitionImage, dimensions): + return .video(videoPath, transitionImage, dimensions) case let .asset(asset): return .asset(asset) case let .draft(draft): @@ -1787,7 +2082,7 @@ public final class MediaEditorScreen: ViewController { 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) @@ -1813,7 +2108,7 @@ public final class MediaEditorScreen: ViewController { fileprivate let transitionOut: (Bool) -> TransitionOut? public var cancelled: (Bool) -> Void = { _ in } - public var completion: (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void = { _, _, _ in } + public var completion: (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void = { _, _, _ in } public var dismissed: () -> Void = { } public init( @@ -1821,7 +2116,7 @@ public final class MediaEditorScreen: ViewController { subject: Signal, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void + completion: @escaping (MediaEditorScreen.Result, MediaEditorResultPrivacy, @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context self.subject = subject @@ -1842,6 +2137,10 @@ public final class MediaEditorScreen: ViewController { fatalError("init(coder:) has not been implemented") } + deinit { + self.exportDisposable.dispose() + } + override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -2019,7 +2318,11 @@ public final class MediaEditorScreen: ViewController { } func maybePresentDiscardAlert() { - if let subject = self.node.subject, case .asset = subject { + if "".isEmpty { + self.requestDismiss(saveDraft: false, animated: true) + return + } + if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false { self.requestDismiss(saveDraft: false, animated: true) return } @@ -2059,31 +2362,10 @@ public final class MediaEditorScreen: ViewController { } func requestDismiss(saveDraft: Bool, animated: Bool) { - if saveDraft, let subject = self.node.subject, let values = self.node.mediaEditor?.values { - if let resultImage = self.node.mediaEditor?.resultImage { - makeEditorImageComposition(account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in - guard let resultImage else { - return - } - let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) - if case let .image(image, dimensions) = subject { - if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = NSTemporaryDirectory() + "\(Int64.random(in: .min ... .max)).jpg" - if let data = image.jpegData(compressionQuality: 0.87) { - try? data.write(to: URL(fileURLWithPath: path)) - let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values) - addStoryDraft(engine: self.context.engine, item: draft) - } - } - } else if case let .draft(draft) = subject { - if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) - let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values) - addStoryDraft(engine: self.context.engine, item: draft) - } - } - }) - } + self.dismissAllTooltips() + + if saveDraft { + self.saveDraft() } else { if case let .draft(draft) = self.node.subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) @@ -2101,14 +2383,54 @@ public final class MediaEditorScreen: ViewController { self?.dismissed() }) } - - func requestCompletion(caption: NSAttributedString, animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { + + private func saveDraft() { + guard let subject = self.node.subject, let values = self.node.mediaEditor?.values else { return } + try? FileManager.default.createDirectory(atPath: draftPath(), withIntermediateDirectories: true) + + if let resultImage = self.node.mediaEditor?.resultImage { + makeEditorImageComposition(account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in + guard let resultImage else { + return + } + let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) + if case let .image(image, dimensions) = subject { + if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { + let path = draftPath() + "\(Int64.random(in: .min ... .max)).jpg" + if let data = image.jpegData(compressionQuality: 0.87) { + try? data.write(to: URL(fileURLWithPath: path)) + let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values) + addStoryDraft(engine: self.context.engine, item: draft) + } + } + } else if case let .draft(draft) = subject { + if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { + removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) + let draft = MediaEditorDraft(path: draft.path, isVideo: draft.isVideo, thumbnail: thumbnailImage, dimensions: draft.dimensions, values: values) + addStoryDraft(engine: self.context.engine, item: draft) + } + } + }) + } + } + + private var didComplete = false + func requestCompletion(caption: NSAttributedString, animated: Bool) { + guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, !self.didComplete else { + return + } + self.didComplete = true + + self.dismissAllTooltips() mediaEditor.stop() + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) @@ -2124,7 +2446,7 @@ public final class MediaEditorScreen: ViewController { } videoResult = .imageFile(path: tempImagePath) duration = 5.0 - case let .video(path, _): + case let .video(path, _, _): videoResult = .videoFile(path: path) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -2155,11 +2477,14 @@ public final class MediaEditorScreen: ViewController { duration = 5.0 } } - self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 720, height: 1280), caption: caption), { [weak self] in + self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } }) - }, self.state.privacy) + }) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) @@ -2168,11 +2493,14 @@ public final class MediaEditorScreen: ViewController { if let image = mediaEditor.resultImage { makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in if let resultImage { - self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), { [weak self] in + self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), self.state.privacy, { [weak self] finished in self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } }) - }, self.state.privacy) + }) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } @@ -2183,8 +2511,9 @@ public final class MediaEditorScreen: ViewController { } private var videoExport: MediaEditorVideoExport? - private var exportDisposable: Disposable? + private var exportDisposable = MetaDisposable() + private var previousSavedValues: MediaEditorValues? func requestSave() { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { return @@ -2194,6 +2523,12 @@ public final class MediaEditorScreen: ViewController { let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + if let previousSavedValues = self.previousSavedValues, mediaEditor.values == previousSavedValues { + self.node.presentSaveTooltip() + return + } + self.previousSavedValues = mediaEditor.values + let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" let saveToPhotos: (String, Bool) -> Void = { path, isVideo in PHPhotoLibrary.shared().performChanges({ @@ -2215,9 +2550,12 @@ public final class MediaEditorScreen: ViewController { } if mediaEditor.resultIsVideo { + mediaEditor.stop() + self.node.entitiesView.pause() + 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, _): @@ -2265,28 +2603,46 @@ public final class MediaEditorScreen: ViewController { let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath) self.videoExport = videoExport - videoExport.startExport() + videoExport.start() - self.exportDisposable = (videoExport.status + self.exportDisposable.set((videoExport.status |> deliverOnMainQueue).start(next: { [weak self] status in if let self { - if case .completed = status { + switch status { + case .completed: self.videoExport = nil saveToPhotos(outputPath, true) self.node.presentSaveTooltip() + + self.node.mediaEditor?.play() + self.node.entitiesView.play() + case let .progress(progress): + if self.videoExport != nil { + self.node.updateVideoExportProgress(progress) + } + case .failed: + self.videoExport = nil + self.node.mediaEditor?.play() + self.node.entitiesView.play() + case .unknown: + break } } - }) + })) }) } else { if let image = mediaEditor.resultImage { - makeEditorImageComposition(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)) - saveToPhotos(outputPath, false) - } - }) + Queue.concurrentDefaultQueue().async { + makeEditorImageComposition(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)) + Queue.mainQueue().async { + saveToPhotos(outputPath, false) + } + } + }) + } self.node.presentSaveTooltip() } } @@ -2296,6 +2652,36 @@ public final class MediaEditorScreen: ViewController { } + fileprivate func cancelVideoExport() { + if let videoExport = self.videoExport { + self.previousSavedValues = nil + + videoExport.cancel() + self.videoExport = nil + self.exportDisposable.set(nil) + + self.node.mediaEditor?.play() + self.node.entitiesView.play() + } + } + + private func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss() + } + if let controller = controller as? SaveProgressScreen { + controller.dismiss() + } + return true + }) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -2386,3 +2772,118 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) } } + +private func draftPath() -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/storyDrafts" +} + + +private final class ToolValueComponent: Component { + typealias EnvironmentType = Empty + + let title: String + let value: String + + init( + title: String, + value: String + ) { + self.title = title + self.value = value + } + + static func ==(lhs: ToolValueComponent, rhs: ToolValueComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private let title = ComponentView() + private let value = ComponentView() + + private let hapticFeedback = HapticFeedback() + + private var component: ToolValueComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ToolValueComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + + let previousValue = self.component?.value + self.component = component + self.state = state + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: component.title, + font: Font.light(34.0), + color: .white + )), + environment: {}, + containerSize: CGSize(width: 180.0, height: 44.0) + ) + let titleFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 0.0), + size: titleSize + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleFrame.size)) + } + + let valueSize = self.value.update( + transition: transition, + component: AnyComponent(Text( + text: component.value, + font: Font.with(size: 90.0, weight: .thin, traits: .monospacedNumbers), + color: .white + )), + environment: {}, + containerSize: CGSize(width: 180.0, height: 44.0) + ) + let valueFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - valueSize.width) / 2.0), y: 40.0), + size: valueSize + ) + if let valueView = self.value.view { + if valueView.superview == nil { + self.addSubview(valueView) + } + transition.setPosition(view: valueView, position: valueFrame.center) + transition.setBounds(view: valueView, bounds: CGRect(origin: .zero, size: valueFrame.size)) + } + + if let previousValue, component.value != previousValue, self.alpha > 0.0 { + self.hapticFeedback.impact(.click05) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift new file mode 100644 index 0000000000..596920b3df --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift @@ -0,0 +1,577 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import MultilineTextComponent +import LottieAnimationComponent +import BundleIconComponent + +private final class ProgressComponent: Component { + typealias EnvironmentType = Empty + + let title: String + let value: Float + let cancel: () -> Void + + init( + title: String, + value: Float, + cancel: @escaping () -> Void + ) { + self.title = title + self.value = value + self.cancel = cancel + } + + static func ==(lhs: ProgressComponent, rhs: ProgressComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private let title = ComponentView() + private let progressLayer = SimpleShapeLayer() + private let cancelButton = ComponentView() + + private var component: ProgressComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + let lineWidth: CGFloat = 3.0 + let progressSize = CGSize(width: 42.0, height: 42.0) + + self.progressLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: progressSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), transform: nil) + self.progressLayer.lineWidth = lineWidth + self.progressLayer.strokeColor = UIColor.white.cgColor + self.progressLayer.fillColor = UIColor.clear.cgColor + self.progressLayer.lineCap = .round + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.progressLayer.bounds = CGRect(origin: .zero, size: progressSize) + + self.layer.addSublayer(self.progressLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let minWidth: CGFloat = 98.0 + let inset: CGFloat = 16.0 + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text(text: component.title, font: Font.regular(14.0), color: .white)), + environment: {}, + containerSize: CGSize(width: 160.0, height: 40.0) + ) + + let width: CGFloat = max(minWidth, titleSize.width + inset * 2.0) + let titleFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 16.0), + size: titleSize + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let progressPosition = CGPoint(x: width / 2.0, y: titleFrame.maxY + 34.0) + self.progressLayer.position = progressPosition + transition.setShapeLayerStrokeEnd(layer: self.progressLayer, strokeEnd: CGFloat(max(0.027, component.value))) + + if self.progressLayer.animation(forKey: "rotation") == nil { + let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + basicAnimation.duration = 2.0 + basicAnimation.fromValue = NSNumber(value: Float(0.0)) + basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0)) + basicAnimation.repeatCount = Float.infinity + basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + self.progressLayer.add(basicAnimation, forKey: "rotation") + } + + let cancelSize = self.cancelButton.update( + transition: transition, + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent( + name: "Media Gallery/Close", + tintColor: UIColor.white + ) + ), + action: { [weak self] in + if let self, let component = self.component { + component.cancel() + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: 160.0, height: 40.0) + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(progressPosition.x - cancelSize.width / 2.0), y: floorToScreenPixels(progressPosition.y - cancelSize.height / 2.0)), size: cancelSize) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + cancelButtonView.frame = cancelButtonFrame + } + + return CGSize(width: width, height: 104.0) + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class BannerComponent: Component { + typealias EnvironmentType = Empty + + let iconName: String + let text: String + + init( + iconName: String, + text: String + ) { + self.iconName = iconName + self.text = text + } + + static func ==(lhs: BannerComponent, rhs: BannerComponent) -> Bool { + if lhs.iconName != rhs.iconName { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + public final class View: UIView { + private let icon = ComponentView() + private let text = ComponentView() + + private var component: BannerComponent? + private weak var state: EmptyComponentState? + + func update(component: BannerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let height: CGFloat = 49.0 + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent( + LottieAnimationComponent(animation: LottieAnimationComponent.AnimationItem(name: component.iconName, mode: .animating(loop: false)), colors: [:], size: CGSize(width: 32.0, height: 32.0)) + ), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + let iconFrame = CGRect( + origin: CGPoint(x: 9.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)), + size: iconSize + ) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + + let textSize = self.text.update( + transition: transition, + component: AnyComponent( + Text(text: component.text, font: Font.regular(14.0), color: .white) + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: height) + ) + + let textFrame = CGRect( + origin: CGPoint(x: iconFrame.maxX + 9.0, y: floorToScreenPixels((height - textSize.height) / 2.0)), + size: textSize + ) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = textFrame + } + + return CGSize(width: textFrame.maxX + 12.0, height: height) + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class SaveProgressScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + enum Content: Equatable { + enum ContentType: Equatable { + case progress + case completion + } + + case progress(String, Float) + case completion(String) + + var type: ContentType { + switch self { + case .progress: + return .progress + case .completion: + return .completion + } + } + } + + let context: AccountContext + let content: Content + let cancel: () -> Void + + init( + context: AccountContext, + content: Content, + cancel: @escaping () -> Void + ) { + self.context = context + self.content = content + self.cancel = cancel + } + + static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: BlurredBackgroundView + private var content = ComponentView() + + private var component: SaveProgressScreenComponent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.5)) + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: SaveProgressScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let previousComponent = self.component + self.component = component + self.state = state + + var animateIn = false + var disappearingView: UIView? + if let previousComponent, previousComponent.content.type != component.content.type { + if let view = self.content.view { + disappearingView = view + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + } + + self.content = ComponentView() + animateIn = true + } + + let cornerRadius: CGFloat + let content: AnyComponent + switch component.content { + case let .progress(title, progress): + content = AnyComponent(ProgressComponent(title: title, value: progress, cancel: component.cancel)) + cornerRadius = 18.0 + case let .completion(text): + content = AnyComponent(BannerComponent(iconName: "anim_savemedia", text: text)) + cornerRadius = 9.0 + } + + let contentSize = self.content.update( + transition: transition, + component: content, + environment: {}, + containerSize: CGSize(width: 160.0, height: 160.0) + ) + let contentFrame = CGRect( + origin: .zero, + size: contentSize + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.backgroundView.addSubview(contentView) + if animateIn { + contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + contentView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25) + } + } + transition.setFrame(view: contentView, frame: contentFrame) + if let disappearingView { + transition.setPosition(view: disappearingView, position: contentFrame.center) + } + } + + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentFrame.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - contentFrame.size.height) / 2.0)), size: contentFrame.size) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) + +final class SaveProgressScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private weak var controller: SaveProgressScreen? + private let context: AccountContext + + fileprivate let componentHost: ComponentView + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + init(controller: SaveProgressScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + super.init() + + self.backgroundColor = .clear + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + private func animateIn() { + if let view = self.componentHost.view { + view.layer.animateScale(from: 0.4, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + + func animateOut(completion: @escaping () -> Void) { + if let view = self.componentHost.view { + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false) + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion() + }) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, transition: Transition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0 + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: topInset, + right: layout.safeInsets.right + ), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + SaveProgressScreenComponent( + context: self.context, + content: controller.content, + cancel: { [weak self] in + if let self, let controller = self.controller { + controller.cancel() + } + } + ) + ), + environment: { + environment + }, + forceUpdate: false, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.addSubview(componentView) + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + if isFirstTime { + self.animateIn() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + fileprivate let context: AccountContext + var content: SaveProgressScreenComponent.Content { + didSet { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut)) + } + self.maybeSetupDismissTimer() + } + } + + private var dismissTimer: SwiftSignalKit.Timer? + + public var cancelled: () -> Void = {} + + init(context: AccountContext, content: SaveProgressScreenComponent.Content) { + self.context = context + self.content = content + + super.init(navigationBarPresentationData: nil) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .Ignore + + self.maybeSetupDismissTimer() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + fileprivate func cancel() { + self.cancelled() + + self.node.animateOut(completion: { [weak self] in + if let self { + self.dismiss() + } + }) + } + + private func maybeSetupDismissTimer() { + if case .completion = self.content { + self.node.isUserInteractionEnabled = false + if self.dismissTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in + if let self { + self.node.animateOut(completion: { [weak self] in + if let self { + self.dismiss() + } + }) + } + }, queue: Queue.mainQueue()) + timer.start() + self.dismissTimer = timer + } + } + } + + private var validLayout: ContainerViewLayout? + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift new file mode 100644 index 0000000000..2c48d81991 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -0,0 +1,297 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AvatarNode +import AccountContext +import MessageInputPanelComponent +import BundleIconComponent + +private final class AvatarComponent: Component { + let context: AccountContext + let peer: EnginePeer + + init(context: AccountContext, peer: EnginePeer) { + self.context = context + self.peer = peer + } + + static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + + private var component: AvatarComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = CGSize(width: 36.0, height: 36.0) + + self.avatarNode.frame = CGRect(origin: CGPoint(), size: size) + self.avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true + ) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +final class StoryPreviewComponent: Component { + typealias EnvironmentType = Empty + + let context: AccountContext + let caption: String + + init( + context: AccountContext, + caption: String + ) { + self.context = context + self.caption = caption + } + + static func ==(lhs: StoryPreviewComponent, rhs: StoryPreviewComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.caption != rhs.caption { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var peerDisposable: Disposable? + fileprivate var accountPeer: EnginePeer? + + init(context: AccountContext) { + self.context = context + + super.init() + + self.peerDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let self { + self.accountPeer = peer + self.updated() + } + }) + } + + deinit { + self.peerDisposable?.dispose() + } + } + + func makeState() -> State { + return State( + context: self.context + ) + } + + public final class View: UIView { + private let line = ComponentView() + private let title = ComponentView() + private let avatar = ComponentView() + private let cancelButton = ComponentView() + private let inputPanel = ComponentView() + private let inputPanelExternalState = MessageInputPanelComponent.ExternalState() + + private let scrubber = ComponentView() + + private var component: StoryPreviewComponent? + private weak var state: State? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StoryPreviewComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let lineSize = self.line.update( + transition: transition, + component: AnyComponent(Rectangle(color: UIColor(white: 1.0, alpha: 0.5))), + environment: {}, + containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 2.0) + ) + let lineFrame = CGRect( + origin: CGPoint(x: 8.0, y: 8.0), + size: lineSize + ) + if let lineView = self.line.view { + if lineView.superview == nil { + lineView.layer.cornerRadius = 1.0 + self.addSubview(lineView) + } + transition.setPosition(view: lineView, position: lineFrame.center) + transition.setBounds(view: lineView, bounds: CGRect(origin: .zero, size: lineFrame.size)) + } + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Media Gallery/Close", + tintColor: UIColor.white + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: 17.0, y: 24.0), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.addSubview(cancelButtonView) + } + transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center) + transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size)) + } + + if let accountPeer = state.accountPeer { + let avatarSize = self.avatar.update( + transition: transition, + component: AnyComponent(AvatarComponent( + context: component.context, + peer: accountPeer + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let avatarFrame = CGRect( + origin: CGPoint(x: availableSize.width - avatarSize.width - 6.0, y: 14.0), + size: avatarSize + ) + if let avatarView = self.avatar.view { + if avatarView.superview == nil { + self.addSubview(avatarView) + } + transition.setPosition(view: avatarView, position: avatarFrame.center) + transition.setBounds(view: avatarView, bounds: CGRect(origin: .zero, size: avatarFrame.size)) + } + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(Text( + text: "My story", + font: Font.semibold(17.0), + color: .white + )), + environment: {}, + containerSize: CGSize(width: 180.0, height: 44.0) + ) + let titleFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 21.0), + size: titleSize + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleFrame.size)) + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let inputPanelSize = self.inputPanel.update( + transition: transition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, + context: component.context, + theme: presentationData.theme, + strings: presentationData.strings, + style: .story, + placeholder: "Reply Privately...", + presentController: { _ in + }, + sendMessageAction: { + }, + setMediaRecordingActive: { _, _, _ in }, + lockMediaRecording: nil, + stopAndPreviewMediaRecording: nil, + discardMediaRecordingPreview: nil, + attachmentAction: { }, + reactionAction: { _ in }, + timeoutAction: nil, + audioRecorder: nil, + videoRecordingStatus: nil, + isRecordingLocked: false, + recordedAudioPreview: nil, + wasRecordingDismissed: false, + timeoutValue: nil, + timeoutSelected: false, + displayGradient: false, + bottomInset: 0.0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 200.0) + ) + + let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelSize.height - 3.0), size: inputPanelSize) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.addSubview(inputPanelView) + } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + } + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 646e6008d9..60657da8a9 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -599,7 +599,7 @@ public final class MessageInputPanelComponent: Component { transition.setFrame(view: inputActionButtonView, frame: CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floorToScreenPixels((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize)) } - var fieldIconNextX = fieldBackgroundFrame.maxX - 2.0 + var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0 if case .story = component.style { let stickerButtonSize = self.stickerButton.update( transition: transition, @@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component { if timeoutButtonView.superview == nil { self.addSubview(timeoutButtonView) } - let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize) + let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 4.0 - timeoutButtonSize.height), size: timeoutButtonSize) transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center) transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size)) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift index 98d59b6194..3d3cb07b71 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContainerScreen.swift @@ -116,6 +116,7 @@ private final class StoryContainerScreenComponent: Component { private var environment: ViewControllerComponentContainer.Environment? private let backgroundLayer: SimpleLayer + private let backgroundEffectView: BlurredBackgroundView private var contentUpdatedDisposable: Disposable? @@ -132,6 +133,9 @@ private final class StoryContainerScreenComponent: Component { self.backgroundLayer.backgroundColor = UIColor.black.cgColor self.backgroundLayer.zPosition = -1000.0 + self.backgroundEffectView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.9), enableBlur: true) + self.backgroundEffectView.layer.zPosition = -1001.0 + super.init(frame: frame) self.layer.addSublayer(self.backgroundLayer) @@ -334,12 +338,12 @@ private final class StoryContainerScreenComponent: Component { if subview is ItemSetView { if self.itemSetPanState == nil { - if let result = subview.hitTest(point, with: event) { + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { return result } } } else { - if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + if let result = subview.hitTest(self.convert(self.convert(point, to: subview), to: subview), with: event) { return result } } @@ -351,6 +355,7 @@ private final class StoryContainerScreenComponent: Component { func animateIn() { if let transitionIn = self.component?.transitionIn, transitionIn.sourceView != nil { self.backgroundLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + self.backgroundEffectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.28, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) if let transitionIn = self.component?.transitionIn, let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id] { if let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View { @@ -372,6 +377,7 @@ private final class StoryContainerScreenComponent: Component { if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], let itemSetComponentView = itemSetView.view.view as? StoryItemSetContainerComponent.View, let transitionOut = component.transitionOut(slice.peer.id, slice.item.id) { let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(layer: self.backgroundLayer, alpha: 0.0) + transition.setAlpha(view: self.backgroundEffectView, alpha: 0.0) let transitionOutCompleted = transitionOut.completed itemSetComponentView.animateOut(transitionOut: transitionOut, completion: { @@ -450,6 +456,17 @@ private final class StoryContainerScreenComponent: Component { self.state = state transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize)) + transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + if case .regular = environment.metrics.widthClass { + self.backgroundLayer.isHidden = true + self.backgroundEffectView.update(size: availableSize, transition: transition.containedViewLayoutTransition) + self.insertSubview(self.backgroundEffectView, at: 0) + + } else { + self.backgroundLayer.isHidden = false + self.backgroundEffectView.removeFromSuperview() + } var isProgressPaused = false if self.itemSetPanState != nil { @@ -501,7 +518,19 @@ private final class StoryContainerScreenComponent: Component { let slice = currentSlices[i] + let cubeAdditionalRotationFraction: CGFloat + if i == focusedIndex { + cubeAdditionalRotationFraction = 0.0 + } else if i < focusedIndex { + cubeAdditionalRotationFraction = -1.0 + } else { + cubeAdditionalRotationFraction = 1.0 + } + + var panFraction: CGFloat = 0.0 if let itemSetPanState = self.itemSetPanState { + panFraction = -itemSetPanState.fraction + if self.visibleItemSetViews[slice.peer.id] != nil { isItemVisible = true } @@ -526,6 +555,18 @@ private final class StoryContainerScreenComponent: Component { self.visibleItemSetViews[slice.peer.id] = itemSetView } + var itemSetContainerSize = availableSize + var itemSetContainerTopInset = environment.statusBarHeight + 12.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) + let mediaHeight = availableHeight - 40.0 + let mediaWidth = floor(mediaHeight * 0.5625) + itemSetContainerSize = CGSize(width: mediaWidth, height: availableHeight) + itemSetContainerTopInset = 0.0 + itemSetContainerSafeInsets.bottom = 0.0 + } + let _ = itemSetView.view.update( transition: itemSetTransition, component: AnyComponent(StoryItemSetContainerComponent( @@ -534,11 +575,13 @@ private final class StoryContainerScreenComponent: Component { slice: slice, theme: environment.theme, strings: environment.strings, - containerInsets: UIEdgeInsets(top: environment.statusBarHeight + 12.0, left: 0.0, bottom: environment.inputHeight, right: 0.0), - safeInsets: environment.safeInsets, + containerInsets: UIEdgeInsets(top: itemSetContainerTopInset, left: 0.0, bottom: environment.inputHeight, right: 0.0), + safeInsets: itemSetContainerSafeInsets, inputHeight: environment.inputHeight, + metrics: environment.metrics, isProgressPaused: isProgressPaused || i != focusedIndex, hideUI: i == focusedIndex && self.itemSetPanState?.didBegin == false, + visibilityFraction: 1.0 - abs(panFraction + cubeAdditionalRotationFraction), presentController: { [weak self] c in guard let self, let environment = self.environment else { return @@ -614,14 +657,14 @@ private final class StoryContainerScreenComponent: Component { } )), environment: {}, - containerSize: availableSize + containerSize: itemSetContainerSize ) if i == focusedIndex { contentDerivedBottomInset = itemSetView.externalState.derivedBottomInset } - let itemFrame = CGRect(origin: CGPoint(), size: availableSize) + let itemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - itemSetContainerSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - itemSetContainerSize.height) / 2.0)), size: itemSetContainerSize) if let itemSetComponentView = itemSetView.view.view { if itemSetView.superview == nil { self.addSubview(itemSetView) @@ -639,13 +682,25 @@ private final class StoryContainerScreenComponent: Component { itemSetTransition.setPosition(view: itemSetComponentView, position: CGRect(origin: CGPoint(), size: itemFrame.size).center) itemSetTransition.setBounds(view: itemSetComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) - itemSetTransition.setPosition(layer: itemSetView.tintLayer, position: CGRect(origin: CGPoint(), size: itemFrame.size).center) - itemSetTransition.setBounds(layer: itemSetView.tintLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + let itemTintSize: CGSize + if case .regular = environment.metrics.widthClass { + itemTintSize = itemSetView.externalState.derivedMediaSize + } else { + itemTintSize = itemFrame.size + } + + itemSetTransition.setPosition(layer: itemSetView.tintLayer, position: CGRect(origin: CGPoint(), size: itemTintSize).center) + itemSetTransition.setBounds(layer: itemSetView.tintLayer, bounds: CGRect(origin: CGPoint(), size: itemTintSize)) let perspectiveConstant: CGFloat = 500.0 let width = itemFrame.width - let sideDistance: CGFloat = 40.0 + let sideDistance: CGFloat + if case .regular = environment.metrics.widthClass { + sideDistance = 0.0 + } else { + sideDistance = 40.0 + } let sideAngle_d: CGFloat = -pow(perspectiveConstant, 2)*pow(sideDistance, 2) let sideAngle_e: CGFloat = pow(perspectiveConstant, 2)*pow(width, 2) @@ -686,21 +741,7 @@ private final class StoryContainerScreenComponent: Component { return targetTransform } - - let cubeAdditionalRotationFraction: CGFloat - if i == focusedIndex { - cubeAdditionalRotationFraction = 0.0 - } else if i < focusedIndex { - cubeAdditionalRotationFraction = -1.0 - } else { - cubeAdditionalRotationFraction = 1.0 - } - - var panFraction: CGFloat = 0.0 - if let itemSetPanState = self.itemSetPanState { - panFraction = -itemSetPanState.fraction - } - + Transition.immediate.setTransform(view: itemSetComponentView, transform: faceTransform) Transition.immediate.setTransform(layer: itemSetView.tintLayer, transform: faceTransform) @@ -741,7 +782,7 @@ private final class StoryContainerScreenComponent: Component { alphaFraction *= 1.3 alphaFraction = max(-1.0, min(1.0, alphaFraction)) alphaFraction = abs(alphaFraction) - + itemSetTransition.setAlpha(layer: itemSetView.tintLayer, alpha: alphaFraction) } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index be58d0ed58..53b2046a22 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -21,6 +21,7 @@ import AvatarNode public final class StoryItemSetContainerComponent: Component { public final class ExternalState { public fileprivate(set) var derivedBottomInset: CGFloat = 0.0 + public fileprivate(set) var derivedMediaSize: CGSize = .zero public init() { } @@ -39,8 +40,10 @@ public final class StoryItemSetContainerComponent: Component { public let containerInsets: UIEdgeInsets public let safeInsets: UIEdgeInsets public let inputHeight: CGFloat + public let metrics: LayoutMetrics public let isProgressPaused: Bool public let hideUI: Bool + public let visibilityFraction: CGFloat public let presentController: (ViewController) -> Void public let close: () -> Void public let navigate: (NavigationDirection) -> Void @@ -56,8 +59,10 @@ public final class StoryItemSetContainerComponent: Component { containerInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, inputHeight: CGFloat, + metrics: LayoutMetrics, isProgressPaused: Bool, hideUI: Bool, + visibilityFraction: CGFloat, presentController: @escaping (ViewController) -> Void, close: @escaping () -> Void, navigate: @escaping (NavigationDirection) -> Void, @@ -72,8 +77,10 @@ public final class StoryItemSetContainerComponent: Component { self.containerInsets = containerInsets self.safeInsets = safeInsets self.inputHeight = inputHeight + self.metrics = metrics self.isProgressPaused = isProgressPaused self.hideUI = hideUI + self.visibilityFraction = visibilityFraction self.presentController = presentController self.close = close self.navigate = navigate @@ -103,12 +110,18 @@ public final class StoryItemSetContainerComponent: Component { if lhs.inputHeight != rhs.inputHeight { return false } + if lhs.metrics != rhs.metrics { + return false + } if lhs.isProgressPaused != rhs.isProgressPaused { return false } if lhs.hideUI != rhs.hideUI { return false } + if lhs.visibilityFraction != rhs.visibilityFraction { + return false + } return true } @@ -1283,12 +1296,16 @@ public final class StoryItemSetContainerComponent: Component { self.itemLayout = itemLayout let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - inputPanelSize.height), size: inputPanelSize) + var inputPanelAlpha: CGFloat = focusedItem?.isMy == true ? 0.0 : 1.0 + if case .regular = component.metrics.widthClass { + inputPanelAlpha *= component.visibilityFraction + } if let inputPanelView = self.inputPanel.view { if inputPanelView.superview == nil { self.addSubview(inputPanelView) } transition.setFrame(view: inputPanelView, frame: inputPanelFrame) - transition.setAlpha(view: inputPanelView, alpha: focusedItem?.isMy == true ? 0.0 : 1.0) + transition.setAlpha(view: inputPanelView, alpha: inputPanelAlpha) } if let captionItem = self.captionItem, captionItem.itemId != component.slice.item.storyItem.id { @@ -1543,6 +1560,10 @@ public final class StoryItemSetContainerComponent: Component { } var footerPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelBottomInset - footerPanelSize.height), size: footerPanelSize) + var footerPanelAlpha: CGFloat = (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0 + if case .regular = component.metrics.widthClass { + footerPanelAlpha *= component.visibilityFraction + } if self.displayViewList { footerPanelFrame.origin.y += footerPanelSize.height } @@ -1551,7 +1572,7 @@ public final class StoryItemSetContainerComponent: Component { self.addSubview(footerPanelView) } transition.setFrame(view: footerPanelView, frame: footerPanelFrame) - transition.setAlpha(view: footerPanelView, alpha: (focusedItem?.isMy == true && !self.displayViewList) ? 1.0 : 0.0) + transition.setAlpha(view: footerPanelView, alpha: footerPanelAlpha) } let bottomGradientHeight = inputPanelSize.height + 32.0 @@ -1650,6 +1671,7 @@ public final class StoryItemSetContainerComponent: Component { } } + component.externalState.derivedMediaSize = contentFrame.size component.externalState.derivedBottomInset = availableSize.height - min(inputPanelFrame.minY, contentFrame.maxY) return contentSize diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 67c85f87e9..ca68e59709 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -550,7 +550,7 @@ final class StoryItemSetContainerSendMessage { guard let attachmentButtonView = inputPanelView.getAttachmentButtonView() else { return nil } - return attachmentButtonView.convert(attachmentButtonView.bounds, to: view) + return attachmentButtonView.convert(attachmentButtonView.bounds, to: nil) } attachmentController.requestController = { [weak self, weak view, weak attachmentController] type, completion in guard let self, let view, let component = view.component else { diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 5c1ece571b..7fa04446d6 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -23,6 +23,7 @@ public final class StoryPeerListComponent: Component { public let strings: PresentationStrings public let storySubscriptions: EngineStorySubscriptions? public let collapseFraction: CGFloat + public let uploadProgress: Float? public let peerAction: (EnginePeer?) -> Void public let contextPeerAction: (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void @@ -33,6 +34,7 @@ public final class StoryPeerListComponent: Component { strings: PresentationStrings, storySubscriptions: EngineStorySubscriptions?, collapseFraction: CGFloat, + uploadProgress: Float?, peerAction: @escaping (EnginePeer?) -> Void, contextPeerAction: @escaping (ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void ) { @@ -42,6 +44,7 @@ public final class StoryPeerListComponent: Component { self.strings = strings self.storySubscriptions = storySubscriptions self.collapseFraction = collapseFraction + self.uploadProgress = uploadProgress self.peerAction = peerAction self.contextPeerAction = contextPeerAction } @@ -62,6 +65,9 @@ public final class StoryPeerListComponent: Component { if lhs.collapseFraction != rhs.collapseFraction { return false } + if lhs.uploadProgress != rhs.uploadProgress { + return false + } return true } @@ -258,16 +264,14 @@ public final class StoryPeerListComponent: Component { hasUnseen = itemSet.hasUnseen var hasItems = true - var itemProgress: CGFloat? + var itemProgress: Float? if peer.id == component.context.account.peerId { - itemProgress = nil if let storySubscriptions = component.storySubscriptions, let accountItem = storySubscriptions.accountItem { hasItems = accountItem.storyCount != 0 } else { hasItems = false } - //itemProgress = component.state?.uploadProgress - //itemProgress = 0.0 + itemProgress = component.uploadProgress } let collapsedItemX: CGFloat diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 42805d74a9..cf5b240f28 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -78,6 +78,7 @@ private final class StoryProgressLayer: SimpleShapeLayer { private struct Params: Equatable { var size: CGSize var lineWidth: CGFloat + var progress: Float } private var currentParams: Params? @@ -102,10 +103,16 @@ private final class StoryProgressLayer: SimpleShapeLayer { fatalError("init(coder:) has not been implemented") } - func update(size: CGSize, lineWidth: CGFloat) { + func reset() { + self.currentParams = nil + self.path = nil + } + + func update(size: CGSize, lineWidth: CGFloat, progress: Float, transition: Transition) { let params = Params( size: size, - lineWidth: lineWidth + lineWidth: lineWidth, + progress: progress ) if self.currentParams == params { return @@ -114,10 +121,13 @@ private final class StoryProgressLayer: SimpleShapeLayer { let lineWidth: CGFloat = 2.0 - let path = CGMutablePath() - path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: size.width * 0.5 - lineWidth * 0.5, startAngle: 0.0, endAngle: CGFloat.pi * 0.25, clockwise: false) + if self.path == nil { + let path = CGMutablePath() + path.addEllipse(in: CGRect(origin: CGPoint(x: lineWidth * 0.5, y: lineWidth * 0.5), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth))) + self.path = path + } - self.path = path + transition.setShapeLayerStrokeEnd(layer: self, strokeEnd: CGFloat(progress)) if self.animation(forKey: "rotation") == nil { let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") @@ -140,7 +150,7 @@ public final class StoryPeerListItemComponent: Component { public let peer: EnginePeer public let hasUnseen: Bool public let hasItems: Bool - public let progress: CGFloat? + public let progress: Float? public let collapseFraction: CGFloat public let collapsedScaleFactor: CGFloat public let collapsedWidth: CGFloat @@ -156,7 +166,7 @@ public final class StoryPeerListItemComponent: Component { peer: EnginePeer, hasUnseen: Bool, hasItems: Bool, - progress: CGFloat?, + progress: Float?, collapseFraction: CGFloat, collapsedScaleFactor: CGFloat, collapsedWidth: CGFloat, @@ -371,7 +381,7 @@ public final class StoryPeerListItemComponent: Component { let hadProgress = self.component?.progress != nil let themeUpdated = self.component?.theme !== component.theme - self.containerNode.isGestureEnabled = component.peer.id != component.context.account.peerId + let previousComponent = self.component self.component = component self.componentState = state @@ -546,6 +556,19 @@ public final class StoryPeerListItemComponent: Component { } else { titleString = component.peer.compactDisplayTitle } + + var titleTransition = transition + if previousComponent?.progress != nil && component.progress == nil { + if let titleView = self.title.view, let snapshotView = titleView.snapshotContentTree() { + titleView.superview?.addSubview(snapshotView) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + titleView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + titleTransition = .immediate + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(Text(text: titleString, font: Font.regular(11.0), color: component.theme.list.itemPrimaryTextColor)), @@ -559,13 +582,13 @@ public final class StoryPeerListItemComponent: Component { titleView.isUserInteractionEnabled = false self.button.addSubview(titleView) } - transition.setPosition(view: titleView, position: titleFrame.center) + titleTransition.setPosition(view: titleView, position: titleFrame.center) titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) transition.setScale(view: titleView, scale: effectiveScale) transition.setAlpha(view: titleView, alpha: 1.0 - component.collapseFraction) } - if component.progress != nil { + if let progress = component.progress { var progressTransition = transition let progressLayer: StoryProgressLayer if let current = self.progressLayer { @@ -578,7 +601,7 @@ public final class StoryPeerListItemComponent: Component { } let progressFrame = CGRect(origin: CGPoint(), size: indicatorFrame.size) progressTransition.setFrame(layer: progressLayer, frame: progressFrame) - progressLayer.update(size: progressFrame.size, lineWidth: 4.0) + progressLayer.update(size: progressFrame.size, lineWidth: 4.0, progress: progress, transition: transition) self.indicatorShapeLayer.opacity = 0.0 } else { @@ -587,9 +610,11 @@ public final class StoryPeerListItemComponent: Component { if let progressLayer = self.progressLayer { self.progressLayer = nil if transition.animation.isImmediate { + progressLayer.reset() progressLayer.removeFromSuperlayer() } else { progressLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak progressLayer] _ in + progressLayer?.reset() progressLayer?.removeFromSuperlayer() }) } diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/Contents.json new file mode 100644 index 0000000000..c6921542a5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_storyreply.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/ic_storyreply.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/ic_storyreply.pdf new file mode 100644 index 0000000000..e80a840c24 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/StoryReplyIcon.imageset/ic_storyreply.pdf @@ -0,0 +1,139 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.250000 1.250000 cm +0.000000 0.000000 0.000000 scn +8.750000 16.000000 m +8.687436 16.000000 8.625057 15.999207 8.562874 15.997632 c +6.709939 15.950687 5.029772 15.208538 3.773461 14.022270 c +3.680609 13.934595 3.590072 13.844494 3.501952 13.752068 c +2.310160 12.502043 1.560415 10.826810 1.503491 8.977144 c +1.501169 8.901714 1.500000 8.825994 1.500000 8.750000 c +1.500000 8.687436 1.500792 8.625057 1.502368 8.562874 c +1.547463 6.782922 2.234077 5.162397 3.339638 3.923870 c +3.508653 3.734527 3.687460 3.554111 3.875264 3.383418 c +5.109709 2.261445 6.732838 1.559564 8.518808 1.503616 c +8.595577 1.501211 8.672647 1.500000 8.750000 1.500000 c +9.071613 1.500000 9.387973 1.520895 9.697865 1.561318 c +10.108599 1.614895 10.484997 1.325361 10.538574 0.914627 c +10.592151 0.503893 10.302618 0.127495 9.891884 0.073919 c +9.517827 0.025126 9.136661 0.000000 8.750000 0.000000 c +8.636739 0.000000 8.523980 0.002151 8.411762 0.006416 c +6.298033 0.086756 4.376344 0.916882 2.905031 2.238455 c +2.683921 2.437062 2.472982 2.646769 2.273091 2.866697 c +0.860781 4.420585 0.000000 6.484770 0.000000 8.750000 c +0.000000 11.081320 0.911740 13.199690 2.398106 14.767997 c +2.500393 14.875923 2.605402 14.981244 2.713021 15.083851 c +4.283196 16.580877 6.409262 17.500000 8.750000 17.500000 c +9.136661 17.500000 9.517827 17.474874 9.891884 17.426081 c +10.302618 17.372505 10.592151 16.996107 10.538574 16.585373 c +10.484998 16.174639 10.108600 15.885105 9.697865 15.938683 c +9.387973 15.979105 9.071614 16.000000 8.750000 16.000000 c +h +13.025583 15.554191 m +13.277974 15.882628 13.748830 15.944277 14.077268 15.691885 c +14.683157 15.226283 15.226282 14.683159 15.691885 14.077269 c +15.944276 13.748831 15.882628 13.277975 15.554191 13.025583 c +15.225752 12.773192 14.754897 12.834841 14.502505 13.163279 c +14.116343 13.665794 13.665793 14.116343 13.163278 14.502506 c +12.834840 14.754898 12.773191 15.225753 13.025583 15.554191 c +h +16.585373 10.538574 m +16.996107 10.592151 17.372505 10.302618 17.426081 9.891884 c +17.474874 9.517827 17.500000 9.136661 17.500000 8.750000 c +17.500000 8.363339 17.474874 7.982173 17.426081 7.608116 c +17.372505 7.197382 16.996107 6.907849 16.585373 6.961426 c +16.174639 7.015002 15.885105 7.391400 15.938682 7.802135 c +15.979105 8.112027 16.000000 8.428386 16.000000 8.750000 c +16.000000 9.071613 15.979105 9.387972 15.938682 9.697865 c +15.885105 10.108599 16.174639 10.484997 16.585373 10.538574 c +h +15.554191 4.474417 m +15.882628 4.222026 15.944277 3.751170 15.691885 3.422732 c +15.226283 2.816843 14.683158 2.273718 14.077269 1.808115 c +13.748831 1.555724 13.277975 1.617372 13.025584 1.945809 c +12.773192 2.274248 12.834840 2.745103 13.163279 2.997495 c +13.665794 3.383657 14.116343 3.834207 14.502506 4.336722 c +14.754898 4.665160 15.225753 4.726809 15.554191 4.474417 c +h +8.273390 11.921345 m +8.273390 11.050000 l +11.703422 11.050000 12.866389 8.579929 13.256549 6.733100 c +13.256555 6.733077 l +13.383529 6.132035 13.447016 5.831513 13.389845 5.732118 c +13.335299 5.637283 13.261444 5.590770 13.152351 5.582543 c +13.038011 5.573922 12.743963 5.799743 12.155870 6.251384 c +11.347326 6.872325 10.103362 7.450000 8.273390 7.450000 c +8.273390 6.578653 l +8.273390 6.000751 8.273390 5.711799 8.157403 5.572701 c +8.056680 5.451907 7.904991 5.385546 7.747917 5.393559 c +7.567042 5.402785 7.354835 5.598900 6.930421 5.991130 c +4.039881 8.662477 l +3.819108 8.866508 3.708722 8.968524 3.667804 9.088018 c +3.631850 9.193013 3.631850 9.306987 3.667804 9.411982 c +3.708722 9.531476 3.819108 9.633492 4.039880 9.837523 c +6.930419 12.508869 l +7.354834 12.901100 7.567042 13.097216 7.747917 13.106441 c +7.904991 13.114454 8.056680 13.048094 8.157403 12.927299 c +8.273390 12.788202 8.273390 12.499249 8.273390 11.921345 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3799 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 20.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003889 00000 n +0000003912 00000 n +0000004085 00000 n +0000004159 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4218 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift index dcff009715..a8b5699f2e 100644 --- a/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift +++ b/submodules/TelegramUI/Sources/FetchVideoMediaResource.swift @@ -8,6 +8,7 @@ import FFMpegBinding import LocalMediaResources import LegacyMediaPickerUI import MediaEditor +import Photos private final class AVURLAssetCopyItem: MediaResourceDataFetchCopyLocalItem { private let url: URL @@ -260,7 +261,7 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr if let mediaEditorValues { let configuration = recommendedVideoExportConfiguration(values: mediaEditorValues, frameRate: 30.0) let videoExport = MediaEditorVideoExport(account: account, subject: .video(avAsset), configuration: configuration, outputPath: tempFile.path) - videoExport.startExport() + videoExport.start() let statusDisposable = videoExport.status.start(next: { status in switch status { @@ -268,10 +269,22 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr var value = stat() if stat(tempFile.path, &value) == 0 { let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4") - if let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) { + if !"".isEmpty, let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) { TempBox.shared.dispose(tempFile) subscriber.putNext(.moveTempFile(file: remuxedTempFile)) } else { + let tempVideoPath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).mp4" + if let _ = try? FileManager.default.copyItem(atPath: tempFile.path, toPath: tempVideoPath) { + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: tempVideoPath)) + }, completionHandler: { _, error in + if let error = error { + print("\(error)") + } + let _ = try? FileManager.default.removeItem(atPath: tempVideoPath) + }) + } + TempBox.shared.dispose(remuxedTempFile) if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: [.mappedRead]) { var range: Range? @@ -293,6 +306,8 @@ public func fetchVideoLibraryMediaResource(account: Account, resource: VideoLibr EngineTempBox.shared.dispose(tempFile) case .failed: subscriber.putError(.generic) + case let .progress(progress): + subscriber.putNext(.progressUpdated(progress)) default: break } @@ -414,7 +429,7 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo } let videoExport = MediaEditorVideoExport(account: account, subject: subject, configuration: configuration, outputPath: tempFile.path) - videoExport.startExport() + videoExport.start() let statusDisposable = videoExport.status.start(next: { status in switch status { @@ -422,7 +437,7 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo var value = stat() if stat(tempFile.path, &value) == 0 { let remuxedTempFile = TempBox.shared.tempFile(fileName: "video.mp4") - if let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) { + if !"".isEmpty, let size = fileSize(tempFile.path), size <= 32 * 1024 * 1024, FFMpegRemuxer.remux(tempFile.path, to: remuxedTempFile.path) { TempBox.shared.dispose(tempFile) subscriber.putNext(.moveTempFile(file: remuxedTempFile)) } else { @@ -447,6 +462,8 @@ func fetchLocalFileVideoMediaResource(account: Account, resource: LocalFileVideo EngineTempBox.shared.dispose(tempFile) case .failed: subscriber.putError(.generic) + case let .progress(progress): + subscriber.putNext(.progressUpdated(progress)) default: break } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 4118be02ab..0f37a3ac53 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -292,8 +292,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil case let .image(image): return .image(image, PixelDimensions(image.size)) - case let .video(path, dimensions): - return .video(path, dimensions) + case let .video(path, transitionImage, dimensions): + return .video(path, transitionImage, dimensions) case let .asset(asset): return .asset(asset) case let .draft(draft): @@ -334,24 +334,39 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } else { return nil } - }, completion: { [weak self] mediaResult, commit, privacy in + }, completion: { [weak self] mediaResult, privacy, commit in guard let self else { dismissCameraImpl?() - commit() + commit({}) return } if let chatListController = self.chatListController as? ChatListControllerImpl { - chatListController.scrollToTop?() + chatListController.scrollToStories() switch mediaResult { case let .image(image, dimensions, caption): if let imageData = compressImageToJPEG(image, quality: 0.6) { switch privacy { - case let .story(storyPrivacy, _): - let _ = self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() + case let .story(storyPrivacy, pin): + chatListController.updateStoryUploadProgress(0.0) + let _ = (self.context.engine.messages.uploadStory(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy) + |> deliverOnMainQueue).start(next: { [weak chatListController] result in + if let chatListController { + switch result { + case let .progress(progress): + chatListController.updateStoryUploadProgress(progress) + case .completed: + Queue.mainQueue().after(0.2) { + chatListController.updateStoryUploadProgress(nil) + } + } + } }) + Queue.mainQueue().justDispatch { + commit({ [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } case let .message(peerIds, timeout): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) @@ -400,6 +415,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon account: self.context.account, peerIds: peerIds, threadIds: [:], messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start() + + commit({}) } } case let .video(content, _, values, duration, dimensions, caption): @@ -418,22 +435,34 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .asset(localIdentifier): resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } - if case let .story(storyPrivacy, _) = privacy { - let _ = self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy).start() - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() + if case let .story(storyPrivacy, pin) = privacy { + chatListController.updateStoryUploadProgress(0.0) + let _ = (self.context.engine.messages.uploadStory(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], pin: pin, privacy: storyPrivacy) + |> deliverOnMainQueue).start(next: { [weak chatListController] result in + if let chatListController { + switch result { + case let .progress(progress): + chatListController.updateStoryUploadProgress(progress) + case .completed: + Queue.mainQueue().after(0.2) { + chatListController.updateStoryUploadProgress(nil) + } + } + } }) + Queue.mainQueue().justDispatch { + commit({ [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } } else { - + commit({}) } } } } dismissCameraImpl?() - Queue.mainQueue().after(0.1) { - commit() - } } ) controller.cancelled = { showDraftTooltip in diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 4ddd04010e..49b5f680c6 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -143,7 +143,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var validLayout: ContainerViewLayout? - init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) { + init(account: Account, sharedContext: SharedAccountContext, text: String, textEntities: [MessageTextEntity], style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, customContentNode: TooltipCustomContentNode? = nil, location: TooltipScreen.Location, displayDuration: TooltipScreen.DisplayDuration, inset: CGFloat = 13.0, cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, requestDismiss: @escaping () -> Void, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)?) { self.tooltipStyle = style self.icon = icon self.customContentNode = customContentNode @@ -286,7 +286,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { if case let .point(_, arrowPosition) = location, case .right = arrowPosition { self.backgroundClipNode.cornerRadius = 8.5 } else { - self.backgroundClipNode.cornerRadius = 12.5 + self.backgroundClipNode.cornerRadius = cornerRadius ?? 12.5 } if #available(iOS 13.0, *) { self.backgroundClipNode.layer.cornerCurve = .continuous @@ -736,6 +736,7 @@ public final class TooltipScreen: ViewController { } private let displayDuration: DisplayDuration private let inset: CGFloat + private let cornerRadius: CGFloat? private let shouldDismissOnTouch: (CGPoint) -> TooltipScreen.DismissOnTouch private let openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? @@ -764,6 +765,7 @@ public final class TooltipScreen: ViewController { location: TooltipScreen.Location, displayDuration: DisplayDuration = .default, inset: CGFloat = 13.0, + cornerRadius: CGFloat? = nil, shouldDismissOnTouch: @escaping (CGPoint) -> TooltipScreen.DismissOnTouch, openActiveTextItem: ((TooltipActiveTextItem, TooltipActiveTextAction) -> Void)? = nil ) { @@ -777,6 +779,7 @@ public final class TooltipScreen: ViewController { self.location = location self.displayDuration = displayDuration self.inset = inset + self.cornerRadius = cornerRadius self.shouldDismissOnTouch = shouldDismissOnTouch self.openActiveTextItem = openActiveTextItem @@ -836,7 +839,7 @@ public final class TooltipScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(account: self.account, sharedContext: self.sharedContext, text: self.text, textEntities: self.textEntities, style: self.style, icon: self.icon, customContentNode: self.customContentNode, location: self.location, displayDuration: self.displayDuration, inset: self.inset, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(account: self.account, sharedContext: self.sharedContext, text: self.text, textEntities: self.textEntities, style: self.style, icon: self.icon, customContentNode: self.customContentNode, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return }