diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 7657ce16de..9877cd2e03 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2533,6 +2533,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + public func transitionViewForOwnStoryItem() -> UIView? { + if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + return transitionView + } + } + return nil + } + public func animateStoryUploadRipple() { if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 4c77d18ada..b4635e29f3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -602,6 +602,9 @@ public extension MediaEditorValues { } var requiresComposing: Bool { + if self.originalDimensions.width > self.originalDimensions.height { + return true + } if abs(1.0 - self.cropScale) > 0.0 { return true } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 0dd882bfbc..8b7c7b0c22 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -519,21 +519,7 @@ public final class MediaEditorVideoExport { return false } } -// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds -// if self.videoOutput === output { -// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) } -// } -// if self.audioOutput === output { -// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: progress) } -// } - } else { -// if self.videoOutput === output { -// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: 1) } -// } -// if self.audioOutput === output { -// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: 1) } -// } writer.markVideoAsFinished() return false } @@ -553,24 +539,11 @@ public final class MediaEditorVideoExport { } self.pauseDispatchGroup.wait() if let buffer = output.copyNextSampleBuffer() { -// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds -// if self.videoOutput === output { -// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) } -// } -// if self.audioOutput === output { -// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: progress) } -// } - if !writer.appendVideoBuffer(buffer) { + if !writer.appendAudioBuffer(buffer) { writer.markAudioAsFinished() return false } } else { -// if self.videoOutput === output { -// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: 1) } -// } -// if self.audioOutput === output { -// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: 1) } -// } writer.markAudioAsFinished() return false } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 72af232319..3d0b72ce37 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -385,11 +385,20 @@ final class MediaEditorScreenComponent: Component { image: state.image(.done), size: CGSize(width: 33.0, height: 33.0) )), - action: { - guard let controller = environment.controller() as? MediaEditorScreen else { + action: { [weak self] in + guard let self, let controller = environment.controller() as? MediaEditorScreen else { return } - controller.requestCompletion(animated: true) + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + var inputText = NSAttributedString(string: "") + switch inputPanelView.getSendMessageInput() { + case let .text(text): + inputText = NSAttributedString(string: text) + } + + controller.requestCompletion(caption: inputText, animated: true) } )), environment: {}, @@ -687,6 +696,38 @@ final class MediaEditorScreenComponent: Component { private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) public final class MediaEditorScreen: ViewController { + public final class TransitionIn { + public weak var sourceView: UIView? + public let sourceRect: CGRect + public let sourceCornerRadius: CGFloat + + public init( + sourceView: UIView, + sourceRect: CGRect, + sourceCornerRadius: CGFloat + ) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.sourceCornerRadius = sourceCornerRadius + } + } + + public final class TransitionOut { + public weak var destinationView: UIView? + public let destinationRect: CGRect + public let destinationCornerRadius: CGFloat + + public init( + destinationView: UIView, + destinationRect: CGRect, + destinationCornerRadius: CGFloat + ) { + self.destinationView = destinationView + self.destinationRect = destinationRect + self.destinationCornerRadius = destinationCornerRadius + } + } + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext @@ -935,11 +976,44 @@ public final class MediaEditorScreen: ViewController { } } - func animateOut(completion: @escaping () -> Void) { + func animateOut(finished: Bool, completion: @escaping () -> Void) { guard let controller = self.controller else { return } - if let sourceHint = controller.sourceHint { + if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { + let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) + + let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width + self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + completion() + }) + self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.frame.height - self.previewContainerView.frame.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.previewContainerView.layer.animate( + from: self.previewContainerView.layer.cornerRadius as NSNumber, + to: self.previewContainerView.bounds.width / 2.0 as NSNumber, + keyPath: "cornerRadius", + timingFunction: kCAMediaTimingFunctionSpring, + duration: 0.4, + removeOnCompletion: false + ) + + if let componentView = self.componentHost.view { + componentView.clipsToBounds = true + componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.frame.height - componentView.frame.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animate( + from: componentView.layer.cornerRadius as NSNumber, + to: componentView.bounds.width / 2.0 as NSNumber, + keyPath: "cornerRadius", + timingFunction: kCAMediaTimingFunctionSpring, + duration: 0.4, + removeOnCompletion: false + ) + } + } else if let sourceHint = controller.sourceHint { switch sourceHint { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -1175,6 +1249,8 @@ public final class MediaEditorScreen: ViewController { fileprivate let context: AccountContext fileprivate let subject: Signal + fileprivate let transitionIn: TransitionIn? + fileprivate let transitionOut: (Bool) -> TransitionOut? public enum SourceHint { case camera @@ -1184,9 +1260,17 @@ public final class MediaEditorScreen: ViewController { public var cancelled: () -> Void = {} public var completion: (MediaEditorScreen.Result, @escaping () -> Void) -> Void = { _, _ in } - public init(context: AccountContext, subject: Signal, completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void) -> Void) { + public init( + context: AccountContext, + subject: Signal, + transitionIn: TransitionIn?, + transitionOut: @escaping (Bool) -> TransitionOut?, + completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void) -> Void + ) { self.context = context self.subject = subject + self.transitionIn = transitionIn + self.transitionOut = transitionOut self.completion = completion super.init(navigationBarPresentationData: nil) @@ -1210,12 +1294,12 @@ public final class MediaEditorScreen: ViewController { func requestDismiss(animated: Bool) { self.cancelled() - self.node.animateOut(completion: { [weak self] in + self.node.animateOut(finished: false, completion: { [weak self] in self?.dismiss() }) } - func requestCompletion(animated: Bool) { + func requestCompletion(caption: NSAttributedString, animated: Bool) { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { return } @@ -1250,8 +1334,8 @@ public final class MediaEditorScreen: ViewController { duration = 5.0 } } - self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 1080, height: 1920), caption: nil), { [weak self] in - self?.node.animateOut(completion: { [weak self] in + self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 1080, height: 1920), caption: caption), { [weak self] in + self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) }) @@ -1259,8 +1343,8 @@ 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: nil), { [weak self] in - self?.node.animateOut(completion: { [weak self] in + self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), { [weak self] in + self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) }) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index feca1eb3cd..09996c5c3e 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -210,7 +210,13 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } if finished { - + if let chatListController = self.chatListController as? ChatListControllerImpl, let transitionView = chatListController.transitionViewForOwnStoryItem() { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height / 2.0 + ) + } } else { if let cameraItemView = self.rootTabController?.viewForCameraItem() { return StoryCameraTransitionOut( @@ -221,18 +227,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } return nil -// if finished { -// return nil -// } else { -// if let self, let cameraItemView = self.rootTabController?.viewForCameraItem() { -// return StoryCameraTransitionOut( -// destinationView: cameraItemView, -// destinationRect: cameraItemView.bounds, -// destinationCornerRadius: cameraItemView.bound.height / 2.0 -// ) -// } -// } -// return nil } ) } @@ -344,114 +338,68 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return .asset(asset) } } - let controller = MediaEditorScreen(context: context, subject: subject, completion: { mediaResult, commit in - enum AdditionalCategoryId: Int { - case everyone - case contacts - case closeFriends - } - - let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }) - - let additionalCategories: [ChatListNodeAdditionalCategory] = [ - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.everyone.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: nil, color: .blue), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), - title: "Everyone", - appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS") - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.contacts.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 1.0 * 0.8, cornerRadius: nil, color: .yellow), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.8, cornerRadius: 6.0, circleCorners: true, color: .yellow), - title: presentationData.strings.ChatListFolder_CategoryContacts, - appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS") - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.closeFriends.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 1.0 * 0.6, cornerRadius: nil, color: .green), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 0.6, cornerRadius: 6.0, circleCorners: true, color: .green), - title: "Close Friends", - appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS") + let controller = MediaEditorScreen(context: context, subject: subject, transitionIn: nil, transitionOut: { finished in + if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView { + return MediaEditorScreen.TransitionOut( + destinationView: destinationView, + destinationRect: transitionOut.destinationRect, + destinationCornerRadius: transitionOut.destinationCornerRadius ) - ] - - let selectionController = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( - title: "Share Story", - searchPlaceholder: "Search contacts", - selectedChats: Set(), - additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: Set([AdditionalCategoryId.everyone.rawValue])), - chatListFilters: nil, - displayPresence: true - )), options: [], filters: [.excludeSelf], alwaysEnabled: true, limit: 1000, reachedLimit: { _ in - })) - selectionController.navigationPresentation = .modal - self.pushViewController(selectionController) - - let _ = (selectionController.result - |> take(1) - |> deliverOnMainQueue).start(next: { [weak selectionController] result in - guard case let .result(peerIds, additionalCategoryIds) = result else { - selectionController?.dismiss() - return - } - - var privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) - if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) { - privacy.base = .everyone - } else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) { - privacy.base = .contacts - } else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) { - privacy.base = .closeFriends - } - privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in - switch id { - case let .peer(peerId): - return peerId - default: - return nil + } else { + return nil + } + }, completion: { mediaResult, commit in + let privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) +// if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) { +// privacy.base = .everyone +// } else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) { +// privacy.base = .contacts +// } else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) { +// privacy.base = .closeFriends +// } +// privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in +// switch id { +// case let .peer(peerId): +// return peerId +// default: +// return nil +// } +// } + + if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { + switch mediaResult { + case let .image(image, dimensions, caption): + if let data = image.jpegData(compressionQuality: 0.8) { + storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: caption?.string ?? "", entities: [], privacy: privacy) + Queue.mainQueue().after(0.3, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) } - } - - selectionController?.displayProgress = true - - if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext { - switch mediaResult { - case let .image(image, dimensions, _): - if let data = image.jpegData(compressionQuality: 0.8) { - storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) - } - case let .video(content, _, values, duration, dimensions, _): - let adjustments: VideoMediaResourceAdjustments - if let valuesData = try? JSONEncoder().encode(values) { - let data = MemoryBuffer(data: valuesData) - let digest = MemoryBuffer(data: data.md5Digest()) - adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) + case let .video(content, _, values, duration, dimensions, caption): + let adjustments: VideoMediaResourceAdjustments + if let valuesData = try? JSONEncoder().encode(values) { + let data = MemoryBuffer(data: valuesData) + let digest = MemoryBuffer(data: data.md5Digest()) + adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true) - let resource: TelegramMediaResource - switch content { - case let .imageFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .videoFile(path): - resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) - case let .asset(localIdentifier): - resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) - } - storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.3, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) + let resource: TelegramMediaResource + switch content { + case let .imageFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .videoFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .asset(localIdentifier): + resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } + storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy) + Queue.mainQueue().after(0.3, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) } } - dismissCameraImpl?() - commit() - selectionController?.dismiss() - }) + } + dismissCameraImpl?() + commit() }) controller.sourceHint = .camera controller.cancelled = {