diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 23ac7ddcb1..48ae9e3b6d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14188,3 +14188,12 @@ Sorry for the inconvenience."; "SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls."; "SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls."; "SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead."; + +"Story.Privacy.ShareStories" = "Share Stories"; +"Story.Privacy.PostStoriesAsHeader" = "POST STORIES AS"; +"Story.Privacy.WhoCanViewStoriesHeader" = "WHO CAN VIEW THIS STORIES"; +"Story.Privacy.PostStories_1" = "Post %@ Story"; +"Story.Privacy.PostStories_any" = "Post %@ Stories"; +"Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply."; +"Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@."; +"Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 3e353b4b4e..52c271d609 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -809,7 +809,7 @@ public protocol MediaEditorScreenResult { public protocol TelegramRootControllerInterface: NavigationController { @discardableResult func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? - func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) + func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func getContactsController() -> ViewController? func getChatsController() -> ViewController? @@ -1152,7 +1152,7 @@ public protocol SharedAccountContext: AnyObject { func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController - func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController + func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController diff --git a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift index 58de64007d..76b2dd5299 100644 --- a/submodules/AccountContext/Sources/AttachmentMainButtonState.swift +++ b/submodules/AccountContext/Sources/AttachmentMainButtonState.swift @@ -42,6 +42,7 @@ public struct AttachmentMainButtonState { public let isEnabled: Bool public let hasShimmer: Bool public let iconName: String? + public let smallSpacing: Bool public let position: Position? public init( @@ -55,6 +56,7 @@ public struct AttachmentMainButtonState { isEnabled: Bool, hasShimmer: Bool, iconName: String? = nil, + smallSpacing: Bool = false, position: Position? = nil ) { self.text = text @@ -67,6 +69,7 @@ public struct AttachmentMainButtonState { self.isEnabled = isEnabled self.hasShimmer = hasShimmer self.iconName = iconName + self.smallSpacing = smallSpacing self.position = position } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index b9e12076ff..b74cbe5631 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -790,6 +790,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode { iconNode = ASImageNode() iconNode.displaysAsynchronously = false iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor) + self.iconNode = iconNode self.addSubnode(iconNode) } if let iconSize = iconNode.image?.size { @@ -1806,7 +1807,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } else { height = bounds.height + 8.0 } - if !isNarrowButton { + if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing { + + } else if !isNarrowButton { height += 9.0 } if isTwoVerticalButtons { @@ -1896,7 +1899,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize) case .bottom: mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize) - secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize) + let buttonSpacing = self.secondaryButtonState.smallSpacing ? 8.0 : sideInset + secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + buttonSpacing + buttonSize.height), size: buttonSize) case .left: secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize) mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX + buttonSize.width + sideInset, y: buttonOriginY), size: buttonSize) diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 7f65ce1f9d..01b660ad8d 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -566,6 +566,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.hasSelectionChanged(false) } + public func clearAll() { + for case let view as DrawingEntityView in self.subviews { + view.reset() + view.selectionView?.removeFromSuperview() + view.removeFromSuperview() + } + } + private func clear(animated: Bool = false) { if animated { for case let view as DrawingEntityView in self.subviews { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 6d13bbbcb4..9ea769f836 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -184,7 +184,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att private let bannedSendVideos: (Int32, Bool)? private let canBoostToUnrestrict: Bool fileprivate let paidMediaAllowed: Bool - private let subject: Subject + fileprivate let subject: Subject fileprivate let forCollage: Bool private let saveEditedPhotos: Bool @@ -1826,6 +1826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att fileprivate let secondaryButtonStatePromise = Promise(nil) private let mainButtonAction: (() -> Void)? + private let secondaryButtonAction: (() -> Void)? public init( context: AccountContext, @@ -1845,7 +1846,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att selectionContext: TGMediaSelectionContext? = nil, saveEditedPhotos: Bool = false, mainButtonState: AttachmentMainButtonState? = nil, - mainButtonAction: (() -> Void)? = nil + mainButtonAction: (() -> Void)? = nil, + secondaryButtonAction: (() -> Void)? = nil ) { self.context = context @@ -1865,6 +1867,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.saveEditedPhotos = saveEditedPhotos self.mainButtonStatePromise.set(.single(mainButtonState)) self.mainButtonAction = mainButtonAction + self.secondaryButtonAction = secondaryButtonAction let selectionContext = selectionContext ?? TGMediaSelectionContext() @@ -1998,7 +2001,14 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } else if collection == nil { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + var hasSelect = false if forCollage { + hasSelect = true + } else if case .story = mode { + hasSelect = true + } + + if hasSelect { self.navigationItem.rightBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Select, target: self, action: #selector(self.selectPressed)) } else { if [.createSticker].contains(mode) { @@ -2338,6 +2348,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) var moreIsVisible = false if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { + if count == 1 { + self.requestAttachmentMenuExpansion() + } moreIsVisible = true } else if case let .media(media) = self.subject { self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count) @@ -2381,7 +2394,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) - if self.selectionCount > 0 { + if case .assets(_, .story) = self.subject, self.selectionCount > 0 { //TODO:localize var text = "Create 1 Story" if self.selectionCount > 1 { @@ -2390,7 +2403,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top))) if self.selectionCount > 1 && self.selectionCount <= 6 { - self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", position: .bottom))) + self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom))) } else { self.secondaryButtonStatePromise.set(.single(nil)) } @@ -2427,6 +2440,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att self.mainButtonAction?() } + func secondaryButtonPressed() { + self.secondaryButtonAction?() + } + func dismissAllTooltips() { self.undoOverlayController?.dismissWithCommitAction() } @@ -2810,7 +2827,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext { private weak var controller: MediaPickerScreenImpl? var selectionCount: Signal { - if self.controller?.forCollage == true { + if let controller = self.controller, case .assets(_, .story) = controller.subject { return .single(0) } else { return Signal { [weak self] subscriber in @@ -2973,7 +2990,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext { } func secondaryButtonAction() { - self.controller?.mainButtonPressed() + self.controller?.secondaryButtonPressed() } } @@ -3162,7 +3179,7 @@ public func storyMediaPickerController( selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, - multipleCompletion: @escaping ([Any]) -> Void, + multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void ) -> ViewController { @@ -3181,9 +3198,18 @@ public func storyMediaPickerController( } } - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { - return nil - }) + let controller = AttachmentController( + context: context, + updatedPresentationData: updatedPresentationData, + chatLocation: nil, + buttons: [.standalone], + initialButton: .standalone, + fromMenu: false, + hasTextInput: false, + makeEntityInputView: { + return nil + } + ) controller.forceSourceRect = true controller.getSourceRect = getSourceRect controller.requestController = { _, present in @@ -3207,7 +3233,18 @@ public func storyMediaPickerController( results.append(asset) } } - multipleCompletion(results) + multipleCompletion(results, false) + } + }, + secondaryButtonAction: { [weak selectionContext] in + if let selectionContext, let selectedItems = selectionContext.selectedItems() { + var results: [Any] = [] + for item in selectedItems { + if let item = item as? TGMediaAsset, let asset = item.backingAsset { + results.append(asset) + } + } + multipleCompletion(results, true) } } ) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index c612e59a65..f2cf4d1cd2 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1651,6 +1651,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { case videoCollage(VideoCollage) case asset(PHAsset) case draft(MediaEditorDraft) + case assets([PHAsset]) func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result { switch self { @@ -3637,11 +3638,10 @@ public class CameraScreenImpl: ViewController, CameraScreen { selectionLimit = 10 } } - //TODO:unmock controller = self.context.sharedContext.makeStoryMediaPickerScreen( context: self.context, isDark: true, - forCollage: self.cameraState.isCollageEnabled || "".isEmpty, + forCollage: self.cameraState.isCollageEnabled, selectionLimit: selectionLimit, getSourceRect: { [weak self] in if let self { @@ -3707,44 +3707,52 @@ public class CameraScreenImpl: ViewController, CameraScreen { } } } - }, multipleCompletion: { [weak self] results in + }, multipleCompletion: { [weak self] results, collage in guard let self else { return } - - if !self.cameraState.isCollageEnabled { - var selectedGrid: Camera.CollageGrid = collageGrids.first! - for grid in collageGrids { - if grid.count == results.count { - selectedGrid = grid - break + + if collage { + if !self.cameraState.isCollageEnabled { + var selectedGrid: Camera.CollageGrid = collageGrids.first! + for grid in collageGrids { + if grid.count == results.count { + selectedGrid = grid + break + } } + self.updateCameraState({ + $0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid) + }, transition: .spring(duration: 0.3)) } - self.updateCameraState({ - $0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid) - }, transition: .spring(duration: 0.3)) - } - - if let assets = results as? [PHAsset] { - var results: [Signal] = [] - for asset in assets { - if asset.mediaType == .video && asset.duration > 1.0 { - results.append(.single(.asset(asset))) - } else { - results.append( - assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat) - |> runOn(Queue.concurrentDefaultQueue()) - |> mapToSignal { image -> Signal in - if let image { - return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft))) - } else { - return .complete() + + if let assets = results as? [PHAsset] { + var results: [Signal] = [] + for asset in assets { + if asset.mediaType == .video && asset.duration > 1.0 { + results.append(.single(.asset(asset))) + } else { + results.append( + assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat) + |> runOn(Queue.concurrentDefaultQueue()) + |> mapToSignal { image -> Signal in + if let image { + return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft))) + } else { + return .complete() + } } - } - ) + ) + } } + self.node.collage?.addResults(signals: results) + } + } else { + if let assets = results as? [PHAsset] { + self.completion(.single(.assets(assets)), nil, { + + }) } - self.node.collage?.addResults(signals: results) } self.galleryController = nil diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e74d17b390..b07b9b4bfb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -65,6 +65,8 @@ swift_library( "//submodules/UrlEscaping", "//submodules/DeviceLocationManager", "//submodules/TelegramUI/Components/SaveProgressScreen", + "//submodules/TelegramUI/Components/MediaAssetsContext", + "//submodules/CheckNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 02770cd84f..1b053546ee 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -160,7 +160,7 @@ public extension MediaEditorScreenImpl { } else { existingMedia = storyItem.media } - rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit) } }) } else { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 63f2308316..ef0262ea56 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -327,6 +327,7 @@ final class MediaEditorScreenComponent: Component { private let switchCameraButton = ComponentView() private let selectionButton = ComponentView() + private let selectionPanel = ComponentView() private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() @@ -741,6 +742,13 @@ final class MediaEditorScreenComponent: Component { return inputText } + func setInputText(_ text: NSAttributedString) { + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + inputPanelView.setSendMessageInput(value: .text(text), updateState: true) + } + private func updateCoverPosition() { guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else { return @@ -1993,39 +2001,118 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - - let selectionButtonSize = self.selectionButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent( - SelectionPanelButtonContentComponent( - count: 1, - isSelected: self.isSelectionPanelOpen, - tag: nil + if controller.node.items.count > 1 { + let selectionButtonSize = self.selectionButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent( + SelectionPanelButtonContentComponent( + count: Int32(controller.node.items.count(where: { $0.isEnabled })), + isSelected: self.isSelectionPanelOpen, + tag: nil + ) + ), + effectAlignment: .center, + action: { [weak self] in + if let self { + self.isSelectionPanelOpen = !self.isSelectionPanelOpen + self.state?.updated() + } + }, + animateAlpha: false + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + let selectionButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0), + size: selectionButtonSize + ) + if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { + if selectionButtonView.superview == nil { + self.addSubview(selectionButtonView) + } + transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) + transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) + transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) + + if self.isSelectionPanelOpen { + let selectionPanelFrame = CGRect( + origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0), + size: CGSize(width: availableSize.width - 24.0, height: 120.0) ) - ), - effectAlignment: .center, - action: { [weak self] in - if let self { - self.isSelectionPanelOpen = !self.isSelectionPanelOpen - self.state?.updated() + + var selectedItemId = "" + if case let .asset(asset) = controller.node.subject { + selectedItemId = asset.localIdentifier } - }, - animateAlpha: false - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - let selectionButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - selectionButtonSize.height - 3.0)), - size: selectionButtonSize - ) - if let selectionButtonView = self.selectionButton.view { - if selectionButtonView.superview == nil { - self.addSubview(selectionButtonView) + + let _ = self.selectionPanel.update( + transition: transition, + component: AnyComponent( + SelectionPanelComponent( + previewContainerView: controller.node.previewContentContainerView, + frame: selectionPanelFrame, + items: controller.node.items, + selectedItemId: selectedItemId, + itemTapped: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + self.isSelectionPanelOpen = false + self.state?.updated() + + if let id { + controller.node.switchToItem(id) + } + }, + itemSelectionToggled: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { + controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled + } + self.state?.updated(transition: .spring(duration: 0.3)) + }, + itemReordered: { [weak self, weak controller] fromId, toId in + guard let self, let controller else { + return + } + guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { + return + } + let fromItem = controller.node.items[fromIndex] + let toItem = controller.node.items[toIndex] + controller.node.items[fromIndex] = toItem + controller.node.items[toIndex] = fromItem + self.state?.updated(transition: .spring(duration: 0.3)) + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { + if selectionPanelView.superview == nil { + self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) + if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateIn(from: buttonView) + } + } + selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { + if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in + selectionPanelView?.removeFromSuperview() + }) + } else { + selectionPanelView.removeFromSuperview() + } + } } - transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) - transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) } } else { inputPanelSize = CGSize(width: 0.0, height: 12.0) @@ -2795,6 +2882,38 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } + struct EditingItem: Equatable { + let asset: PHAsset + var values: MediaEditorValues? + var caption = NSAttributedString() + var thumbnail: UIImage? + var isEnabled = true + var version: Int = 0 + + init(asset: PHAsset) { + self.asset = asset + } + + public static func ==(lhs: EditingItem, rhs: EditingItem) -> Bool { + if lhs.asset.localIdentifier != rhs.asset.localIdentifier { + return false + } + if lhs.values != rhs.values { + return false + } + if lhs.caption != rhs.caption { + return false + } + if lhs.thumbnail != rhs.thumbnail { + return false + } + if lhs.version != rhs.version { + return false + } + return true + } + } + final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate, UIScrollViewDelegate { private weak var controller: MediaEditorScreenImpl? private let context: AccountContext @@ -2803,6 +2922,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var subject: MediaEditorScreenImpl.Subject? var actualSubject: MediaEditorScreenImpl.Subject? + var items: [EditingItem] = [] private var subjectDisposable: Disposable? private var appInForegroundDisposable: Disposable? @@ -2891,6 +3011,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private let readyValue = Promise() + var componentHostView: MediaEditorScreenComponent.View? { + return self.componentHost.view as? MediaEditorScreenComponent.View + } + init(controller: MediaEditorScreenImpl) { self.controller = controller self.context = controller.context @@ -3062,7 +3186,46 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID |> deliverOnMainQueue ).start(next: { [weak self] subject in if let self, let subject { - self.setup(with: subject) + self.actualSubject = subject + + var effectiveSubject = subject + switch subject { + case let .assets(assets): + effectiveSubject = .asset(assets.first!) + self.items = assets.map { EditingItem(asset: $0) } + case let .draft(draft, _): + for entity in draft.values.entities { + if case let .sticker(sticker) = entity { + switch sticker.content { + case let .message(ids, _, _, _, _): + effectiveSubject = .message(ids) + case let .gift(gift, _): + effectiveSubject = .gift(gift) + default: + break + } + } + } + default: + break + } + + var privacy: MediaEditorResultPrivacy? + var values: MediaEditorValues? + var isDraft = false + if case let .draft(draft, _) = subject { + privacy = draft.privacy + values = draft.values + isDraft = true + } + + self.setup( + subject: effectiveSubject, + privacy: privacy, + values: values, + caption: nil, + isDraft: isDraft + ) } }) @@ -3183,35 +3346,24 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.stickerCutoutStatusDisposable?.dispose() } - private func setup(with subject: MediaEditorScreenImpl.Subject) { + func setup( + subject: MediaEditorScreenImpl.Subject, + privacy: MediaEditorResultPrivacy? = nil, + values: MediaEditorValues?, + caption: NSAttributedString?, + isDraft: Bool = false + ) { guard let controller = self.controller else { return } - self.actualSubject = subject - - var effectiveSubject = subject - if case let .draft(draft, _ ) = subject { - for entity in draft.values.entities { - if case let .sticker(sticker) = entity { - switch sticker.content { - case let .message(ids, _, _, _, _): - effectiveSubject = .message(ids) - case let .gift(gift, _): - effectiveSubject = .gift(gift) - default: - break - } - } - } - } - self.subject = effectiveSubject + self.subject = subject Queue.mainQueue().justDispatch { controller.setupAudioSessionIfNeeded() } - - if case let .draft(draft, _) = subject, let privacy = draft.privacy { + + if let privacy { controller.state.privacy = privacy } @@ -3229,7 +3381,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID controller.isSavingAvailable = isSavingAvailable controller.requestLayout(transition: .immediate) - let mediaDimensions = effectiveSubject.dimensions + 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(size: fittedSize) @@ -3268,27 +3420,28 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let initialValues: MediaEditorValues? - if case let .draft(draft, _) = subject { - initialValues = draft.values + if let values { + initialValues = values - for entity in draft.values.entities { + for entity in values.entities { self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false) } - if let drawingData = initialValues?.drawing?.pngData() { + if let drawingData = values.drawing?.pngData() { self.drawingView.setup(withDrawing: drawingData) } } else { initialValues = nil } - var mediaEditorMode: MediaEditor.Mode = .default - if case .stickerEditor = controller.mode { + let mediaEditorMode: MediaEditor.Mode + switch controller.mode { + case .stickerEditor: mediaEditorMode = .sticker - } else if case .avatarEditor = controller.mode { - mediaEditorMode = .avatar - } else if case .coverEditor = controller.mode { + case .avatarEditor, .coverEditor: mediaEditorMode = .avatar + default: + mediaEditorMode = .default } if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView { @@ -3314,7 +3467,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } - let mediaEditor = MediaEditor(context: self.context, mode: mediaEditorMode, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor( + context: self.context, + mode: mediaEditorMode, + subject: subject.editorSubject, + values: initialValues, + hasHistogram: true + ) if case .avatarEditor = controller.mode { mediaEditor.setVideoIsMuted(true) } else if case let .coverEditor(dimensions) = controller.mode { @@ -3327,7 +3486,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID mediaEditor.seek(initialVideoPosition, andPlay: true) } } - if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { + if !isDraft, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered { switch subject { case .message, .gift: mediaEditor.setNightTheme(true) @@ -3347,46 +3506,48 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } } - self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus - |> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in - guard let self else { - return + if case .stickerEditor = controller.mode { + self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus + |> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in + guard let self else { + return + } + self.stickerCutoutStatus = cutoutStatus + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) + }) + mediaEditor.maskUpdated = { [weak self] mask, apply in + guard let self else { + return + } + if self.stickerMaskDrawingView == nil { + self.setupMaskDrawingView(size: mask.size) + } + if apply, let maskData = mask.pngData() { + self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true) + } } - self.stickerCutoutStatus = cutoutStatus - self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) - }) - mediaEditor.maskUpdated = { [weak self] mask, apply in - guard let self else { - return + mediaEditor.classificationUpdated = { [weak self] classes in + guard let self else { + return + } + self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 }) } - if self.stickerMaskDrawingView == nil { - self.setupMaskDrawingView(size: mask.size) - } - if apply, let maskData = mask.pngData() { - self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true) - } - } - mediaEditor.classificationUpdated = { [weak self] classes in - guard let self else { - return - } - self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 }) } mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false)) - if case .empty = effectiveSubject { + if case .empty = subject { self.stickerMaskDrawingView?.emptyColor = .black self.stickerMaskDrawingView?.clearWithEmptyColor() } - switch effectiveSubject { + switch subject { case .message, .gift: break default: self.readyValue.set(.single(true)) } - switch effectiveSubject { + switch subject { case let .image(_, _, additionalImage, position): if let additionalImage { let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in @@ -3431,7 +3592,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID case .message, .gift: var isGift = false let messages: Signal<[Message], NoError> - if case let .message(messageIds) = effectiveSubject { + if case let .message(messageIds) = subject { messages = self.context.engine.data.get( EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:))) ) @@ -3444,7 +3605,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } return messages } - } else if case let .gift(gift) = effectiveSubject { + } else if case let .gift(gift) = subject { isGift = true let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) @@ -3468,7 +3629,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let wallpaperColors: Signal<(UIColor?, UIColor?), NoError> - if let subject = self.subject, case .gift = subject { + if case .gift = subject { wallpaperColors = self.mediaEditorPromise.get() |> mapToSignal { mediaEditor in if let mediaEditor { @@ -3498,7 +3659,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1) renderer.render(completion: { result in - if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in + if isDraft, let existingEntityView = self.entitiesView.getView(where: { entityView in if let stickerEntityView = entityView as? DrawingStickerEntityView { if case .message = (stickerEntityView.entity as! DrawingStickerEntity).content { return true @@ -3508,13 +3669,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } return false }) as? DrawingStickerEntityView { - #if DEBUG - if let data = result.dayImage.pngData() { - let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).png" - try? data.write(to: URL(fileURLWithPath: path)) - } - #endif - existingEntityView.isNightTheme = isNightTheme let messageEntity = existingEntityView.entity as! DrawingStickerEntity messageEntity.renderImage = result.dayImage @@ -3524,7 +3678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } else { var content: DrawingStickerEntity.Content var position: CGPoint - switch effectiveSubject { + switch subject { case let .message(messageIds): content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius) position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0) @@ -3579,7 +3733,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.backgroundDimView.isHidden = false }) } - } else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject { + } else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject { self.previewContainerView.alpha = 1.0 self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in @@ -3608,7 +3762,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } - if effectiveSubject.isPhoto { + if subject.isPhoto { 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 @@ -3621,6 +3775,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } } + } else { + if let caption { + mediaEditor.onFirstDisplay = { [weak self] in + self?.componentHostView?.setInputText(caption) + } + } } mediaEditor.onPlaybackAction = { [weak self] action in @@ -3815,7 +3975,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return nil } let colorSpace = CGColorSpaceCreateDeviceRGB() - let imageSize = CGSize(width: 1080, height: 1920) + let imageSize = storyDimensions if let context = DrawingContext(size: imageSize, scale: 1.0, opaque: true, colorSpace: colorSpace) { context.withFlippedContext { context in if let image = mediaEditor.resultImage?.cgImage { @@ -4214,9 +4374,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) completion() case .camera: - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateIn(from: .camera, completion: completion) - } + self.componentHostView?.animateIn(from: .camera, completion: completion) if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage { var transitionImage = mainTransitionImage @@ -4227,7 +4385,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID backgroundImage = additionalTransitionImage foregroundImage = mainTransitionImage } - if let combinedTransitionImage = generateImage(CGSize(width: 1080, height: 1920), scale: 1.0, rotatedContext: { size, context in + if let combinedTransitionImage = generateImage(storyDimensions, scale: 1.0, rotatedContext: { size, context in UIGraphicsPushContext(context) backgroundImage.draw(in: CGRect(origin: CGPoint(x: (size.width - backgroundImage.size.width) / 2.0, y: (size.height - backgroundImage.size.height) / 2.0), size: backgroundImage.size)) @@ -4253,9 +4411,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.setupTransitionImage(sourceImage) } if let sourceView = transitionIn.sourceView { - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateIn(from: .gallery) - } + self.componentHostView?.animateIn(from: .gallery) let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width @@ -4273,7 +4429,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.backgroundDimView.isHidden = false self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35) - if let componentView = self.componentHost.view { + if let componentView = self.componentHostView { componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction) componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) @@ -4293,8 +4449,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if animateIn, let layout = self.validLayout { self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true) completion() - } else if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateIn(from: .camera, completion: completion) + } else { + self.componentHostView?.animateIn(from: .camera, completion: completion) } } } @@ -4346,9 +4502,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID destinationTransitionView = destinationTransitionOutView destinationTransitionRect = galleryTransitionIn.sourceRect } - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOut(to: .gallery) - } + self.componentHostView?.animateOut(to: .gallery) } let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width @@ -4446,7 +4600,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID removeOnCompletion: false ) - if let componentView = self.componentHost.view { + if let componentView = self.componentHostView { 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: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) @@ -4464,18 +4618,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } } else if let transitionIn = controller.transitionIn, case .camera = transitionIn { - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOut(to: .camera) - } + self.componentHostView?.animateOut(to: .camera) let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut)) transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() }) } else { if controller.isEmbeddedEditor { - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOut(to: .gallery) - } + self.componentHostView?.animateOut(to: .gallery) self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in @@ -4495,9 +4645,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.isDisplayingTool = tool let transition: ComponentTransition = .easeInOut(duration: 0.2) - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToTool(inPlace: inPlace, transition: transition) - } + self.componentHostView?.animateOutToTool(inPlace: inPlace, transition: transition) self.requestUpdate(transition: transition) } @@ -4505,9 +4653,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.isDisplayingTool = nil let transition: ComponentTransition = .easeInOut(duration: 0.2) - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromTool(inPlace: inPlace, transition: transition) - } + self.componentHostView?.animateInFromTool(inPlace: inPlace, transition: transition) self.requestUpdate(transition: transition) } @@ -4721,12 +4867,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } var location: CLLocationCoordinate2D? - if let subject = self.actualSubject { - if case let .asset(asset) = subject { - location = asset.location?.coordinate - } else if case let .draft(draft, _) = subject { - location = draft.location - } + if case let .draft(draft, _) = self.actualSubject { + location = draft.location + } else if case let .asset(asset) = self.subject { + location = asset.location?.coordinate } let locationController = storyLocationPickerController( context: self.context, @@ -5195,7 +5339,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) { guard let weather else { - return } let maxWeatherCount = 1 @@ -5222,6 +5365,58 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID ) } + func getCaption() -> NSAttributedString { + return self.componentHostView?.getInputText() ?? NSAttributedString() + } + + func switchToItem(_ identifier: String) { + guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == identifier }), case let .asset(asset) = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) else { + return + } + + let entities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } + let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.entitiesView) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + + var updatedCurrentItem = self.items[currentItemIndex] + updatedCurrentItem.caption = self.getCaption() + + if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values { + updatedCurrentItem.values = mediaEditor.values + updatedCurrentItem.version += 1 + + if let resultImage = mediaEditor.resultImage { + mediaEditor.seek(0.0, andPlay: false) + makeEditorImageComposition( + context: self.ciContext, + postbox: self.context.account.postbox, + inputImage: resultImage, + dimensions: storyDimensions, + values: mediaEditor.values, + time: .zero, + textScale: 2.0, + completion: { [weak self] resultImage in + updatedCurrentItem.version += 1 + updatedCurrentItem.thumbnail = resultImage + self?.items[currentItemIndex] = updatedCurrentItem + } + ) + } + } else { + updatedCurrentItem.version += 1 + self.items[currentItemIndex] = updatedCurrentItem + } + + self.entitiesView.clearAll() + + let targetItem = self.items[itemIndex] + controller.node.setup( + subject: .asset(targetItem.asset), + values: targetItem.values, + caption: targetItem.caption + ) + } + func requestCompletion(playHaptic: Bool = true) { guard let controller = self.controller else { return @@ -5323,7 +5518,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) - if result == self.componentHost.view { + if result == self.componentHostView { let point = self.view.convert(point, to: self.previewContainerView) if let previewResult = self.previewContainerView.hitTest(point, with: event) { return previewResult @@ -6181,6 +6376,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID case message([MessageId]) case gift(StarGift.UniqueGift) case sticker(TelegramMediaFile, [String]) + case assets([PHAsset]) var dimensions: PixelDimensions { switch self { @@ -6192,8 +6388,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) case let .draft(draft, _): return draft.dimensions - case .message, .gift, .sticker, .videoCollage: - return PixelDimensions(width: 1080, height: 1920) + case .message, .gift, .sticker, .videoCollage, .assets: + return PixelDimensions(storyDimensions) } } @@ -6220,6 +6416,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return .gift(gift) case let .sticker(sticker, _): return .sticker(sticker) + case let .assets(assets): + return .asset(assets.first!) } } @@ -6247,6 +6445,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return false case .sticker: return false + case .assets: + return false } } } @@ -6555,7 +6755,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let privacy = privacy ?? self.state.privacy - let text = self.getCaption().string + let text = self.node.getCaption().string let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) } let coverImage: UIImage? @@ -6567,7 +6767,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let stateContext = ShareWithPeersScreen.StateContext( context: self.context, - subject: .stories(editing: false), + subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))), editing: false, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), closeFriends: self.closeFriends.get(), @@ -7105,13 +7305,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self?.dismissed() }) } - - func getCaption() -> NSAttributedString { - return (self.node.componentHost.view as? MediaEditorScreenComponent.View)?.getInputText() ?? NSAttributedString() - } - + fileprivate func checkCaptionLimit() -> Bool { - let caption = self.getCaption() + let caption = self.node.getCaption() if caption.length > self.context.userLimits.maxStoryCaptionLength { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in @@ -7160,7 +7356,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - var caption = self.getCaption() + var caption = self.node.getCaption() caption = convertMarkdownToAttributes(caption) var hasEntityChanges = false @@ -7209,7 +7405,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { - self.saveDraft(id: randomId, edit: true) + self.saveDraft(id: randomId, isEdit: true) self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in @@ -7498,7 +7694,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } duration = 5.0 case .sticker: - let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in + let image = generateImage(storyDimensions, contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) }, opaque: false, scale: 1.0) let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png" @@ -7509,6 +7705,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID duration = 3.0 firstFrame = .single((image, nil)) + case .assets: + fatalError() } let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) @@ -8399,6 +8597,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } case let .sticker(file, _): exportSubject = .single(.sticker(file: file)) + case .assets: + fatalError() } let _ = (exportSubject diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift similarity index 61% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift rename to submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift index 03419f173a..26d9dad3e6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorDrafts.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenDrafts.swift @@ -11,7 +11,16 @@ import DrawingUI extension MediaEditorScreenImpl { func isEligibleForDraft() -> Bool { - if self.isEditingStory { + guard !self.isEditingStory else { + return false + } + if case .avatarEditor = self.mode { + return false + } + if case .coverEditor = self.mode { + return false + } + if case .assets = self.node.actualSubject { return false } guard let mediaEditor = self.node.mediaEditor else { @@ -21,13 +30,6 @@ extension MediaEditorScreenImpl { let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - if case .avatarEditor = self.mode { - return false - } - if case .coverEditor = self.mode { - return false - } - let filteredEntities = self.node.entitiesView.entities.filter { entity in if entity is DrawingMediaEntity { return false @@ -44,34 +46,42 @@ extension MediaEditorScreenImpl { let values = mediaEditor.values let filteredValues = values.withUpdatedEntities([]) + let caption = self.node.getCaption() - let caption = self.getCaption() if let subject = self.node.subject { - if case .asset = subject, !values.hasChanges && caption.string.isEmpty { - return false - } else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { - return false - } else if case .gift = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { - return false - } else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo { - return false - } else if case .videoCollage = subject { + switch subject { + case .asset: + if !values.hasChanges && caption.string.isEmpty { + return false + } + case .message, .gift: + if !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty { + return false + } + case .empty: + if !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo { + return false + } + case .videoCollage: return false + default: + break } } return true } - func saveDraft(id: Int64?, edit: Bool = false) { + func saveDraft(id: Int64?, isEdit: Bool = false, completion: ((MediaEditorDraft) -> Void)? = nil) { guard case .storyEditor = self.mode, let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else { return } + try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) let values = mediaEditor.values let privacy = self.state.privacy let forwardSource = self.forwardSource - let caption = self.getCaption() + let caption = self.node.getCaption() let duration = mediaEditor.duration ?? 0.0 let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) @@ -99,10 +109,19 @@ extension MediaEditorScreenImpl { } if let resultImage = mediaEditor.resultImage { - if !edit { + if !isEdit { mediaEditor.seek(0.0, andPlay: false) } - makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in + + makeEditorImageComposition( + context: self.node.ciContext, + postbox: self.context.account.postbox, + inputImage: resultImage, + dimensions: storyDimensions, + values: values, + time: .zero, + textScale: 2.0, + completion: { resultImage in guard let resultImage else { return } @@ -148,54 +167,64 @@ extension MediaEditorScreenImpl { } let context = self.context - func innerSaveDraft(media: MediaInput) { + func innerSaveDraft(media: MediaInput, save: Bool = true) -> MediaEditorDraft? { let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0)) - if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) { - let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)" - let draft = MediaEditorDraft( - path: path, - isVideo: media.isVideo, - thumbnail: thumbnailImage, - dimensions: media.dimensions, - duration: media.duration, - values: values, - caption: caption, - privacy: privacy, - forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) }, - timestamp: timestamp, - location: location, - expiresOn: expiresOn - ) - switch media { - case let .image(image, _): - if let data = image.jpegData(compressionQuality: 0.87) { - try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) - } - case let .video(path, _, _): - try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine)) + guard let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) else { + return nil + } + let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)" + let draft = MediaEditorDraft( + path: path, + isVideo: media.isVideo, + thumbnail: thumbnailImage, + dimensions: media.dimensions, + duration: media.duration, + values: values, + caption: caption, + privacy: privacy, + forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) }, + timestamp: timestamp, + location: location, + expiresOn: expiresOn + ) + switch media { + case let .image(image, _): + if let data = image.jpegData(compressionQuality: 0.87) { + try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine))) } + case let .video(path, _, _): + try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine)) + } + if save { if let id { saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id) } else { addStoryDraft(engine: context.engine, item: draft) } } + return draft } switch subject { case .empty: break case let .image(image, dimensions, _, _): - innerSaveDraft(media: .image(image: image, dimensions: dimensions)) + if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) { + completion?(draft) + } case let .video(path, _, _, _, _, dimensions, _, _, _): - innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) + if let draft = innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) { + completion?(draft) + } case let .videoCollage(items): let _ = items case let .asset(asset): if asset.mediaType == .video { PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in if let urlAsset = avAsset as? AVURLAsset { - innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) + if let draft = innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) { + completion?(draft) + } } } } else { @@ -203,22 +232,32 @@ extension MediaEditorScreenImpl { options.deliveryMode = .highQualityFormat PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in if let image { - innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) + if let draft = innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) { + completion?(draft) + } } } } case let .draft(draft, _): if draft.isVideo { - innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) + if let draft = innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) { + completion?(draft) + } } else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) { - innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) + if let draft = innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) { + completion?(draft) + } } case .message, .gift: if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) { - innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) + if let draft = innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) { + completion?(draft) + } } case .sticker: break + case .assets: + break } if case let .draft(draft, _) = actualSubject { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenRecording.swift similarity index 100% rename from submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorRecording.swift rename to submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreenRecording.swift diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift index cbe16e36f0..ca1ff99611 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelButtonContentComponent.swift @@ -34,7 +34,7 @@ final class SelectionPanelButtonContentComponent: Component { return false } - private let backgroundView: BlurredBackgroundView + let backgroundView: BlurredBackgroundView private let outline = SimpleLayer() private let icon = SimpleLayer() private let label = ComponentView() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift index e0edb237b4..b405b061f7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SelectionPanelComponent.swift @@ -1,7 +1,670 @@ - import Foundation import UIKit import Display import ComponentFlow +import SwiftSignalKit +import AccountContext +import MediaEditor +import MediaAssetsContext +import CheckNode +import TelegramPresentationData +final class SelectionPanelComponent: Component { + let previewContainerView: PortalSourceView + let frame: CGRect + let items: [MediaEditorScreenImpl.EditingItem] + let selectedItemId: String + let itemTapped: (String?) -> Void + let itemSelectionToggled: (String) -> Void + let itemReordered: (String, String) -> Void + + init( + previewContainerView: PortalSourceView, + frame: CGRect, + items: [MediaEditorScreenImpl.EditingItem], + selectedItemId: String, + itemTapped: @escaping (String?) -> Void, + itemSelectionToggled: @escaping (String) -> Void, + itemReordered: @escaping (String, String) -> Void + ) { + self.previewContainerView = previewContainerView + self.frame = frame + self.items = items + self.selectedItemId = selectedItemId + self.itemTapped = itemTapped + self.itemSelectionToggled = itemSelectionToggled + self.itemReordered = itemReordered + } + + static func ==(lhs: SelectionPanelComponent, rhs: SelectionPanelComponent) -> Bool { + return lhs.frame == rhs.frame && lhs.items == rhs.items && lhs.selectedItemId == rhs.selectedItemId + } + + final class View: UIView, UIGestureRecognizerDelegate { + final class ItemView: UIView { + private let backgroundNode: ASImageNode + private let imageNode: ImageNode + private let checkNode: InteractiveCheckNode + private var selectionLayer: SimpleShapeLayer? + + var toggleSelection: () -> Void = {} + + override init(frame: CGRect) { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + + self.imageNode = ImageNode() + self.imageNode.contentMode = .scaleAspectFill + + self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: defaultDarkColorPresentationTheme, style: .overlay)) + + super.init(frame: frame) + self.clipsToBounds = true + self.layer.cornerRadius = 6.0 + + self.addSubview(self.backgroundNode.view) + self.addSubview(self.imageNode.view) + self.addSubview(self.checkNode.view) + + self.checkNode.valueChanged = { [weak self] value in + guard let self else { + return + } + self.toggleSelection() + } + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + fileprivate var item: MediaEditorScreenImpl.EditingItem? + func update(item: MediaEditorScreenImpl.EditingItem, number: Int, isSelected: Bool, isEnabled: Bool, size: CGSize, portalView: PortalView?, transition: ComponentTransition) { + let previousItem = self.item + self.item = item + + if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version { + let imageSignal: Signal + if let thumbnail = item.thumbnail { + imageSignal = .single(thumbnail) + self.imageNode.contentMode = .scaleAspectFill + } else { + imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true) + self.imageNode.contentUpdated = { [weak self] image in + if let self { + if self.backgroundNode.image == nil { + if let image, image.size.width > image.size.height { + self.imageNode.contentMode = .scaleAspectFit + Queue.concurrentDefaultQueue().async { + let colors = mediaEditorGetGradientColors(from: image) + let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array) + Queue.mainQueue().async { + self.backgroundNode.image = gradientImage + } + } + } else { + self.imageNode.contentMode = .scaleAspectFill + } + } + } + } + } + self.imageNode.setSignal(imageSignal) + } + + let backgroundSize = CGSize(width: size.width, height: floorToScreenPixels(size.width / 9.0 * 16.0)) + self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) + + self.imageNode.frame = CGRect(origin: .zero, size: size) + + //self.checkNode.content = .counter(number) + self.checkNode.setSelected(isEnabled, animated: previousItem != nil) + + let checkSize = CGSize(width: 29.0, height: 29.0) + self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - checkSize.width - 4.0, y: 4.0), size: checkSize) + + if isSelected, let portalView { + portalView.view.frame = CGRect(origin: .zero, size: size) + self.insertSubview(portalView.view, aboveSubview: self.imageNode.view) + } + + let lineWidth: CGFloat = 2.0 - UIScreenPixel + let selectionFrame = CGRect(origin: .zero, size: size) + if isSelected { + let selectionLayer: SimpleShapeLayer + if let current = self.selectionLayer { + selectionLayer = current + } else { + selectionLayer = SimpleShapeLayer() + self.selectionLayer = selectionLayer + self.layer.addSublayer(selectionLayer) + + selectionLayer.fillColor = UIColor.clear.cgColor + selectionLayer.strokeColor = UIColor.white.cgColor + selectionLayer.lineWidth = lineWidth + selectionLayer.frame = selectionFrame + selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) + +// if !transition.animation.isImmediate { +// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) +// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) +// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2) +// } + } + + } else if let selectionLayer = self.selectionLayer { + self.selectionLayer = nil + selectionLayer.removeFromSuperlayer() + +// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil) +// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false) +// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in +// selectionLayer.removeFromSuperlayer() +// }) + } + } + } + + private let backgroundView: BlurredBackgroundView + private let backgroundMaskView: UIView + private let backgroundMaskPanelView: UIView + + private let scrollView: UIScrollView + private var itemViews: [AnyHashable: ItemView] = [:] + private var portalView: PortalView? + + private var reorderRecognizer: ReorderGestureRecognizer? + private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint, snapshotView: UIView)? + + private var tapRecognizer: UITapGestureRecognizer? + + private var component: SelectionPanelComponent? + private var state: EmptyComponentState? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true) + self.backgroundMaskView = UIView(frame: .zero) + + self.backgroundMaskPanelView = UIView(frame: .zero) + self.backgroundMaskPanelView.backgroundColor = UIColor.white + self.backgroundMaskPanelView.clipsToBounds = true + self.backgroundMaskPanelView.layer.cornerRadius = 10.0 + + self.scrollView = UIScrollView(frame: .zero) + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.layer.cornerRadius = 10.0 + + super.init(frame: frame) + + self.backgroundView.mask = self.backgroundMaskView + + let reorderRecognizer = ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let self, let item = self.item(at: point) else { + return (allowed: false, requiresLongPress: false, item: nil) + } + + return (allowed: true, requiresLongPress: true, item: item) + }, + willBegin: { point in + }, + began: { [weak self] item in + guard let self else { + return + } + self.setReorderingItem(item: item) + }, + ended: { [weak self] in + guard let self else { + return + } + self.setReorderingItem(item: nil) + }, + moved: { [weak self] distance in + guard let self else { + return + } + self.moveReorderingItem(distance: distance) + }, + isActiveUpdated: { _ in + } + ) + reorderRecognizer.delegate = self + self.reorderRecognizer = reorderRecognizer + self.addGestureRecognizer(reorderRecognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap)) + self.tapRecognizer = tapRecognizer + self.addGestureRecognizer(tapRecognizer) + + self.addSubview(self.backgroundView) + self.addSubview(self.scrollView) + + self.backgroundMaskView.addSubview(self.backgroundMaskPanelView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + deinit { + } + + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + self.reorderRecognizer?.isEnabled = false + self.reorderRecognizer?.isEnabled = true + + let location = gestureRecognizer.location(in: self) + if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId { + component.itemTapped(item.asset.localIdentifier) + } else { + component.itemTapped(nil) + } + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UITapGestureRecognizer { + return true + } + if otherGestureRecognizer is UIPanGestureRecognizer { + if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) { + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + return true + } else { + return false + } + } + return false + } + + func item(at point: CGPoint) -> ItemView? { + let point = self.convert(point, to: self.scrollView) + for (_, itemView) in self.itemViews { + if itemView.frame.contains(point) { + return itemView + } + } + return nil + } + + func setReorderingItem(item: ItemView?) { + self.tapRecognizer?.isEnabled = false + self.tapRecognizer?.isEnabled = true + + var mappedItem: (AnyHashable, ItemView)? + if let item { + for (id, visibleItem) in self.itemViews { + if visibleItem === item { + mappedItem = (id, visibleItem) + break + } + } + } + + if self.reorderingItem?.id != mappedItem?.0 { + let transition: ComponentTransition = .spring(duration: 0.4) + if let (id, itemView) = mappedItem, let snapshotView = itemView.snapshotView(afterScreenUpdates: false) { + itemView.isHidden = true + + let position = self.scrollView.convert(itemView.center, to: self) + snapshotView.center = position + transition.setScale(view: snapshotView, scale: 0.9) + self.addSubview(snapshotView) + + self.reorderingItem = (id, position, position, snapshotView) + } else { + if let (id, _, _, snapshotView) = self.reorderingItem { + if let itemView = self.itemViews[id] { + if let innerSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) { + innerSnapshotView.center = self.convert(snapshotView.center, to: self.scrollView) + innerSnapshotView.transform = CGAffineTransformMakeScale(0.9, 0.9) + self.scrollView.addSubview(innerSnapshotView) + + transition.setPosition(view: innerSnapshotView, position: itemView.center, completion: { [weak innerSnapshotView] _ in + innerSnapshotView?.removeFromSuperview() + itemView.isHidden = false + }) + transition.setScale(view: innerSnapshotView, scale: 1.0) + } + + transition.setPosition(view: snapshotView, position: self.scrollView.convert(itemView.center, to: self), completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + transition.setScale(view: snapshotView, scale: 1.0) + transition.setAlpha(view: snapshotView, alpha: 0.0) + } + } + self.reorderingItem = nil + } + self.state?.updated(transition: transition) + } + } + + func moveReorderingItem(distance: CGPoint) { + guard let component = self.component else { + return + } + if let (id, initialPosition, _, snapshotView) = self.reorderingItem { + let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y) + self.reorderingItem = (id, initialPosition, targetPosition, snapshotView) + snapshotView.center = targetPosition + + let mappedPosition = self.convert(targetPosition, to: self.scrollView) + + if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier { + for (_, visibleItem) in self.itemViews { + if visibleItem === visibleReorderingItem { + continue + } + if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier { + component.itemReordered(fromId, toId) + break + } + } + } + } + } + + func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) { + + } + + func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) { + completion() + } + + func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + if self.portalView == nil { + if let portalView = PortalView(matchPosition: false) { + portalView.view.layer.rasterizationScale = UIScreenScale + + let scale = 95.0 / component.previewContainerView.frame.width + portalView.view.transform = CGAffineTransformMakeScale(scale, scale) + + component.previewContainerView.addPortal(view: portalView) + self.portalView = portalView + } + } + + var validIds = Set() + + let itemSize = CGSize(width: 95.0, height: 112.0) + let spacing: CGFloat = 4.0 + + var itemFrame: CGRect = CGRect(origin: CGPoint(x: spacing, y: spacing), size: itemSize) + + var index = 1 + for item in component.items { + let id = item.asset.localIdentifier + validIds.insert(id) + + var itemTransition = transition + let itemView: ItemView + if let current = self.itemViews[id] { + itemView = current + } else { + itemView = ItemView(frame: itemFrame) + self.scrollView.addSubview(itemView) + self.itemViews[id] = itemView + + itemTransition = .immediate + } + itemView.toggleSelection = { [weak self] in + guard let self, let component = self.component else { + return + } + component.itemSelectionToggled(id) + } + itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition) + + itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size)) + itemTransition.setPosition(view: itemView, position: itemFrame.center) + + itemFrame.origin.x += itemSize.width + spacing + index += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in + itemView?.removeFromSuperview() + }) + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + + let contentSize = CGSize(width: itemFrame.minX, height: itemSize.height + spacing * 2.0) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + + let backgroundSize = CGSize(width: min(availableSize.width - 24.0, contentSize.width), height: contentSize.height) + self.backgroundView.frame = CGRect(origin: .zero, size: availableSize) + self.backgroundView.update(size: availableSize, transition: .immediate) + + let contentFrame = CGRect(origin: CGPoint(x: availableSize.width - 12.0 - backgroundSize.width, y: component.frame.minY), size: backgroundSize) + self.backgroundMaskPanelView.frame = contentFrame + self.scrollView.frame = contentFrame + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) + } +} + +private final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?) + private let willBegin: (CGPoint) -> Void + private let began: (SelectionPanelComponent.View.ItemView) -> Void + private let ended: () -> Void + private let moved: (CGPoint) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var itemView: SelectionPanelComponent.View.ItemView? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SelectionPanelComponent.View.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + + self.isActiveUpdated(false) + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let itemView = self.itemView { + self.began(itemView) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let itemView = self.itemView { + self.began(itemView) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y) + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index fa895d358c..b3a328a3b1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10018,7 +10018,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro viewControllers = viewControllers.filter { !($0 is AttachmentController)} rootController.setViewControllers(viewControllers, animated: false) - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } }, cancelled: {} @@ -10053,7 +10053,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage)) }, - multipleCompletion: { _ in }, + multipleCompletion: { _, _ in }, dismissed: {}, groupsPresented: {} ) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 30e6265ef6..1c656cd9be 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -991,10 +991,22 @@ final class ShareWithPeersScreenComponent: Component { } let sectionTitle: String - if section.id == 0, case .stories = component.stateContext.subject { - sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : "" + if section.id == 0, case let .stories(_, count) = component.stateContext.subject { + if component.coverItem == nil { + if count > 1 { + sectionTitle = environment.strings.Story_Privacy_PostStoriesAsHeader + } else { + sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader + } + } else { + sectionTitle = "" + } } else if section.id == 2 { - sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader + if case let .stories(_, count) = component.stateContext.subject, count > 1 { + sectionTitle = environment.strings.Story_Privacy_WhoCanViewStoriesHeader + } else { + sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader + } } else if section.id == 1 { if case let .members(isGroup, _, _) = component.stateContext.subject { sectionTitle = isGroup ? environment.strings.BoostGift_Members_SectionTitle : environment.strings.BoostGift_Subscribers_SectionTitle @@ -1637,12 +1649,21 @@ final class ShareWithPeersScreenComponent: Component { } let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600)) - var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string - - if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { - footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string + var footerText: String + if case let .stories(_, count) = component.stateContext.subject, count > 1 { + if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { + footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageManyInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageManyInfo(footerValue).string + } else { + footerText = environment.strings.Story_Privacy_KeepOnMyPageManyInfo(footerValue).string + } + } else { + if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true { + footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string + } else { + footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string + } } - + let footerSize = sectionFooter.update( transition: sectionFooterTransition, component: AnyComponent(MultilineTextComponent( @@ -2371,7 +2392,7 @@ final class ShareWithPeersScreenComponent: Component { ) var footersTotalHeight: CGFloat = 0.0 - if case let .stories(editing) = component.stateContext.subject { + if case let .stories(editing, _) = component.stateContext.subject { let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor) let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor) let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor) @@ -2451,7 +2472,7 @@ final class ShareWithPeersScreenComponent: Component { itemHeight: peerItemSize.height, itemCount: peers.count )) - } else if case let .stories(editing) = component.stateContext.subject { + } else if case let .stories(editing, _) = component.stateContext.subject { if !editing && hasChannels { sections.append(ItemLayout.Section( id: 0, @@ -2533,12 +2554,17 @@ final class ShareWithPeersScreenComponent: Component { switch component.stateContext.subject { case .peers: title = environment.strings.Story_Privacy_PostStoryAs - case let .stories(editing): + case let .stories(editing, count): if editing { title = environment.strings.Story_Privacy_EditStory } else { - title = environment.strings.Story_Privacy_ShareStory - actionButtonTitle = environment.strings.Story_Privacy_PostStory + if count > 1 { + title = environment.strings.Story_Privacy_ShareStories + actionButtonTitle = environment.strings.Story_Privacy_PostStories(count) + } else { + title = environment.strings.Story_Privacy_ShareStory + actionButtonTitle = environment.strings.Story_Privacy_PostStory + } } case let .chats(grayList): if grayList { @@ -2627,7 +2653,7 @@ final class ShareWithPeersScreenComponent: Component { inset = 1000.0 } else if case .channels = component.stateContext.subject { inset = 1000.0 - } else if case let .stories(editing) = component.stateContext.subject { + } else if case let .stories(editing, _) = component.stateContext.subject { if editing { inset = 351.0 inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight @@ -3026,7 +3052,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] var optionItems: [ShareWithPeersScreenComponent.OptionItem] = [] var coverItem: ShareWithPeersScreenComponent.CoverItem? - if case let .stories(editing) = stateContext.subject { + if case let .stories(editing, _) = stateContext.subject { var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 { var peerNamesArray: [String] = [] diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift index 9047c66cc4..731bbf596e 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift @@ -44,7 +44,7 @@ public extension ShareWithPeersScreen { final class StateContext { public enum Subject: Equatable { case peers(peers: [EnginePeer], peerId: EnginePeer.Id?) - case stories(editing: Bool) + case stories(editing: Bool, count: Int32) case chats(blocked: Bool) case contacts(base: EngineStoryPrivacy.Base) case contactsSearch(query: String, onlyContacts: Bool) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 2f0f3c2f74..856df6d79b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5068,7 +5068,7 @@ public final class StoryItemSetContainerComponent: Component { let stateContext = ShareWithPeersScreen.StateContext( context: context, - subject: .stories(editing: true), + subject: .stories(editing: true, count: 1), editing: true, initialSelectedPeers: selectedPeers, closeFriends: component.closeFriends.get(), diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b2563c9c57..d234859ace 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3572,7 +3572,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion) } - public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { + public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController { return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented) } @@ -3737,7 +3737,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { externalState.storyTarget = target if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId)) diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 78099bd0fa..3a6662cddd 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -391,6 +391,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return .asset(asset) case let .draft(draft): return .draft(draft, nil) + case let .assets(assets): + return .assets(assets) } } @@ -451,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let customTarget, case .botPreview = customTarget { externalState.storyTarget = customTarget - self.proceedWithStoryUpload(target: customTarget, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() return @@ -484,7 +486,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon externalState.isPeerArchived = channel.storiesHidden ?? false } - self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) dismissCameraImpl?() }) @@ -548,8 +550,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon }) } - public func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { - guard let result = result as? MediaEditorScreenImpl.Result else { + public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) { + guard let results = results as? [MediaEditorScreenImpl.Result] else { return } let context = self.context @@ -657,83 +659,85 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } if let _ = self.chatListController as? ChatListControllerImpl { - var media: EngineStoryInputMedia? - - if let mediaResult = result.media { - switch mediaResult { - case let .image(image, dimensions): - let tempFile = TempBox.shared.tempFile(fileName: "file") - defer { - TempBox.shared.dispose(tempFile) - } - if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { - media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers) - } - case let .video(content, firstFrameImage, 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) - - 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)) - } + for result in results { + var media: EngineStoryInputMedia? + + if let mediaResult = result.media { + switch mediaResult { + case let .image(image, dimensions): let tempFile = TempBox.shared.tempFile(fileName: "file") defer { TempBox.shared.dispose(tempFile) } - let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } - let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in - let file = TempBox.shared.tempFile(fileName: "image.jpg") - if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { - return file - } else { - return nil - } + if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) { + media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers) } - - var coverTime: Double? - if let coverImageTimestamp = values.coverImageTimestamp { - if let trimRange = values.videoTrimRange { - coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound) - } else { - coverTime = min(duration, coverImageTimestamp) + case let .video(content, firstFrameImage, 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) + + let resource: TelegramMediaResource + switch content { + case let .imageFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .videoFile(path): + resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments) + case let .asset(localIdentifier): + resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } + let tempFile = TempBox.shared.tempFile(fileName: "file") + defer { + TempBox.shared.dispose(tempFile) + } + let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) } + let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in + let file = TempBox.shared.tempFile(fileName: "image.jpg") + if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) { + return file + } else { + return nil + } + } + + var coverTime: Double? + if let coverImageTimestamp = values.coverImageTimestamp { + if let trimRange = values.videoTrimRange { + coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound) + } else { + coverTime = min(duration, coverImageTimestamp) + } + } + + media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime) } - - media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime) + default: + break } - default: - break + } else if let existingMedia { + media = .existing(media: existingMedia._asMedia()) + } + + if let media { + let _ = (context.engine.messages.uploadStory( + target: target, + media: media, + mediaAreas: result.mediaAreas, + text: result.caption.string, + entities: generateChatInputTextEntities(result.caption), + pin: result.options.pin, + privacy: result.options.privacy, + isForwardingDisabled: result.options.isForwardingDisabled, + period: result.options.timeout, + randomId: result.randomId, + forwardInfo: forwardInfo + ) + |> deliverOnMainQueue).startStandalone(next: { stableId in + moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId)) + }) } - } else if let existingMedia { - media = .existing(media: existingMedia._asMedia()) - } - - if let media { - let _ = (context.engine.messages.uploadStory( - target: target, - media: media, - mediaAreas: result.mediaAreas, - text: result.caption.string, - entities: generateChatInputTextEntities(result.caption), - pin: result.options.pin, - privacy: result.options.privacy, - isForwardingDisabled: result.options.isForwardingDisabled, - period: result.options.timeout, - randomId: result.randomId, - forwardInfo: forwardInfo - ) - |> deliverOnMainQueue).startStandalone(next: { stableId in - moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId)) - }) } completionImpl() } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index fdc900c25c..922679abce 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1483,7 +1483,7 @@ public final class WebAppController: ViewController, AttachmentContainable { externalState.storyTarget = target if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) } }) if let navigationController = self.controller?.getNavigationController() {