diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cba160b51a..121e9361b2 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11218,6 +11218,7 @@ Sorry for the inconvenience."; "GroupBoost.AdditionalFeatures" = "Additional Features"; "GroupBoost.AdditionalFeaturesText" = "By gaining **boosts**, your group reaches higher levels and unlocks more features."; +"ChannelBoost.AdditionalFeaturesText" = "By gaining **boosts**, your channel reaches higher levels and unlocks more features."; "Stats.Boosts.Group.NoBoostersYet" = "No users currently boost your group"; "Stats.Boosts.Group.BoostersInfo" = "Your group is currently boosted by these members."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 47820b1bc5..bdb918f337 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -941,7 +941,7 @@ public protocol SharedAccountContext: AnyObject { func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController - func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController + func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController func makeBusinessSetupScreen(context: AccountContext) -> ViewController func makeChatbotSetupScreen(context: AccountContext, initialData: ChatbotSetupScreenInitialData) -> ViewController func makeChatbotSetupScreenInitialData(context: AccountContext) -> Signal diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index a8a5e15385..a23c4a9523 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -70,6 +70,14 @@ public enum PremiumDemoSubject { case messageTags case lastSeen case messagePrivacy + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots } public enum PremiumLimitSubject { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 64a9518c5e..846bc2613e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -5617,7 +5617,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private func openFilterSettings() { self.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(false) if let navigationController = self.context.sharedContext.mainWindow?.viewController as? NavigationController { - let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, dismissed: { [weak self] in + let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: true, scrollToTags: false, dismissed: { [weak self] in self?.chatListDisplayNode.mainContainerNode.updateEnableAdjacentFilterLoading(true) }) navigationController.pushViewController(controller) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 9622c4bd39..3b496c6b11 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -43,6 +43,19 @@ private enum ChatListFilterPresetListSection: Int32 { case tags } +public enum ChatListFilterPresetListEntryTag: ItemListItemTag { + case displayTags + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ChatListFilterPresetListEntryTag, self == other { + return true + } else { + return false + } + } +} + + private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String { if peers.isEmpty { return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder @@ -208,7 +221,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { } }, activatedWhileDisabled: { arguments.updateDisplayTagsLocked() - }) + }, tag: ChatListFilterPresetListEntryTag.displayTags) case .displayTagsFooter: //TODO:localize return ItemListTextItem(presentationData: presentationData, text: .plain("Display folder names for each chat in the chat list."), sectionId: self.section) @@ -306,7 +319,7 @@ public enum ChatListFilterPresetListControllerMode { case modal } -public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, dismissed: (() -> Void)? = nil) -> ViewController { +public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController { let initialState = ChatListFilterPresetListControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -657,7 +670,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true) + let entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 78d220a911..5d6719096e 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -60,6 +60,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let context: AccountContext private let size: CGSize private let hasBin: Bool + private let isStickerEditor: Bool weak var drawingView: DrawingView? public weak var selectionContainerView: DrawingSelectionContainerView? @@ -95,16 +96,20 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let yAxisView = UIView() private let angleLayer = SimpleShapeLayer() private let bin = ComponentView() - + + private let stickerOverlayLayer = SimpleShapeLayer() + private let stickerFrameLayer = SimpleShapeLayer() + public var onInteractionUpdated: (Bool) -> Void = { _ in } public var edgePreviewUpdated: (Bool) -> Void = { _ in } private let hapticFeedback = HapticFeedback() - public init(context: AccountContext, size: CGSize, hasBin: Bool = false) { + public init(context: AccountContext, size: CGSize, hasBin: Bool = false, isStickerEditor: Bool = false) { self.context = context self.size = size self.hasBin = hasBin + self.isStickerEditor = isStickerEditor super.init(frame: CGRect(origin: .zero, size: size)) @@ -140,6 +145,13 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.opacity = 0.0 self.angleLayer.lineDashPattern = [12, 12] as [NSNumber] + self.stickerOverlayLayer.fillColor = UIColor(rgb: 0x000000, alpha: 0.6).cgColor + + self.stickerFrameLayer.fillColor = UIColor.clear.cgColor + self.stickerFrameLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.55).cgColor + self.stickerFrameLayer.lineDashPattern = [24, 24] as [NSNumber] + self.stickerFrameLayer.lineCap = .round + self.addSubview(self.topEdgeView) self.addSubview(self.leftEdgeView) self.addSubview(self.rightEdgeView) @@ -148,12 +160,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.addSubview(self.xAxisView) self.addSubview(self.yAxisView) self.layer.addSublayer(self.angleLayer) + + if isStickerEditor { + self.layer.addSublayer(self.stickerOverlayLayer) + self.layer.addSublayer(self.stickerFrameLayer) + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + public override func addSubview(_ view: UIView) { + super.addSubview(view) + if self.stickerOverlayLayer.superlayer != nil, view is DrawingEntityView { + self.layer.addSublayer(self.stickerOverlayLayer) + self.layer.addSublayer(self.stickerFrameLayer) + } + } + public override func layoutSubviews() { super.layoutSubviews() @@ -189,6 +214,25 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { self.angleLayer.path = anglePath self.angleLayer.lineWidth = width self.angleLayer.bounds = CGRect(origin: .zero, size: CGSize(width: 3000.0, height: width)) + + let frameWidth = floor(self.bounds.width * 0.97) + let frameRect = CGRect(origin: CGPoint(x: floor((self.bounds.width - frameWidth) / 2.0), y: floor((self.bounds.height - frameWidth) / 2.0)), size: CGSize(width: frameWidth, height: frameWidth)) + + self.stickerOverlayLayer.frame = self.bounds + + let overlayOuterRect = UIBezierPath(rect: self.bounds) + let overlayInnerRect = UIBezierPath(cgPath: CGPath(roundedRect: frameRect, cornerWidth: frameWidth / 8.0, cornerHeight: frameWidth / 8.0, transform: nil)) + let overlayLineWidth: CGFloat = 2.0 * 2.2 + + overlayOuterRect.append(overlayInnerRect) + overlayOuterRect.usesEvenOddFillRule = true + + self.stickerOverlayLayer.path = overlayOuterRect.cgPath + self.stickerOverlayLayer.fillRule = .evenOdd + + self.stickerFrameLayer.frame = self.bounds + self.stickerFrameLayer.lineWidth = overlayLineWidth + self.stickerFrameLayer.path = CGPath(roundedRect: frameRect.insetBy(dx: -overlayLineWidth / 2.0, dy: -overlayLineWidth / 2.0), cornerWidth: frameWidth / 8.0 * 1.02, cornerHeight: frameWidth / 8.0 * 1.02, transform: nil) } public var entities: [DrawingEntity] { @@ -841,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { self.selectEntity(viewToSelect.entity, animate: false) self.onInteractionUpdated(true) - } else if gestureRecognizer.numberOfTouches == 2, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { + } else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { mediaEntityView.handlePan(gestureRecognizer) } } diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index f56914ff3f..270874cdbc 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -192,6 +192,7 @@ private final class StickerSelectionComponent: Component { insertText: { _ in }, backwardsDeleteText: {}, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self, let controller = self.component?.getController() { controller.present(c, in: .window(.root), with: a) diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 7b2a393180..069066261f 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -889,11 +889,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } }) - if case let .share(_, selfPeer, _) = self.mode { + switch self.mode { + case let .share(_, selfPeer, _): if let selfPeer { self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer) } self.headerNode.mapNode.hasPickerAnnotation = true + case .pick: + self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, location: TelegramMediaMap(coordinate: CLLocationCoordinate2DMake(0, 0)), queryId: nil, resultId: nil, forcedSelection: true) + self.headerNode.mapNode.hasPickerAnnotation = true } self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index cd210579e8..45682f7729 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -155,6 +155,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { case wallpaper case story case addImage + case createSticker } case assets(PHAssetCollection?, AssetsMode) @@ -273,7 +274,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.presentationData = controller.presentationData var assetType: PHAssetMediaType? - if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage].contains(mode) { + if case let .assets(_, mode) = controller.subject, [.wallpaper, .addImage, .createSticker].contains(mode) { assetType = .image } let mediaAssetsContext = MediaAssetsContext(assetType: assetType) @@ -432,7 +433,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.showsVerticalScrollIndicator = false - if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage].contains(mode) { + if case let .assets(_, mode) = controller.subject, [.wallpaper, .story, .addImage, .createSticker].contains(mode) { } else { let selectionGesture = MediaPickerGridSelectionGesture() @@ -1566,7 +1567,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default: + case .default, .createSticker: self.titleView.title = presentationData.strings.MediaPicker_Recents self.titleView.isEnabled = true case .story: @@ -2258,15 +2259,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return self.controllerNode.defaultTransitionView() } - fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { + public func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource) } - fileprivate func transitionImage(for identifier: String) -> UIImage? { + public func transitionImage(for identifier: String) -> UIImage? { return self.controllerNode.transitionImage(for: identifier) } - func updateHiddenMediaId(_ id: String?) { + public func updateHiddenMediaId(_ id: String?) { self.controllerNode.hiddenMediaId.set(.single(id)) } diff --git a/submodules/PremiumUI/Resources/badge b/submodules/PremiumUI/Resources/badge new file mode 100644 index 0000000000..933f6153e3 Binary files /dev/null and b/submodules/PremiumUI/Resources/badge differ diff --git a/submodules/PremiumUI/Resources/badge.scn b/submodules/PremiumUI/Resources/badge.scn deleted file mode 100644 index 6c59e8ffb0..0000000000 Binary files a/submodules/PremiumUI/Resources/badge.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/boost b/submodules/PremiumUI/Resources/boost new file mode 100644 index 0000000000..3c0b0748ef Binary files /dev/null and b/submodules/PremiumUI/Resources/boost differ diff --git a/submodules/PremiumUI/Resources/boost.scn b/submodules/PremiumUI/Resources/boost.scn deleted file mode 100644 index d301679181..0000000000 Binary files a/submodules/PremiumUI/Resources/boost.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/business.png b/submodules/PremiumUI/Resources/business.png new file mode 100644 index 0000000000..ed3acfa1f7 Binary files /dev/null and b/submodules/PremiumUI/Resources/business.png differ diff --git a/submodules/PremiumUI/Resources/business.scn b/submodules/PremiumUI/Resources/business.scn new file mode 100644 index 0000000000..a88303fa79 Binary files /dev/null and b/submodules/PremiumUI/Resources/business.scn differ diff --git a/submodules/PremiumUI/Resources/coin b/submodules/PremiumUI/Resources/coin new file mode 100644 index 0000000000..dbb4f92289 Binary files /dev/null and b/submodules/PremiumUI/Resources/coin differ diff --git a/submodules/PremiumUI/Resources/coin.scn b/submodules/PremiumUI/Resources/coin.scn deleted file mode 100644 index 064fba2a31..0000000000 Binary files a/submodules/PremiumUI/Resources/coin.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/emoji b/submodules/PremiumUI/Resources/emoji new file mode 100644 index 0000000000..6eddddf5e4 Binary files /dev/null and b/submodules/PremiumUI/Resources/emoji differ diff --git a/submodules/PremiumUI/Resources/emoji.scn b/submodules/PremiumUI/Resources/emoji.scn deleted file mode 100644 index f4f25b3ba7..0000000000 Binary files a/submodules/PremiumUI/Resources/emoji.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/gift b/submodules/PremiumUI/Resources/gift new file mode 100644 index 0000000000..87a9c2a5e8 Binary files /dev/null and b/submodules/PremiumUI/Resources/gift differ diff --git a/submodules/PremiumUI/Resources/gift.scn b/submodules/PremiumUI/Resources/gift.scn deleted file mode 100644 index 95125f3de3..0000000000 Binary files a/submodules/PremiumUI/Resources/gift.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/lightspeed b/submodules/PremiumUI/Resources/lightspeed new file mode 100644 index 0000000000..bf04894009 Binary files /dev/null and b/submodules/PremiumUI/Resources/lightspeed differ diff --git a/submodules/PremiumUI/Resources/lightspeed.scn b/submodules/PremiumUI/Resources/lightspeed.scn deleted file mode 100644 index 660d751f6d..0000000000 Binary files a/submodules/PremiumUI/Resources/lightspeed.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star new file mode 100644 index 0000000000..3b27e0220e Binary files /dev/null and b/submodules/PremiumUI/Resources/star differ diff --git a/submodules/PremiumUI/Resources/star.scn b/submodules/PremiumUI/Resources/star.scn deleted file mode 100644 index e5380d2212..0000000000 Binary files a/submodules/PremiumUI/Resources/star.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/swirl b/submodules/PremiumUI/Resources/swirl new file mode 100644 index 0000000000..cd47c82fbb Binary files /dev/null and b/submodules/PremiumUI/Resources/swirl differ diff --git a/submodules/PremiumUI/Resources/swirl.scn b/submodules/PremiumUI/Resources/swirl.scn deleted file mode 100644 index 6238c1d96d..0000000000 Binary files a/submodules/PremiumUI/Resources/swirl.scn and /dev/null differ diff --git a/submodules/PremiumUI/Resources/tag b/submodules/PremiumUI/Resources/tag new file mode 100644 index 0000000000..5c89a52aa1 Binary files /dev/null and b/submodules/PremiumUI/Resources/tag differ diff --git a/submodules/PremiumUI/Resources/tag.scn b/submodules/PremiumUI/Resources/tag.scn deleted file mode 100644 index 03eef5673c..0000000000 Binary files a/submodules/PremiumUI/Resources/tag.scn and /dev/null differ diff --git a/submodules/PremiumUI/Sources/BadgeBusinessView.swift b/submodules/PremiumUI/Sources/BadgeBusinessView.swift new file mode 100644 index 0000000000..1127c15366 --- /dev/null +++ b/submodules/PremiumUI/Sources/BadgeBusinessView.swift @@ -0,0 +1,61 @@ +import Foundation +import UIKit +import SceneKit +import Display +import AppBundle + +private let sceneVersion: Int = 1 + +final class BadgeBusinessView: UIView, PhoneDemoDecorationView { + private let sceneView: SCNView + + private var leftParticles: SCNNode? + private var rightParticles: SCNNode? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) + self.sceneView.backgroundColor = .clear + if let scene = loadCompressedScene(name: "business", version: sceneVersion) { + self.sceneView.scene = scene + } + self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 + + super.init(frame: frame) + + self.alpha = 0.0 + + self.addSubview(self.sceneView) + + self.leftParticles = self.sceneView.scene?.rootNode.childNode(withName: "leftParticles", recursively: false) + self.rightParticles = self.sceneView.scene?.rootNode.childNode(withName: "rightParticles", recursively: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setVisible(_ visible: Bool) { + if visible, let leftParticles = self.leftParticles, let rightParticles = self.rightParticles, leftParticles.parent == nil { + self.sceneView.scene?.rootNode.addChildNode(leftParticles) + self.sceneView.scene?.rootNode.addChildNode(rightParticles) + } + + let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear) + transition.updateAlpha(layer: self.layer, alpha: visible ? 0.5 : 0.0, completion: { [weak self] finished in + if let strongSelf = self, finished && !visible && strongSelf.leftParticles?.parent != nil { + strongSelf.leftParticles?.removeFromParentNode() + strongSelf.rightParticles?.removeFromParentNode() + } + }) + } + + func resetAnimation() { + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.sceneView.frame = CGRect(origin: .zero, size: frame.size) + } +} diff --git a/submodules/PremiumUI/Sources/BadgeStarsView.swift b/submodules/PremiumUI/Sources/BadgeStarsView.swift index f8f80a116d..f1374916e5 100644 --- a/submodules/PremiumUI/Sources/BadgeStarsView.swift +++ b/submodules/PremiumUI/Sources/BadgeStarsView.swift @@ -13,8 +13,8 @@ final class BadgeStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "badge", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "badge", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 @@ -67,8 +67,8 @@ final class EmojiStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "emoji", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "emoji", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 @@ -121,8 +121,8 @@ final class TagStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "tag", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "tag", version: 1) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift index b82af37cfd..fc1216ae68 100644 --- a/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift +++ b/submodules/PremiumUI/Sources/BoostHeaderBackgroundComponent.swift @@ -12,7 +12,7 @@ import TelegramCore import MultilineTextComponent import TelegramPresentationData -private let sceneVersion: Int = 3 +private let sceneVersion: Int = 1 public final class BoostHeaderBackgroundComponent: Component { let isVisible: Bool @@ -58,7 +58,7 @@ public final class BoostHeaderBackgroundComponent: Component { private func setup() { - guard let url = getAppBundle().url(forResource: "boost", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else { + guard let scene = loadCompressedScene(name: "boost", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift index 4f020cfca5..765be52fb3 100644 --- a/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift +++ b/submodules/PremiumUI/Sources/EmojiHeaderComponent.swift @@ -13,8 +13,6 @@ import AnimationCache import MultiAnimationRenderer import EmojiStatusComponent -private let sceneVersion: Int = 3 - class EmojiHeaderComponent: Component { let context: AccountContext let animationCache: AnimationCache diff --git a/submodules/PremiumUI/Sources/FasterStarsView.swift b/submodules/PremiumUI/Sources/FasterStarsView.swift index a96b632a1f..967fc2213d 100644 --- a/submodules/PremiumUI/Sources/FasterStarsView.swift +++ b/submodules/PremiumUI/Sources/FasterStarsView.swift @@ -5,6 +5,8 @@ import Display import AppBundle import LegacyComponents +private let sceneVersion: Int = 1 + final class FasterStarsView: UIView, PhoneDemoDecorationView { private let sceneView: SCNView @@ -13,8 +15,8 @@ final class FasterStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "lightspeed", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "lightspeed", version: sceneVersion) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift index 6a1b879649..771982bd65 100644 --- a/submodules/PremiumUI/Sources/GiftAvatarComponent.swift +++ b/submodules/PremiumUI/Sources/GiftAvatarComponent.swift @@ -14,7 +14,7 @@ import MergedAvatarsNode import MultilineTextComponent import TelegramPresentationData -private let sceneVersion: Int = 3 +private let sceneVersion: Int = 1 final class GiftAvatarComponent: Component { let context: AccountContext @@ -106,7 +106,7 @@ final class GiftAvatarComponent: Component { } private func setup() { - guard let url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else { + guard let scene = loadCompressedScene(name: "gift", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift index 5a918550c9..d65a427ff3 100644 --- a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -371,6 +371,7 @@ final class PhoneDemoComponent: Component { case emoji case hello case tag + case business } enum Model { @@ -547,6 +548,13 @@ final class PhoneDemoComponent: Component { self.decorationView = starsView self.decorationContainerView.addSubview(starsView) } + case .business: + if let _ = self.decorationView as? BadgeBusinessView { + } else { + let starsView = BadgeBusinessView(frame: self.decorationContainerView.bounds) + self.decorationView = starsView + self.decorationContainerView.addSubview(starsView) + } } self.phoneView.setup(context: component.context, videoFile: component.videoFile, position: component.position) diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 1c97beee06..96c2cbfb8f 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -698,7 +698,7 @@ private final class SheetContent: CombinedComponent { isCurrent = mode == .current } case .features: - textString = strings.GroupBoost_AdditionalFeaturesText + textString = isGroup ? strings.GroupBoost_AdditionalFeaturesText : strings.ChannelBoost_AdditionalFeaturesText } let defaultTitle = strings.ChannelBoost_Level("\(level)").string diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift index 86fba672fc..24a3c5ec50 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift @@ -8,7 +8,7 @@ import GZip import AppBundle import LegacyComponents -private let sceneVersion: Int = 1 +private let sceneVersion: Int = 2 private func deg2rad(_ number: Float) -> Float { return number * .pi / 180 @@ -223,24 +223,7 @@ class PremiumCoinComponent: Component { } private func setup() { - let resourceUrl: URL - if let url = getAppBundle().url(forResource: "coin", withExtension: "scn") { - resourceUrl = url - } else { - let fileName = "coin_\(sceneVersion).scn" - let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) - if !FileManager.default.fileExists(atPath: tmpUrl.path) { - guard let url = getAppBundle().url(forResource: "coin", withExtension: ""), - let compressedData = try? Data(contentsOf: url), - let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { - return - } - try? decompressedData.write(to: tmpUrl) - } - resourceUrl = tmpUrl - } - - guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { + guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else { return } @@ -316,8 +299,8 @@ class PremiumCoinComponent: Component { return } - let fromScale: Float = 0.85 - let toScale: Float = 0.9 + let fromScale: Float = 0.9 + let toScale: Float = 1.0 let animation = CABasicAnimation(keyPath: "scale") animation.duration = 2.0 diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 61b0df9f10..5504cdc4d8 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1177,7 +1177,7 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_LastSeenInfo case .messagePrivacy: text = strings.Premium_MessagePrivacyInfo - case .doubleLimits, .stories, .business: + default: text = "" } @@ -1392,6 +1392,14 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case lastSeen case messagePrivacy case business + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 89161fe5e4..d2afc9fc9c 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -533,6 +533,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .messagePrivacy case .business: demoSubject = .business + default: + demoSubject = .doubleLimits } let buttonText: String diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index a8ab697eda..9cb4597682 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -439,6 +439,15 @@ public enum PremiumPerk: CaseIterable { case lastSeen case messagePrivacy case business + case folderTags + + case businessLocation + case businessHours + case businessGreetingMessage + case businessQuickReplies + case businessAwayMessage + case businessChatBots + public static var allCases: [PremiumPerk] { return [ @@ -520,6 +529,8 @@ public enum PremiumPerk: CaseIterable { return "message_privacy" case .business: return "business" + default: + return "" } } @@ -567,6 +578,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_MessagePrivacy case .business: return strings.Premium_Business + default: + return "" } } @@ -614,6 +627,8 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_MessagePrivacyInfo case .business: return strings.Premium_BusinessInfo + default: + return "" } } @@ -661,6 +676,8 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/MessagePrivacy" case .business: return "Premium/Perk/Business" + default: + return "" } } } @@ -1948,75 +1965,77 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ))), action: { [weak state] _ in var demoSubject: PremiumDemoScreen.Subject - switch perk { - case .doubleLimits: - demoSubject = .doubleLimits - case .moreUpload: - demoSubject = .moreUpload - case .fasterDownload: - demoSubject = .fasterDownload - case .voiceToText: - demoSubject = .voiceToText - case .noAds: - demoSubject = .noAds - case .uniqueReactions: - demoSubject = .uniqueReactions - case .premiumStickers: - demoSubject = .premiumStickers - case .advancedChatManagement: - demoSubject = .advancedChatManagement - case .profileBadge: - demoSubject = .profileBadge - case .animatedUserpics: - demoSubject = .animatedUserpics - case .appIcons: - demoSubject = .appIcons - case .animatedEmoji: - demoSubject = .animatedEmoji - case .emojiStatus: - demoSubject = .emojiStatus - case .translation: - demoSubject = .translation - case .stories: - demoSubject = .stories - case .colors: - demoSubject = .colors - let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() - case .wallpapers: - demoSubject = .wallpapers - let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() - case .messageTags: - demoSubject = .messageTags - let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() - case .lastSeen: - demoSubject = .lastSeen - let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() - case .messagePrivacy: - demoSubject = .messagePrivacy - let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() - case .business: - demoSubject = .business + switch perk { + case .doubleLimits: + demoSubject = .doubleLimits + case .moreUpload: + demoSubject = .moreUpload + case .fasterDownload: + demoSubject = .fasterDownload + case .voiceToText: + demoSubject = .voiceToText + case .noAds: + demoSubject = .noAds + case .uniqueReactions: + demoSubject = .uniqueReactions + case .premiumStickers: + demoSubject = .premiumStickers + case .advancedChatManagement: + demoSubject = .advancedChatManagement + case .profileBadge: + demoSubject = .profileBadge + case .animatedUserpics: + demoSubject = .animatedUserpics + case .appIcons: + demoSubject = .appIcons + case .animatedEmoji: + demoSubject = .animatedEmoji + case .emojiStatus: + demoSubject = .emojiStatus + case .translation: + demoSubject = .translation + case .stories: + demoSubject = .stories + case .colors: + demoSubject = .colors + let _ = ApplicationSpecificNotice.setDismissedPremiumColorsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .wallpapers: + demoSubject = .wallpapers + let _ = ApplicationSpecificNotice.setDismissedPremiumWallpapersBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .messageTags: + demoSubject = .messageTags + let _ = ApplicationSpecificNotice.setDismissedMessageTagsBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .lastSeen: + demoSubject = .lastSeen + let _ = ApplicationSpecificNotice.setDismissedLastSeenBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .messagePrivacy: + demoSubject = .messagePrivacy + let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() + case .business: + demoSubject = .business + default: + demoSubject = .doubleLimits + } + + let isPremium = state?.isPremium == true + var dismissImpl: (() -> Void)? + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) + controller.action = { [weak state] in + dismissImpl?() + if state?.isPremium == false { + buy() } - - let isPremium = state?.isPremium == true - var dismissImpl: (() -> Void)? - let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.perks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) - controller.action = { [weak state] in - dismissImpl?() - if state?.isPremium == false { - buy() - } - } - controller.disposed = { - updateIsFocused(false) - } - present(controller) - dismissImpl = { [weak controller] in - controller?.dismiss(animated: true, completion: nil) - } - updateIsFocused(true) - - addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) + } + controller.disposed = { + updateIsFocused(false) + } + present(controller) + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true, completion: nil) + } + updateIsFocused(true) + + addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) } )))) i += 1 @@ -2100,64 +2119,92 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { foregroundColor: .white, iconName: perk.iconName ))), - action: { _ in - switch perk { - case .location: - let _ = (accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in - guard let accountContext else { - return + action: { [weak state] _ in + let isPremium = state?.isPremium == true + if isPremium { + switch perk { + case .location: + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessLocation in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in })) + }) + case .hours: + let _ = (accountContext.engine.data.get( + TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId) + ) + |> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in })) + }) + case .quickReplies: + let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData)) + }) + case .greetings: + let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false)) + }) + case .awayMessages: + let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in + guard let accountContext else { + return + } + push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true)) + }) + case .chatbots: + push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext)) + } + } else { + var demoSubject: PremiumDemoScreen.Subject + switch perk { + case .location: + demoSubject = .businessLocation + case .hours: + demoSubject = .businessHours + case .quickReplies: + demoSubject = .businessQuickReplies + case .greetings: + demoSubject = .businessGreetingMessage + case .awayMessages: + demoSubject = .businessAwayMessage + case .chatbots: + demoSubject = .businessChatBots + } + var dismissImpl: (() -> Void)? + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: [.businessLocation, .businessHours, .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, .businessChatBots], buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) + controller.action = { [weak state] in + dismissImpl?() + if state?.isPremium == false { + buy() } - push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in })) - }) - case .hours: - let _ = (accountContext.engine.data.get( - TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId) - ) - |> deliverOnMainQueue).start(next: { [weak accountContext] businessHours in - guard let accountContext else { - return - } - push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in })) - }) - case .quickReplies: - let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in - guard let accountContext else { - return - } - push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData)) - }) - case .greetings: - let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in - guard let accountContext else { - return - } - push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false)) - }) - case .awayMessages: - let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in - guard let accountContext else { - return - } - push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true)) - }) - case .chatbots: - let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext) - |> take(1) - |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in - guard let accountContext else { - return - } - push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) - }) + } + controller.disposed = { + updateIsFocused(false) + } + present(controller) + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true, completion: nil) + } + updateIsFocused(true) } } )))) @@ -2259,7 +2306,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { iconName: "Premium/BusinessPerk/Tag" ))), action: { _ in - push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, dismissed: nil)) + push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } )))) diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 83ea682faf..3f375d3fd9 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -833,6 +833,129 @@ public class PremiumLimitsListScreen: ViewController { ) ) ) + + + availableItems[.businessLocation] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessLocation, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["business_location"], + decoration: .business + )), + title: strings.Business_Location, + text: strings.Business_LocationInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessHours] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessHours, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["business_hours"], + decoration: .business + )), + title: strings.Business_OpeningHours, + text: strings.Business_OpeningHoursInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessQuickReplies] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessQuickReplies, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["greeting_message"], + decoration: .business + )), + title: strings.Business_QuickReplies, + text: strings.Business_QuickRepliesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessGreetingMessage] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessGreetingMessage, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["greeting_message"], + decoration: .business + )), + title: strings.Business_GreetingMessages, + text: strings.Business_GreetingMessagesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessAwayMessage] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessAwayMessage, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["away_message"], + decoration: .business + )), + title: strings.Business_AwayMessages, + text: strings.Business_AwayMessagesInfo, + textColor: textColor + ) + ) + ) + ) + + availableItems[.businessChatBots] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.businessChatBots, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: context, + position: .top, + model: .island, + videoFile: configuration.videos["business_bots"], + decoration: .business + )), + title: strings.Business_Chatbots, + text: strings.Business_ChatbotsInfo, + textColor: textColor + ) + ) + ) + ) + + if let order = controller.order { var items: [DemoPagerComponent.Item] = order.compactMap { availableItems[$0] } diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 4dc409d480..549f8e605a 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -8,7 +8,7 @@ import GZip import AppBundle import LegacyComponents -private let sceneVersion: Int = 6 +private let sceneVersion: Int = 7 private func deg2rad(_ number: Float) -> Float { return number * .pi / 180 @@ -45,7 +45,31 @@ private func generateDiffuseTexture() -> UIImage { })! } -class PremiumStarComponent: Component { +func loadCompressedScene(name: String, version: Int) -> SCNScene? { + let resourceUrl: URL + if let url = getAppBundle().url(forResource: name, withExtension: "scn") { + resourceUrl = url + } else { + let fileName = "\(name)_\(version).scn" + let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) + if !FileManager.default.fileExists(atPath: tmpUrl.path) { + guard let url = getAppBundle().url(forResource: name, withExtension: ""), + let compressedData = try? Data(contentsOf: url), + let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { + return nil + } + try? decompressedData.write(to: tmpUrl) + } + resourceUrl = tmpUrl + } + + guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { + return nil + } + return scene +} + +final class PremiumStarComponent: Component { let isIntro: Bool let isVisible: Bool let hasIdleAnimations: Bool @@ -251,24 +275,7 @@ class PremiumStarComponent: Component { } private func setup() { - let resourceUrl: URL - if let url = getAppBundle().url(forResource: "star", withExtension: "scn") { - resourceUrl = url - } else { - let fileName = "star_\(sceneVersion).scn" - let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) - if !FileManager.default.fileExists(atPath: tmpUrl.path) { - guard let url = getAppBundle().url(forResource: "star", withExtension: ""), - let compressedData = try? Data(contentsOf: url), - let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { - return - } - try? decompressedData.write(to: tmpUrl) - } - resourceUrl = tmpUrl - } - - guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { + guard let scene = loadCompressedScene(name: "star", version: sceneVersion) else { return } diff --git a/submodules/PremiumUI/Sources/SwirlStarsView.swift b/submodules/PremiumUI/Sources/SwirlStarsView.swift index d7ba345bf7..8c3ce911b0 100644 --- a/submodules/PremiumUI/Sources/SwirlStarsView.swift +++ b/submodules/PremiumUI/Sources/SwirlStarsView.swift @@ -5,6 +5,8 @@ import Display import AppBundle import SwiftSignalKit +private let sceneVersion: Int = 1 + final class SwirlStarsView: UIView, PhoneDemoDecorationView { private let sceneView: SCNView @@ -13,8 +15,8 @@ final class SwirlStarsView: UIView, PhoneDemoDecorationView { override init(frame: CGRect) { self.sceneView = SCNView(frame: CGRect(origin: .zero, size: frame.size)) self.sceneView.backgroundColor = .clear - if let url = getAppBundle().url(forResource: "swirl", withExtension: "scn") { - self.sceneView.scene = try? SCNScene(url: url, options: nil) + if let scene = loadCompressedScene(name: "swirl", version: sceneVersion) { + self.sceneView.scene = scene } self.sceneView.isUserInteractionEnabled = false self.sceneView.preferredFramesPerSecond = 60 diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 5d06521519..2ac21284ac 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -572,6 +572,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 52108d79d1..f9d764597d 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -232,6 +232,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void + public let openStickerEditor: () -> Void public let requestMessageUpdate: (MessageId, Bool) -> Void public let cancelInteractiveKeyboardGestures: () -> Void @@ -355,6 +356,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void, openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void, openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void, + openStickerEditor: @escaping () -> Void, requestMessageUpdate: @escaping (MessageId, Bool) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, dismissTextInput: @escaping () -> Void, @@ -458,6 +460,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openPremiumStatusInfo = openPremiumStatusInfo self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu self.openGroupBoostInfo = openGroupBoostInfo + self.openStickerEditor = openStickerEditor self.requestMessageUpdate = requestMessageUpdate self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 1b55c9e79c..d746fb87a8 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -56,6 +56,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { let dismissTextInput: () -> Void let insertText: (NSAttributedString) -> Void let backwardsDeleteText: () -> Void + let openStickerEditor: () -> Void let presentController: (ViewController, Any?) -> Void let presentGlobalOverlayController: (ViewController, Any?) -> Void let getNavigationController: () -> NavigationController? @@ -72,6 +73,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { dismissTextInput: @escaping () -> Void, insertText: @escaping (NSAttributedString) -> Void, backwardsDeleteText: @escaping () -> Void, + openStickerEditor: @escaping () -> Void, presentController: @escaping (ViewController, Any?) -> Void, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?, @@ -86,6 +88,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.dismissTextInput = dismissTextInput self.insertText = insertText self.backwardsDeleteText = backwardsDeleteText + self.openStickerEditor = openStickerEditor self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController self.getNavigationController = getNavigationController @@ -106,6 +109,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { self.dismissTextInput = chatControllerInteraction.dismissTextInput self.insertText = panelInteraction.insertText self.backwardsDeleteText = panelInteraction.backwardsDeleteText + self.openStickerEditor = chatControllerInteraction.openStickerEditor self.presentController = chatControllerInteraction.presentController self.presentGlobalOverlayController = chatControllerInteraction.presentGlobalOverlayController self.getNavigationController = chatControllerInteraction.navigationController @@ -1140,6 +1144,9 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { return } guard let file = item.itemFile else { + if groupId == AnyHashable("recent"), case .icon(.add) = item.content { + interaction.openStickerEditor() + } return } diff --git a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift index 4cb104ad66..01ac92dbe0 100644 --- a/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/EmptyStateIndicatorComponent/Sources/EmptyStateIndicatorComponent.swift @@ -16,6 +16,8 @@ public final class EmptyStateIndicatorComponent: Component { public let text: String public let actionTitle: String? public let action: () -> Void + public let additionalActionTitle: String? + public let additionalAction: () -> Void public init( context: AccountContext, @@ -24,7 +26,9 @@ public final class EmptyStateIndicatorComponent: Component { title: String, text: String, actionTitle: String?, - action: @escaping () -> Void + action: @escaping () -> Void, + additionalActionTitle: String?, + additionalAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -33,6 +37,8 @@ public final class EmptyStateIndicatorComponent: Component { self.text = text self.actionTitle = actionTitle self.action = action + self.additionalActionTitle = additionalActionTitle + self.additionalAction = additionalAction } public static func ==(lhs: EmptyStateIndicatorComponent, rhs: EmptyStateIndicatorComponent) -> Bool { @@ -54,6 +60,9 @@ public final class EmptyStateIndicatorComponent: Component { if lhs.actionTitle != rhs.actionTitle { return false } + if lhs.additionalActionTitle != rhs.additionalActionTitle { + return false + } return true } @@ -65,6 +74,7 @@ public final class EmptyStateIndicatorComponent: Component { private let title = ComponentView() private let text = ComponentView() private var button: ComponentView? + private var additionalButton: ComponentView? override public init(frame: CGRect) { super.init(frame: frame) @@ -139,7 +149,7 @@ public final class EmptyStateIndicatorComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 240.0, height: 50.0) + containerSize: CGSize(width: 260.0, height: 50.0) ) } else { if let button = self.button { @@ -148,14 +158,52 @@ public final class EmptyStateIndicatorComponent: Component { } } + var additionalButtonSize: CGSize? + if let additionalActionTitle = component.additionalActionTitle { + let additionalButton: ComponentView + if let current = self.additionalButton { + additionalButton = current + } else { + additionalButton = ComponentView() + self.additionalButton = additionalButton + } + + additionalButtonSize = additionalButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text( + text: additionalActionTitle, font: + Font.regular(17.0), + color: component.theme.list.itemAccentColor) + ), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.additionalAction() + } + )), + environment: {}, + containerSize: CGSize(width: 262.0, height: 50.0) + ) + } else { + if let additionalButton = self.additionalButton { + self.additionalButton = nil + additionalButton.view?.removeFromSuperview() + } + } + let animationSpacing: CGFloat = 11.0 let titleSpacing: CGFloat = 17.0 - let buttonSpacing: CGFloat = 17.0 + let buttonSpacing: CGFloat = 21.0 var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height if let buttonSize { totalHeight += buttonSpacing + buttonSize.height } + if let additionalButtonSize { + totalHeight += buttonSpacing + additionalButtonSize.height + } var contentY = floor((availableSize.height - totalHeight) * 0.5) @@ -185,7 +233,14 @@ public final class EmptyStateIndicatorComponent: Component { self.addSubview(buttonView) } transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - buttonSize.width) * 0.5), y: contentY), size: buttonSize)) - contentY += buttonSize.height + contentY += buttonSize.height + buttonSpacing + } + if let additionalButtonSize, let additionalButtonView = self.additionalButton?.view { + if additionalButtonView.superview == nil { + self.addSubview(additionalButtonView) + } + transition.setFrame(view: additionalButtonView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - additionalButtonSize.width) * 0.5), y: contentY), size: additionalButtonSize)) + contentY += additionalButtonSize.height } return availableSize diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 13dc762e1f..7c141f89ee 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2519,6 +2519,7 @@ public final class EmojiPagerContentComponent: Component { case premiumStar case topic(String, Int32) case stop + case add } case animation(EntityKeyboardAnimationData) @@ -3559,6 +3560,15 @@ public final class EmojiPagerContentComponent: Component { let imageSize = image.size.aspectFitted(CGSize(width: size.width - 6.0, height: size.height - 6.0)) image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floor((size.height - imageSize.height) / 2.0)), size: imageSize)) } + case .add: + context.setFillColor(UIColor.black.withAlphaComponent(0.08).cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size).insetBy(dx: 8.0, dy: 8.0)) + context.setFillColor(UIColor.black.withAlphaComponent(0.16).cgColor) + + let plusSize = CGSize(width: 4.5, height: 31.5) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.width) / 2.0), y: floorToScreenPixels((size.height - plusSize.height) / 2.0), width: plusSize.width, height: plusSize.height), cornerRadius: plusSize.width / 2.0).cgPath) + context.addPath(UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - plusSize.height) / 2.0), y: floorToScreenPixels((size.height - plusSize.width) / 2.0), width: plusSize.height, height: plusSize.width), cornerRadius: plusSize.width / 2.0).cgPath) + context.fillPath() } UIGraphicsPopContext() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 7f7ec18d10..4fbf2390c1 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -1784,6 +1784,7 @@ public extension EmojiPagerContentComponent { } if let recentStickers = recentStickers { + let groupId = "recent" for item in recentStickers.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue @@ -1807,7 +1808,6 @@ public extension EmojiPagerContentComponent { tintMode: tintMode ) - let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index c7072d35a6..7768a92c55 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -182,7 +182,11 @@ public final class ListActionItemComponent: Component { let contentRightInset: CGFloat switch component.accessory { case .none: - contentRightInset = 16.0 + if let _ = component.icon { + contentRightInset = 42.0 + } else { + contentRightInset = 16.0 + } case .arrow: contentRightInset = 30.0 case .toggle: @@ -193,7 +197,7 @@ public final class ListActionItemComponent: Component { contentHeight += component.contentInsets.top if component.leftIcon != nil { - contentLeftInset += 46.0 + contentLeftInset += 52.0 } let titleSize = self.title.update( @@ -243,7 +247,7 @@ public final class ListActionItemComponent: Component { var iconOffset: CGFloat = 0.0 if case .none = component.accessory { - iconOffset = 6.0 + iconOffset = 26.0 } let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal index d03c2f7e8f..6559709cc8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDefault.metal @@ -25,8 +25,8 @@ vertex RasterizerData defaultVertexShader(uint vertexID [[vertex_id]], fragment half4 defaultFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]]) { constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); - half3 color = texture.sample(samplr, in.texCoord).rgb; - return half4(color, 1.0); + half4 color = texture.sample(samplr, in.texCoord); + return color; } fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]], @@ -39,12 +39,12 @@ fragment half histogramPrepareFragmentShader(RasterizerData in [[stage_in]], } typedef struct { - float3 topColor; - float3 bottomColor; + float4 topColor; + float4 bottomColor; } GradientColors; fragment half4 gradientFragmentShader(RasterizerData in [[stage_in]], constant GradientColors& colors [[buffer(0)]]) { - return half4(half3(mix(colors.topColor, colors.bottomColor, in.texCoord.y)), 1.0); + return half4(half3(mix(colors.topColor.rgb, colors.bottomColor.rgb, in.texCoord.y)), 1.0); } diff --git a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift similarity index 66% rename from submodules/DrawingUI/Sources/ImageObjectSeparation.swift rename to submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index 764975d4f6..9ecd12429b 100644 --- a/submodules/DrawingUI/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -8,7 +8,7 @@ import VideoToolbox private let queue = Queue() -public func cutoutStickerImage(from image: UIImage) -> Signal { +public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> Signal { if #available(iOS 17.0, *) { guard let cgImage = image.cgImage else { return .single(nil) @@ -23,21 +23,26 @@ public func cutoutStickerImage(from image: UIImage) -> Signal subscriber.putCompletion() return } - let instances = instances(atPoint: nil, inObservation: result) - if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { - let filter = CIFilter.blendWithMask() - filter.inputImage = inputImage - filter.backgroundImage = CIImage(color: .clear) - filter.maskImage = CIImage(cvPixelBuffer: mask) - if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { - let image = UIImage(cgImage: cgImage) - subscriber.putNext(image) - subscriber.putCompletion() - return + if onlyCheck { + subscriber.putNext(UIImage()) + subscriber.putCompletion() + } else { + let instances = instances(atPoint: nil, inObservation: result) + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(image) + subscriber.putCompletion() + return + } } + subscriber.putNext(nil) + subscriber.putCompletion() } - subscriber.putNext(nil) - subscriber.putCompletion() } try? handler.perform([request]) return ActionDisposable { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 6dba11a9ea..fe03b681b0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -94,6 +94,11 @@ public final class MediaEditor { } } + public enum Mode { + case `default` + case sticker + } + public enum Subject { case image(UIImage, PixelDimensions) case video(String, UIImage?, Bool, String?, PixelDimensions, Double) @@ -116,6 +121,7 @@ public final class MediaEditor { } private let context: AccountContext + private let mode: Mode private let subject: Subject private let clock = CMClockGetHostTimeClock() @@ -182,6 +188,9 @@ public final class MediaEditor { } } + public private(set) var canCutout: Bool = false + public var canCutoutUpdated: (Bool) -> Void = { _ in } + private var textureCache: CVMetalTextureCache! public var hasPortraitMask: Bool { @@ -391,8 +400,9 @@ public final class MediaEditor { } } - public init(context: AccountContext, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { + public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) { self.context = context + self.mode = mode self.subject = subject if let values { self.values = values @@ -668,6 +678,19 @@ public final class MediaEditor { } else { textureSource.setMainInput(.image(image)) } + + + if case .sticker = self.mode { + let _ = (cutoutStickerImage(from: image, onlyCheck: true) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, result != nil else { + return + } + self.canCutout = true + self.canCutoutUpdated(true) + }) + } + } if let player, let playerItem = player.currentItem, !textureSourceResult.playerIsReference { textureSource.setMainInput(.video(playerItem)) @@ -677,7 +700,12 @@ public final class MediaEditor { } self.renderer.textureSource = textureSource - self.setGradientColors(textureSourceResult.gradientColors) + switch self.mode { + case .default: + self.setGradientColors(textureSourceResult.gradientColors) + case .sticker: + self.setGradientColors(GradientColors(top: .clear, bottom: .clear)) + } if let _ = textureSourceResult.player { self.updateRenderChain() @@ -1615,7 +1643,7 @@ public final class MediaEditor { public func setGradientColors(_ gradientColors: GradientColors) { self.gradientColorsPromise.set(.single(gradientColors)) - self.updateValues(mode: .skipRendering) { values in + self.updateValues(mode: self.sourceIsVideo ? .skipRendering : .generic) { values in return values.withUpdatedGradientColors(gradientColors: gradientColors.array) } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index eb276b8a08..d97aa11d7d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -178,7 +178,7 @@ final class MediaEditorComposer { } } -public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) { +public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inputImage: UIImage, dimensions: CGSize, outputDimensions: CGSize? = nil, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) { let colorSpace = CGColorSpaceCreateDeviceRGB() let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! var drawingImage: CIImage? @@ -192,7 +192,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp entities.append(contentsOf: composerEntitiesForDrawingEntity(postbox: postbox, textScale: textScale, entity: entity.entity, colorSpace: colorSpace)) } - makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in + makeEditorImageFrameComposition(context: context, inputImage: inputImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: outputDimensions ?? dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in if let ciImage { if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { Queue.mainQueue().async { @@ -206,20 +206,19 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp } private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, outputDimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, textScale: CGFloat = 1.0, completion: @escaping (CIImage?) -> Void) { - var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + var isClear = false + if let gradientColor = values.gradientColors?.first, gradientColor.alpha.isZero { + isClear = true + } + + var resultImage = CIImage(color: isClear ? .clear : .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) - var initialScale: CGFloat - if mediaImage.extent.height > mediaImage.extent.width && values.isStory { - initialScale = max(dimensions.width / mediaImage.extent.width, dimensions.height / mediaImage.extent.height) - } else { - initialScale = dimensions.width / mediaImage.extent.width - } - if values.isStory { resultImage = mediaImage.samplingLinear().composited(over: resultImage) } else { + let initialScale = dimensions.width / mediaImage.extent.width var horizontalScale = initialScale if values.cropMirroring { horizontalScale *= -1.0 diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index cbd042f975..fcf114e4df 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -634,6 +634,10 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: offset, cropRect: self.cropRect, cropScale: scale, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } + public func withUpdatedCropRect(cropRect: CGRect, rotation: CGFloat, mirroring: Bool) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: .zero, cropRect: cropRect, cropScale: 1.0, cropRotation: rotation, cropMirroring: mirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) + } + func withUpdatedGradientColors(gradientColors: [UIColor]) -> MediaEditorValues { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: gradientColors, videoTrimRange: self.videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } @@ -716,6 +720,10 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: self.qualityPreset) } + public func withUpdatedQualityPreset(_ qualityPreset: MediaQualityPreset?) -> MediaEditorValues { + return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, nightTheme: self.nightTheme, drawing: self.drawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, qualityPreset: qualityPreset) + } + public var resultDimensions: PixelDimensions { if self.videoIsFullHd { return PixelDimensions(width: 1080, height: 1920) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 4611bc5f57..54a849f09e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -165,6 +165,7 @@ final class OutputRenderPass: DefaultRenderPass { renderPassDescriptor.colorAttachments[0].texture = (drawable as? CAMetalDrawable)?.texture renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) let drawableSize = renderTarget.drawableSize diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index 737dd4ee67..71f84cdfa1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -128,15 +128,6 @@ final class UniversalTextureSource: TextureSource { self.update() } } -// -// private func setupDisplayLink(frameRate: Int) { -// self.displayLink?.invalidate() -// self.displayLink = nil -// -// if self.playerItemOutput != nil { - -// } -// } } private protocol InputContext { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index 45f313016f..bd96115295 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -233,7 +233,7 @@ final class VideoFinishPass: RenderPass { } private let canvasSize = CGSize(width: 1080.0, height: 1920.0) - private var gradientColors = GradientColors(topColor: simd_float3(0.0, 0.0, 0.0), bottomColor: simd_float3(0.0, 0.0, 0.0)) + private var gradientColors = GradientColors(topColor: simd_float4(0.0, 0.0, 0.0, 0.0), bottomColor: simd_float4(0.0, 0.0, 0.0, 0.0)) func update(values: MediaEditorValues, videoDuration: Double?, additionalVideoDuration: Double?) { let position = CGPoint( x: canvasSize.width / 2.0 + values.cropOffset.x, @@ -241,6 +241,7 @@ final class VideoFinishPass: RenderPass { ) self.isStory = values.isStory + self.isSticker = values.gradientColors?.first?.alpha == 0.0 self.mainPosition = VideoFinishPass.VideoPosition(position: position, size: self.mainPosition.size, scale: values.cropScale, rotation: values.cropRotation, baseScale: self.mainPosition.baseScale) if let position = values.additionalVideoPosition, let scale = values.additionalVideoScale, let rotation = values.additionalVideoRotation { @@ -262,12 +263,12 @@ final class VideoFinishPass: RenderPass { } if let gradientColors = values.gradientColors, let top = gradientColors.first, let bottom = gradientColors.last { - let (topRed, topGreen, topBlue, _) = top.components - let (bottomRed, bottomGreen, bottomBlue, _) = bottom.components + let (topRed, topGreen, topBlue, topAlpha) = top.components + let (bottomRed, bottomGreen, bottomBlue, bottomAlpha) = bottom.components self.gradientColors = GradientColors( - topColor: simd_float3(Float(topRed), Float(topGreen), Float(topBlue)), - bottomColor: simd_float3(Float(bottomRed), Float(bottomGreen), Float(bottomBlue)) + topColor: simd_float4(Float(topRed), Float(topGreen), Float(topBlue), Float(topAlpha)), + bottomColor: simd_float4(Float(bottomRed), Float(bottomGreen), Float(bottomBlue), Float(bottomAlpha)) ) } } @@ -289,6 +290,7 @@ final class VideoFinishPass: RenderPass { ) private var isStory = true + private var isSticker = true private var videoPositionChanges: [VideoPositionChange] = [] private var videoDuration: Double? private var additionalVideoDuration: Double? @@ -482,10 +484,18 @@ final class VideoFinishPass: RenderPass { } let baseScale: CGFloat - if input.height > input.width { - baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height)) + if !self.isSticker { + if input.height > input.width { + baseScale = max(canvasSize.width / CGFloat(input.width), canvasSize.height / CGFloat(input.height)) + } else { + baseScale = canvasSize.width / CGFloat(input.width) + } } else { - baseScale = canvasSize.width / CGFloat(input.width) + if input.height > input.width { + baseScale = canvasSize.width / CGFloat(input.width) + } else { + baseScale = canvasSize.width / CGFloat(input.height) + } } self.mainPosition = self.mainPosition.with(size: CGSize(width: input.width, height: input.height), baseScale: baseScale) @@ -508,9 +518,13 @@ final class VideoFinishPass: RenderPass { let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! - renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + if self.gradientColors.topColor.w > 0.0 { + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + } else { + renderPassDescriptor.colorAttachments[0].loadAction = .clear + } renderPassDescriptor.colorAttachments[0].storeAction = .store - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return input } @@ -521,12 +535,14 @@ final class VideoFinishPass: RenderPass { znear: -1.0, zfar: 1.0) ) - renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!) - self.encodeGradient( - using: renderCommandEncoder, - containerSize: containerSize, - device: device - ) + if self.gradientColors.topColor.w > 0.0 { + renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!) + self.encodeGradient( + using: renderCommandEncoder, + containerSize: containerSize, + device: device + ) + } renderCommandEncoder.setRenderPipelineState(self.mainPipelineState!) @@ -578,8 +594,8 @@ final class VideoFinishPass: RenderPass { } struct GradientColors { - var topColor: simd_float3 - var bottomColor: simd_float3 + var topColor: simd_float4 + var bottomColor: simd_float4 } func encodeGradient( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e0bf2ad253..728aedc52c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent", "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", "//submodules/TelegramUI/Components/MediaScrubberComponent", + "//submodules/Components/BlurredBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift new file mode 100644 index 0000000000..2e411f9cf7 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -0,0 +1,363 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import MultilineTextComponent +import DrawingUI +import MediaEditor +import Photos +import LottieAnimationComponent +import MessageInputPanelComponent + +private final class MediaCutoutScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mediaEditor: MediaEditor + + init( + context: AccountContext, + mediaEditor: MediaEditor + ) { + self.context = context + self.mediaEditor = mediaEditor + } + + static func ==(lhs: MediaCutoutScreenComponent, rhs: MediaCutoutScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + public final class View: UIView { + private let buttonsContainerView = UIView() + private let buttonsBackgroundView = UIView() + private let cancelButton = ComponentView() + private let label = ComponentView() + private let doneButton = ComponentView() + + private var component: MediaCutoutScreenComponent? + private weak var state: State? + private var environment: ViewControllerComponentContainer.Environment? + + override init(frame: CGRect) { + self.buttonsContainerView.clipsToBounds = true + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.addSubview(self.buttonsContainerView) + self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func animateInFromEditor() { + self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + private var animatingOut = false + func animateOutToEditor(completion: @escaping () -> Void) { + self.animatingOut = true + + self.cancelButton.view?.isHidden = true + + self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + completion() + }) + self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + + self.state?.updated() + } + + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + self.component = component + self.state = state + +// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + +// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor + + let buttonSideInset: CGFloat + let buttonBottomInset: CGFloat = 8.0 + var controlsBottomInset: CGFloat = 0.0 + let previewSize: CGSize + var topInset: CGFloat = environment.statusBarHeight + 5.0 + if isTablet { + let previewHeight = availableSize.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + buttonSideInset = 30.0 + } else { + previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) + buttonSideInset = 10.0 + if availableSize.height < previewSize.height + 30.0 { + topInset = 0.0 + controlsBottomInset = -75.0 + } else { + self.buttonsBackgroundView.backgroundColor = .clear + } + } + +// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) + + let cancelButtonSize = self.cancelButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "media_backToCancel", + mode: .animating(loop: false), + range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 33.0, height: 33.0) + ) + ), + action: { + guard let controller = environment.controller() as? MediaCutoutScreen else { + return + } + controller.requestDismiss(reset: true, animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let cancelButtonFrame = CGRect( + origin: CGPoint(x: buttonSideInset, y: buttonBottomInset), + size: cancelButtonSize + ) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.buttonsContainerView.addSubview(cancelButtonView) + } + transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) + } + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent(Text(text: "Tap an object to cut it out", font: Font.regular(17.0), color: .white)), + environment: {}, + containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) + ) + let labelFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: buttonBottomInset + 4.0), + size: labelSize + ) + if let labelView = self.label.view { + if labelView.superview == nil { + self.buttonsContainerView.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: labelFrame) + } + + transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) + transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class MediaCutoutScreen: ViewController { + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { + private weak var controller: MediaCutoutScreen? + private let context: AccountContext + + fileprivate let componentHost: ComponentView + + private var presentationData: PresentationData + private var validLayout: ContainerViewLayout? + + init(controller: MediaCutoutScreen) { + self.controller = controller + self.context = controller.context + + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.componentHost = ComponentView() + + super.init() + + self.backgroundColor = .clear + } + + override func didLoad() { + super.didLoad() + + self.view.disablesInteractiveModalDismiss = true + self.view.disablesInteractiveKeyboardGestureRecognizer = true + } + + @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func animateInFromEditor() { + if let view = self.componentHost.view as? MediaCutoutScreenComponent.View { + view.animateInFromEditor() + } + } + + func animateOutToEditor(completion: @escaping () -> Void) { + if let mediaEditor = self.controller?.mediaEditor { + mediaEditor.play() + } + if let view = self.componentHost.view as? MediaCutoutScreenComponent.View { + view.animateOutToEditor(completion: completion) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + guard let controller = self.controller else { + return + } + let isFirstTime = self.validLayout == nil + self.validLayout = layout + + let isTablet = layout.metrics.isTablet + + let previewSize: CGSize + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0 + if isTablet { + let previewHeight = layout.size.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + } + let bottomInset = layout.size.height - previewSize.height - topInset + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: layout.statusBarHeight ?? 0.0, + navigationHeight: 0.0, + safeInsets: UIEdgeInsets( + top: topInset, + left: layout.safeInsets.left, + bottom: bottomInset, + right: layout.safeInsets.right + ), + inputHeight: layout.inputHeight ?? 0.0, + metrics: layout.metrics, + deviceMetrics: layout.deviceMetrics, + orientation: nil, + isVisible: true, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + + let componentSize = self.componentHost.update( + transition: transition, + component: AnyComponent( + MediaCutoutScreenComponent( + context: self.context, + mediaEditor: controller.mediaEditor + ) + ), + environment: { + environment + }, + forceUpdate: forceUpdate || animateOut, + containerSize: layout.size + ) + if let componentView = self.componentHost.view { + if componentView.superview == nil { + self.view.insertSubview(componentView, at: 3) + componentView.clipsToBounds = true + } + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) + } + + if isFirstTime { + self.animateInFromEditor() + } + } + } + + fileprivate var node: Node { + return self.displayNode as! Node + } + + fileprivate let context: AccountContext + fileprivate let mediaEditor: MediaEditor + + public var dismissed: () -> Void = {} + + private var initialValues: MediaEditorValues + + public init(context: AccountContext, mediaEditor: MediaEditor) { + self.context = context + self.mediaEditor = mediaEditor + self.initialValues = mediaEditor.values.makeCopy() + + super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + self.statusBar.statusBarStyle = .White + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func loadDisplayNode() { + self.displayNode = Node(controller: self) + + super.displayNodeDidLoad() + } + + func requestDismiss(reset: Bool, animated: Bool) { + if reset { + self.mediaEditor.values = self.initialValues + } + + self.dismissed() + + self.node.animateOutToEditor(completion: { + self.dismiss() + }) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 52e1f0ef04..2f078dad8e 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -40,6 +40,7 @@ import TelegramStringFormatting import ForwardInfoPanelComponent import ContextReferenceButtonComponent import MediaScrubberComponent +import BlurredBackgroundComponent private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -78,6 +79,7 @@ final class MediaEditorScreenComponent: Component { let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void + let openCutout: () -> Void init( context: AccountContext, @@ -93,7 +95,8 @@ final class MediaEditorScreenComponent: Component { selectedEntity: DrawingEntity?, entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, openDrawing: @escaping (DrawingScreenType) -> Void, - openTools: @escaping () -> Void + openTools: @escaping () -> Void, + openCutout: @escaping () -> Void ) { self.context = context self.externalState = externalState @@ -109,6 +112,7 @@ final class MediaEditorScreenComponent: Component { self.entityViewForEntity = entityViewForEntity self.openDrawing = openDrawing self.openTools = openTools + self.openCutout = openCutout } static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { @@ -149,6 +153,7 @@ final class MediaEditorScreenComponent: Component { case sticker case tools case done + case cutout } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -165,6 +170,8 @@ final class MediaEditorScreenComponent: Component { image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/AddSticker"), color: .white)! case .tools: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! + case .cutout: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)! case .done: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -254,6 +261,7 @@ final class MediaEditorScreenComponent: Component { private let stickerButton = ComponentView() private let toolsButton = ComponentView() private let doneButton = ComponentView() + private let cutoutButton = ComponentView() private let fadeView = UIButton() @@ -385,6 +393,7 @@ final class MediaEditorScreenComponent: Component { self.inputPanelExternalState.deleteBackward() } }, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self { self.environment?.controller()?.present(c, in: .window(.root), with: a) @@ -580,7 +589,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateOutToTool(transition: Transition) { + func animateOutToTool(inPlace: Bool, transition: Transition) { if let view = self.cancelButton.view { view.alpha = 0.0 } @@ -592,7 +601,9 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if !inPlace { + view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } } @@ -607,7 +618,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateInFromTool(transition: Transition) { + func animateInFromTool(inPlace: Bool, transition: Transition) { if let view = self.cancelButton.view { view.alpha = 1.0 } @@ -622,7 +633,9 @@ final class MediaEditorScreenComponent: Component { ] for button in buttons { if let view = button.view { - view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + if !inPlace { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } } @@ -685,6 +698,7 @@ final class MediaEditorScreenComponent: Component { let openDrawing = component.openDrawing let openTools = component.openTools + let openCutout = component.openCutout let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 @@ -748,33 +762,48 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: cancelButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) } - let doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done : environment.strings.Story_Editor_Next + var doneButtonTitle: String? + var doneButtonIcon: UIImage + switch controller.mode { + case .storyEditor: + doneButtonTitle = isEditingStory ? environment.strings.Story_Editor_Done.uppercased() : environment.strings.Story_Editor_Next.uppercased() + doneButtonIcon = UIImage(bundleImageName: "Media Editor/Next")! + case .stickerEditor: + doneButtonTitle = nil + doneButtonIcon = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Apply"), color: .white)! + } + let doneButtonSize = self.doneButton.update( transition: transition, component: AnyComponent(PlainButtonComponent( content: AnyComponent(DoneButtonContentComponent( backgroundColor: UIColor(rgb: 0x007aff), - icon: UIImage(bundleImageName: "Media Editor/Next")!, - title: doneButtonTitle.uppercased())), + icon: doneButtonIcon, + title: doneButtonTitle)), effectAlignment: .center, action: { [weak self] in guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { return } - guard !controller.node.recording.isActive else { - return - } - guard controller.checkCaptionLimit() else { - return - } - if controller.isEditingStory { - controller.requestCompletion(animated: true) - } else { - if controller.checkIfCompletionIsAllowed() { - controller.openPrivacySettings(completion: { [weak controller] in - controller?.requestCompletion(animated: true) - }) + switch controller.mode { + case .storyEditor: + guard !controller.node.recording.isActive else { + return } + guard controller.checkCaptionLimit() else { + return + } + if controller.isEditingStory { + controller.requestStoryCompletion(animated: true) + } else { + if controller.checkIfCompletionIsAllowed() { + controller.openPrivacySettings(completion: { [weak controller] in + controller?.requestStoryCompletion(animated: true) + }) + } + } + case .stickerEditor: + controller.requestStickerCompletion(animated: true) } } )), @@ -827,7 +856,7 @@ final class MediaEditorScreenComponent: Component { let drawButtonFrame = CGRect( origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 5.0 - drawButtonSize.width / 2.0 - 3.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset + 1.0), size: drawButtonSize - ) + ) if let drawButtonView = self.drawButton.view { if drawButtonView.superview == nil { self.addSubview(drawButtonView) @@ -839,6 +868,7 @@ final class MediaEditorScreenComponent: Component { } } + let textButtonSize = self.textButton.update( transition: transition, component: AnyComponent(Button( @@ -950,6 +980,41 @@ final class MediaEditorScreenComponent: Component { } } + if controller.node.canCutout { + let cutoutButtonSize = self.cutoutButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.cutout), + title: "Cut Out an Object" + )), + effectAlignment: .center, + action: { + openCutout() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let cutoutButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - cutoutButtonSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + controlsBottomInset - cutoutButtonSize.height - 14.0), + size: cutoutButtonSize + ) + if let cutoutButtonView = self.cutoutButton.view { + if cutoutButtonView.superview == nil { + self.addSubview(cutoutButtonView) + + cutoutButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + cutoutButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.0) + cutoutButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + } + transition.setPosition(view: cutoutButtonView, position: cutoutButtonFrame.center) + transition.setBounds(view: cutoutButtonView, bounds: CGRect(origin: .zero, size: cutoutButtonFrame.size)) + transition.setAlpha(view: cutoutButtonView, alpha: buttonsAreHidden ? 0.0 : bottomButtonsAlpha) + } + } + let mediaEditor = controller.node.mediaEditor var timeoutValue: String @@ -1083,224 +1148,6 @@ final class MediaEditorScreenComponent: Component { ) } - let nextInputMode: MessageInputPanelComponent.InputMode - switch self.currentInputMode { - case .text: - nextInputMode = .emoji - case .emoji: - nextInputMode = .text - default: - nextInputMode = .emoji - } - - var canRecordVideo = true - if let subject = controller.node.subject { - if case let .video(_, _, _, additionalPath, _, _, _, _, _) = subject, additionalPath != nil { - canRecordVideo = false - } - } - - self.inputPanel.parentState = state - let inputPanelSize = self.inputPanel.update( - transition: transition, - component: AnyComponent(MessageInputPanelComponent( - externalState: self.inputPanelExternalState, - context: component.context, - theme: environment.theme, - strings: environment.strings, - style: .editor, - placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), - maxLength: Int(component.context.userLimits.maxStoryCaptionLength), - queryTypes: [.mention], - alwaysDarkWhenHasText: false, - resetInputContents: nil, - nextInputMode: { _ in return nextInputMode }, - areVoiceMessagesAvailable: false, - presentController: { [weak controller] c in - guard let controller else { - return - } - controller.present(c, in: .window(.root)) - }, - presentInGlobalOverlay: { [weak controller] c in - guard let controller else { - return - } - controller.presentInGlobalOverlay(c) - }, - sendMessageAction: { [weak self] in - guard let self else { - return - } - self.deactivateInput() - }, - sendMessageOptionsAction: nil, - sendStickerAction: { _ in }, - setMediaRecordingActive: canRecordVideo ? { [weak controller] isActive, _, finished, sourceView in - guard let controller else { - return - } - controller.node.recording.setMediaRecordingActive(isActive, finished: finished, sourceView: sourceView) - } : nil, - lockMediaRecording: { [weak controller, weak self] in - guard let controller, let self else { - return - } - controller.node.recording.isLocked = true - self.state?.updated(transition: .easeInOut(duration: 0.2)) - }, - stopAndPreviewMediaRecording: { [weak controller] in - guard let controller else { - return - } - controller.node.recording.setMediaRecordingActive(false, finished: true, sourceView: nil) - }, - discardMediaRecordingPreview: nil, - attachmentAction: nil, - myReaction: nil, - likeAction: nil, - likeOptionsAction: nil, - inputModeAction: { [weak self] in - if let self { - switch self.currentInputMode { - case .text: - self.currentInputMode = .emoji - case .emoji: - self.currentInputMode = .text - default: - self.currentInputMode = .emoji - } - if self.currentInputMode == .text { - self.activateInput() - } else { - self.state?.updated(transition: .immediate) - } - } - }, - timeoutAction: isEditingStory ? nil : { [weak controller] view, gesture in - guard let controller else { - return - } - controller.presentTimeoutSetup(sourceView: view, gesture: gesture) - }, - forwardAction: nil, - moreAction: nil, - presentVoiceMessagesUnavailableTooltip: nil, - presentTextLengthLimitTooltip: { [weak controller] in - guard let controller else { - return - } - controller.presentCaptionLimitPremiumSuggestion(isPremium: controller.context.isPremium) - }, - presentTextFormattingTooltip: { [weak controller] in - guard let controller else { - return - } - controller.presentCaptionEntitiesPremiumSuggestion() - }, - paste: { [weak self, weak controller] data in - guard let self, let controller else { - return - } - switch data { - case let .sticker(image, _): - if max(image.size.width, image.size.height) > 1.0 { - let entity = DrawingStickerEntity(content: .image(image, .sticker)) - controller.node.interaction?.insertEntity(entity, scale: 1.0) - self.deactivateInput() - } - case let .images(images): - if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { - let entity = DrawingStickerEntity(content: .image(image, .rectangle)) - controller.node.interaction?.insertEntity(entity, scale: 2.5) - self.deactivateInput() - } - case .text: - Queue.mainQueue().after(0.1) { - let text = self.getInputText() - if text.length > component.context.userLimits.maxStoryCaptionLength { - controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) - } - } - default: - break - } - }, - audioRecorder: nil, - videoRecordingStatus: controller.node.recording.status, - isRecordingLocked: controller.node.recording.isLocked, - hasRecordedVideo: mediaEditor?.values.additionalVideoPath != nil, - recordedAudioPreview: nil, - hasRecordedVideoPreview: false, - wasRecordingDismissed: false, - timeoutValue: timeoutValue, - timeoutSelected: false, - displayGradient: false, - bottomInset: 0.0, - isFormattingLocked: !state.isPremium, - hideKeyboard: self.currentInputMode == .emoji, - customInputView: nil, - forceIsEditing: self.currentInputMode == .emoji, - disabledPlaceholder: nil, - header: header, - isChannel: false, - storyItem: nil, - chatLocation: nil - )), - environment: {}, - containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) - ) - - if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { - Queue.mainQueue().justDispatch { - controller.node.entitiesView.selectEntity(nil) - } - } - - if self.inputPanelExternalState.isEditing { - if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { - inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) - } - } - keyboardHeight = inputHeight - - let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) - if self.inputPanelExternalState.isEditing { - fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) - } else { - fadeTransition.setAlpha(view: self.fadeView, alpha: 0.0) - } - transition.setFrame(view: self.fadeView, frame: CGRect(origin: .zero, size: availableSize)) - - let isEditingCaption = self.inputPanelExternalState.isEditing - if self.isEditingCaption != isEditingCaption { - self.isEditingCaption = isEditingCaption - - if isEditingCaption { - controller.dismissAllTooltips() - mediaEditor?.maybePauseVideo() - } else { - mediaEditor?.maybeUnpauseVideo() - } - } - - let inputPanelBackgroundSize = self.inputPanelBackground.update( - transition: transition, - component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 60.0) - ) - if let inputPanelBackgroundView = self.inputPanelBackground.view { - if inputPanelBackgroundView.superview == nil { - self.addSubview(inputPanelBackgroundView) - } - let isVisible = isEditingCaption && inputHeight > 44.0 - transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize)) - if !self.animatingButtons { - transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) - } - } - var isEditingTextEntity = false var sizeSliderVisible = false var sizeValue: CGFloat? @@ -1310,562 +1157,783 @@ final class MediaEditorScreenComponent: Component { sizeValue = textEntity.fontSize } - var inputPanelBottomInset: CGFloat = -controlsBottomInset - if inputHeight > 0.0 { - inputPanelBottomInset = inputHeight - environment.safeInsets.bottom - } - let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) - if let inputPanelView = self.inputPanel.view { - if inputPanelView.superview == nil { - self.addSubview(inputPanelView) - } - transition.setFrame(view: inputPanelView, frame: inputPanelFrame) - transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) - } - - if let playerState = state.playerState { - let scrubberInset: CGFloat = 9.0 - - let minDuration: Double - let maxDuration: Double - if playerState.isAudioOnly { - minDuration = 5.0 - maxDuration = 15.0 - } else { - minDuration = 1.0 - maxDuration = storyMaxVideoDuration + if case .storyEditor = controller.mode { + let nextInputMode: MessageInputPanelComponent.InputMode + switch self.currentInputMode { + case .text: + nextInputMode = .emoji + case .emoji: + nextInputMode = .text + default: + nextInputMode = .emoji } - let previousTrackCount = self.currentVisibleTracks?.count - let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } - self.currentVisibleTracks = visibleTracks - - var scrubberTransition = transition - if let previousTrackCount, previousTrackCount != visibleTracks.count { - scrubberTransition = .easeInOut(duration: 0.2) + var canRecordVideo = true + if let subject = controller.node.subject { + if case let .video(_, _, _, additionalPath, _, _, _, _, _) = subject, additionalPath != nil { + canRecordVideo = false + } } - let isAudioOnly = playerState.isAudioOnly - let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) - - let scrubber: ComponentView - if let current = self.scrubber { - scrubber = current - } else { - scrubber = ComponentView() - self.scrubber = scrubber - } - - let scrubberSize = scrubber.update( - transition: scrubberTransition, - component: AnyComponent(MediaScrubberComponent( + self.inputPanel.parentState = state + let inputPanelSize = self.inputPanel.update( + transition: transition, + component: AnyComponent(MessageInputPanelComponent( + externalState: self.inputPanelExternalState, context: component.context, - style: .editor, theme: environment.theme, - generationTimestamp: playerState.generationTimestamp, - position: playerState.position, - minDuration: minDuration, - maxDuration: maxDuration, - isPlaying: playerState.isPlaying, - tracks: visibleTracks, - positionUpdated: { [weak mediaEditor] position, apply in - if let mediaEditor { - mediaEditor.seek(position, andPlay: apply) - } - }, - trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in - guard let mediaEditor else { + strings: environment.strings, + style: .editor, + placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption), + maxLength: Int(component.context.userLimits.maxStoryCaptionLength), + queryTypes: [.mention], + alwaysDarkWhenHasText: false, + resetInputContents: nil, + nextInputMode: { _ in return nextInputMode }, + areVoiceMessagesAvailable: false, + presentController: { [weak controller] c in + guard let controller else { return } - let trimRange = start.. 1.0 { + let entity = DrawingStickerEntity(content: .image(image, .sticker)) + controller.node.interaction?.insertEntity(entity, scale: 1.0) + self.deactivateInput() + } + case let .images(images): + if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { + let entity = DrawingStickerEntity(content: .image(image, .rectangle)) + controller.node.interaction?.insertEntity(entity, scale: 2.5) + self.deactivateInput() + } + case .text: + Queue.mainQueue().after(0.1) { + let text = self.getInputText() + if text.length > component.context.userLimits.maxStoryCaptionLength { + controller.presentCaptionLimitPremiumSuggestion(isPremium: self.state?.isPremium ?? false) } } - } else if trackId == 1 { - mediaEditor.setAdditionalVideoTrimRange(trimRange, apply: apply) - if hasMainVideoTrack { - if apply { - mediaEditor.play() + default: + break + } + }, + audioRecorder: nil, + videoRecordingStatus: controller.node.recording.status, + isRecordingLocked: controller.node.recording.isLocked, + hasRecordedVideo: mediaEditor?.values.additionalVideoPath != nil, + recordedAudioPreview: nil, + hasRecordedVideoPreview: false, + wasRecordingDismissed: false, + timeoutValue: timeoutValue, + timeoutSelected: false, + displayGradient: false, + bottomInset: 0.0, + isFormattingLocked: !state.isPremium, + hideKeyboard: self.currentInputMode == .emoji, + customInputView: nil, + forceIsEditing: self.currentInputMode == .emoji, + disabledPlaceholder: nil, + header: header, + isChannel: false, + storyItem: nil, + chatLocation: nil + )), + environment: {}, + containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight) + ) + + if self.inputPanelExternalState.isEditing && controller.node.entitiesView.hasSelection { + Queue.mainQueue().justDispatch { + controller.node.entitiesView.selectEntity(nil) + } + } + + if self.inputPanelExternalState.isEditing { + if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) { + inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false)) + } + } + keyboardHeight = inputHeight + + let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) + if self.inputPanelExternalState.isEditing { + fadeTransition.setAlpha(view: self.fadeView, alpha: 1.0) + } else { + fadeTransition.setAlpha(view: self.fadeView, alpha: 0.0) + } + transition.setFrame(view: self.fadeView, frame: CGRect(origin: .zero, size: availableSize)) + + let isEditingCaption = self.inputPanelExternalState.isEditing + if self.isEditingCaption != isEditingCaption { + self.isEditingCaption = isEditingCaption + + if isEditingCaption { + controller.dismissAllTooltips() + mediaEditor?.maybePauseVideo() + } else { + mediaEditor?.maybeUnpauseVideo() + } + } + + let inputPanelBackgroundSize = self.inputPanelBackground.update( + transition: transition, + component: AnyComponent(BlurredGradientComponent(position: .bottom, tag: nil)), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: keyboardHeight + 60.0) + ) + if let inputPanelBackgroundView = self.inputPanelBackground.view { + if inputPanelBackgroundView.superview == nil { + self.addSubview(inputPanelBackgroundView) + } + let isVisible = isEditingCaption && inputHeight > 44.0 + transition.setFrame(view: inputPanelBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: isVisible ? availableSize.height - inputPanelBackgroundSize.height : availableSize.height), size: inputPanelBackgroundSize)) + if !self.animatingButtons { + transition.setAlpha(view: inputPanelBackgroundView, alpha: isVisible ? 1.0 : 0.0, delay: isVisible ? 0.0 : 0.4) + } + } + + var inputPanelBottomInset: CGFloat = -controlsBottomInset + if inputHeight > 0.0 { + inputPanelBottomInset = inputHeight - environment.safeInsets.bottom + } + let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) + if let inputPanelView = self.inputPanel.view { + if inputPanelView.superview == nil { + self.addSubview(inputPanelView) + } + transition.setFrame(view: inputPanelView, frame: inputPanelFrame) + transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) + } + + if let playerState = state.playerState { + let scrubberInset: CGFloat = 9.0 + + let minDuration: Double + let maxDuration: Double + if playerState.isAudioOnly { + minDuration = 5.0 + maxDuration = 15.0 + } else { + minDuration = 1.0 + maxDuration = storyMaxVideoDuration + } + + let previousTrackCount = self.currentVisibleTracks?.count + let visibleTracks = playerState.tracks.filter { $0.visibleInTimeline }.map { MediaScrubberComponent.Track($0) } + self.currentVisibleTracks = visibleTracks + + var scrubberTransition = transition + if let previousTrackCount, previousTrackCount != visibleTracks.count { + scrubberTransition = .easeInOut(duration: 0.2) + } + + let isAudioOnly = playerState.isAudioOnly + let hasMainVideoTrack = playerState.tracks.contains(where: { $0.id == 0 }) + + let scrubber: ComponentView + if let current = self.scrubber { + scrubber = current + } else { + scrubber = ComponentView() + self.scrubber = scrubber + } + + let scrubberSize = scrubber.update( + transition: scrubberTransition, + component: AnyComponent(MediaScrubberComponent( + context: component.context, + style: .editor, + theme: environment.theme, + generationTimestamp: playerState.generationTimestamp, + position: playerState.position, + minDuration: minDuration, + maxDuration: maxDuration, + isPlaying: playerState.isPlaying, + tracks: visibleTracks, + positionUpdated: { [weak mediaEditor] position, apply in + if let mediaEditor { + mediaEditor.seek(position, andPlay: apply) + } + }, + trackTrimUpdated: { [weak mediaEditor] trackId, start, end, updatedEnd, apply in + guard let mediaEditor else { + return + } + let trimRange = start..= upperBound { + start = lowerBound + } else if start < lowerBound { + start = lowerBound + } + } + + mediaEditor.seek(start, andPlay: true) + mediaEditor.play() + } else { + mediaEditor.stop() + } + } + } else if trackId == 1 { + mediaEditor.setAdditionalVideoOffset(offset, apply: apply) + } + }, + trackLongPressed: { [weak self] trackId, sourceView in + guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { + return + } + controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) } - }, - trackOffsetUpdated: { trackId, offset, apply in - guard let mediaEditor else { + )), + environment: {}, + containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) + ) + + let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) + if let scrubberView = scrubber.view { + var animateIn = false + if scrubberView.superview == nil { + animateIn = true + if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { + self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) + } else { + self.addSubview(scrubberView) + } + } + if animateIn { + scrubberView.frame = scrubberFrame + } else { + scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) + } + if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { + transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) + } else if animateIn { + scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) + } + } + } else { + if let scrubber = self.scrubber { + self.scrubber = nil + if let scrubberView = scrubber.view { + scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + scrubberView.removeFromSuperview() + }) + scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) + } + } + } + + let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) + + let saveContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + saveContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storysave", + mode: .still(position: .begin), + range: nil + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(saveButtonTag) + ) + ) + } else { + saveContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/SaveIcon", + tintColor: nil + ) + ) + ) + } + + let saveButtonSize = self.saveButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: saveContentComponent, + action: { [weak self] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { return } - if trackId == 2 { - mediaEditor.setAudioTrackOffset(offset, apply: apply) - if isAudioOnly { - let offset = (mediaEditor.values.audioTrackOffset ?? 0.0) - let start = (mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0) - if apply { - mediaEditor.seek(offset + start, andPlay: true) - } else { - mediaEditor.seek(offset + start, andPlay: false) - mediaEditor.stop() - } - } else { - if apply { - let audioStart = mediaEditor.values.audioTrackTrimRange?.lowerBound ?? 0.0 - let audioOffset = min(0.0, mediaEditor.values.audioTrackOffset ?? 0.0) + guard !controller.node.recording.isActive else { + return + } + if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { + view.playOnce() + } + controller.requestSave() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let saveButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: saveButtonSize + ) + if let saveButtonView = self.saveButton.view { + if saveButtonView.superview == nil { + setupButtonShadow(saveButtonView) + self.addSubview(saveButtonView) + } + + let saveButtonAlpha = component.isSavingAvailable ? topButtonsAlpha : 0.3 + saveButtonView.isUserInteractionEnabled = component.isSavingAvailable + + transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) + transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) + transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) + } + + var topButtonOffsetX: CGFloat = 0.0 + + if let subject = controller.node.subject, case .message = subject { + let isNightTheme = mediaEditor?.values.nightTheme == true + + let dayNightContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + dayNightContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: isNightTheme ? "anim_sun" : "anim_sun_reverse", + mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(dayNightButtonTag) + ) + ) + } else { + dayNightContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let dayNightButtonSize = self.dayNightButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: dayNightContentComponent, + action: { [weak self, weak state, weak mediaEditor] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } + + if let mediaEditor { + state?.dayNightDidChange = true + + if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) { + controller.node.previewContainerView.addSubview(snapshotView) - var start = -audioOffset + audioStart - if let duration = mediaEditor.duration { - let lowerBound = mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0 - let upperBound = mediaEditor.values.videoTrimRange?.upperBound ?? duration - if start >= upperBound { - start = lowerBound - } else if start < lowerBound { - start = lowerBound + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + + Queue.mainQueue().after(0.1) { + mediaEditor.toggleNightTheme() + controller.node.entitiesView.eachView { view in + if let stickerEntityView = view as? DrawingStickerEntityView { + stickerEntityView.isNightTheme = mediaEditor.values.nightTheme } } - - mediaEditor.seek(start, andPlay: true) - mediaEditor.play() - } else { - mediaEditor.stop() } } - } else if trackId == 1 { - mediaEditor.setAdditionalVideoOffset(offset, apply: apply) } - }, - trackLongPressed: { [weak self] trackId, sourceView in - guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { - return - } - controller.node.presentTrackOptions(trackId: trackId, sourceView: sourceView) - } - )), - environment: {}, - containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height) - ) - - let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0), size: scrubberSize) - if let scrubberView = scrubber.view { - var animateIn = false - if scrubberView.superview == nil { - animateIn = true - if let inputPanelBackgroundView = self.inputPanelBackground.view, inputPanelBackgroundView.superview != nil { - self.insertSubview(scrubberView, belowSubview: inputPanelBackgroundView) - } else { - self.addSubview(scrubberView) + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let dayNightButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: dayNightButtonSize + ) + if let dayNightButtonView = self.dayNightButton.view { + if dayNightButtonView.superview == nil { + setupButtonShadow(dayNightButtonView) + self.addSubview(dayNightButtonView) + + dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) } + transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center) + transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size)) + transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) } - if animateIn { - scrubberView.frame = scrubberFrame - } else { - scrubberTransition.setFrame(view: scrubberView, frame: scrubberFrame) - } - if !self.animatingButtons && !(!hasMainVideoTrack && animateIn) { - transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities || isEditingCaption || isRecordingAdditionalVideo || isEditingTextEntity ? 0.0 : 1.0) - } else if animateIn { - scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) - } - } - } else { - if let scrubber = self.scrubber { - self.scrubber = nil - if let scrubberView = scrubber.view { - scrubberView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - scrubberView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in - scrubberView.removeFromSuperview() + + topButtonOffsetX += 50.0 + } else { + if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil { + dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in + dayNightButtonView?.removeFromSuperview() }) - scrubberView.layer.animateScale(from: 1.0, to: 0.6, duration: 0.2, removeOnCompletion: false) + dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } } - } - - let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) - let saveContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - saveContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storysave", - mode: .still(position: .begin), - range: nil - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(saveButtonTag) - ) - ) - } else { - saveContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/SaveIcon", - tintColor: nil - ) - ) - ) - } - - let saveButtonSize = self.saveButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: saveContentComponent, - action: { [weak self] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { - view.playOnce() - } - controller.requestSave() - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let saveButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - saveButtonSize.width, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: saveButtonSize - ) - if let saveButtonView = self.saveButton.view { - if saveButtonView.superview == nil { - setupButtonShadow(saveButtonView) - self.addSubview(saveButtonView) - } - - let saveButtonAlpha = component.isSavingAvailable ? topButtonsAlpha : 0.3 - saveButtonView.isUserInteractionEnabled = component.isSavingAvailable - - transition.setPosition(view: saveButtonView, position: saveButtonFrame.center) - transition.setBounds(view: saveButtonView, bounds: CGRect(origin: .zero, size: saveButtonFrame.size)) - transition.setScale(view: saveButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: saveButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? saveButtonAlpha : 0.0) - } - - var topButtonOffsetX: CGFloat = 0.0 - - if let subject = controller.node.subject, case .message = subject { - let isNightTheme = mediaEditor?.values.nightTheme == true - - let dayNightContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - dayNightContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: isNightTheme ? "anim_sun" : "anim_sun_reverse", - mode: state.dayNightDidChange ? .animating(loop: false) : .still(position: .end) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(dayNightButtonTag) - ) - ) - } else { - dayNightContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/MuteIcon", - tintColor: nil + if let playerState = state.playerState, playerState.hasAudio { + let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false + + let muteContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + muteContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storymute", + mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), + range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(muteButtonTag) ) ) - ) - } - - let dayNightButtonSize = self.dayNightButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: dayNightContentComponent, - action: { [weak self, weak state, weak mediaEditor] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - - if let mediaEditor { - state?.dayNightDidChange = true + } else { + muteContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let muteButtonSize = self.muteButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: muteContentComponent, + action: { [weak self, weak state, weak mediaEditor] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } - if let snapshotView = controller.node.previewContainerView.snapshotView(afterScreenUpdates: false) { - controller.node.previewContainerView.addSubview(snapshotView) + if let mediaEditor { + state?.muteDidChange = true + let isMuted = !mediaEditor.values.videoIsMuted + mediaEditor.setVideoIsMuted(isMuted) + state?.updated() - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, delay: 0.1, removeOnCompletion: false, completion: { _ in - snapshotView.removeFromSuperview() - }) + controller.node.presentMutedTooltip() } + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let muteButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: muteButtonSize + ) + if let muteButtonView = self.muteButton.view { + if muteButtonView.superview == nil { + setupButtonShadow(muteButtonView) + self.addSubview(muteButtonView) + + muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) + transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) + transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { + muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in + muteButtonView?.removeFromSuperview() + }) + muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + if let playerState = state.playerState { + let playbackContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + playbackContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( + LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: "anim_storyplayback", + mode: state.playbackDidChange ? .animating(loop: false) : .still(position: .end), + range: playerState.isPlaying ? (0.5, 1.0) : (0.0, 0.5) + ), + colors: ["__allcolors__": .white], + size: CGSize(width: 30.0, height: 30.0) + ).tagged(playbackButtonTag) + ) + ) + } else { + playbackContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: playerState.isPlaying ? "Media Editor/Pause" : "Media Editor/Play", + tintColor: nil + ) + ) + ) + } + + let playbackButtonSize = self.playbackButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: playbackContentComponent, + action: { [weak self, weak mediaEditor, weak state] in + guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { + return + } + guard !controller.node.recording.isActive else { + return + } + if let mediaEditor { + state?.playbackDidChange = true + mediaEditor.togglePlayback() + } + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let playbackButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - 20.0 - playbackButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), + size: playbackButtonSize + ) + if let playbackButtonView = self.playbackButton.view { + if playbackButtonView.superview == nil { + setupButtonShadow(playbackButtonView) + self.addSubview(playbackButtonView) + + playbackButtonView.layer.animateAlpha(from: 0.0, to: playbackButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) + playbackButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + } + transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) + transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) + transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) + transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + } + + topButtonOffsetX += 50.0 + } else { + if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { + playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in + playbackButtonView?.removeFromSuperview() + }) + playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + } + } + + let switchCameraButtonSize = self.switchCameraButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent( + FlipButtonContentComponent(tag: switchCameraButtonTag) + ), + action: { [weak self] in + if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen { + controller.node.recording.togglePosition() - Queue.mainQueue().after(0.1) { - mediaEditor.toggleNightTheme() - controller.node.entitiesView.eachView { view in - if let stickerEntityView = view as? DrawingStickerEntityView { - stickerEntityView.isNightTheme = mediaEditor.values.nightTheme - } - } + if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { + view.playAnimation() } } } - )), + ).withIsExclusive(false)), environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) + containerSize: CGSize(width: 48.0, height: 48.0) ) - let dayNightButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - dayNightButtonSize.width - 50.0, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: dayNightButtonSize + let switchCameraButtonFrame = CGRect( + origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), + size: switchCameraButtonSize ) - if let dayNightButtonView = self.dayNightButton.view { - if dayNightButtonView.superview == nil { - setupButtonShadow(dayNightButtonView) - self.addSubview(dayNightButtonView) - - dayNightButtonView.layer.animateAlpha(from: 0.0, to: dayNightButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - dayNightButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) + if let switchCameraButtonView = self.switchCameraButton.view { + if switchCameraButtonView.superview == nil { + self.addSubview(switchCameraButtonView) } - transition.setPosition(view: dayNightButtonView, position: dayNightButtonFrame.center) - transition.setBounds(view: dayNightButtonView, bounds: CGRect(origin: .zero, size: dayNightButtonFrame.size)) - transition.setScale(view: dayNightButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: dayNightButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) + transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) + transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) + transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - topButtonOffsetX += 50.0 - } else { - if let dayNightButtonView = self.dayNightButton.view, dayNightButtonView.superview != nil { - dayNightButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak dayNightButtonView] _ in - dayNightButtonView?.removeFromSuperview() - }) - dayNightButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - if let playerState = state.playerState, playerState.hasAudio { - let isVideoMuted = mediaEditor?.values.videoIsMuted ?? false - - let muteContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - muteContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storymute", - mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), - range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(muteButtonTag) - ) - ) - } else { - muteContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: "Media Editor/MuteIcon", - tintColor: nil - ) - ) - ) - } - - let muteButtonSize = self.muteButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: muteContentComponent, - action: { [weak self, weak state, weak mediaEditor] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - - if let mediaEditor { - state?.muteDidChange = true - let isMuted = !mediaEditor.values.videoIsMuted - mediaEditor.setVideoIsMuted(isMuted) - state?.updated() - - controller.node.presentMutedTooltip() - } - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let muteButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - muteButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: muteButtonSize - ) - if let muteButtonView = self.muteButton.view { - if muteButtonView.superview == nil { - setupButtonShadow(muteButtonView) - self.addSubview(muteButtonView) - - muteButtonView.layer.animateAlpha(from: 0.0, to: muteButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - muteButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) - } - transition.setPosition(view: muteButtonView, position: muteButtonFrame.center) - transition.setBounds(view: muteButtonView, bounds: CGRect(origin: .zero, size: muteButtonFrame.size)) - transition.setScale(view: muteButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: muteButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) - } - - topButtonOffsetX += 50.0 - } else { - if let muteButtonView = self.muteButton.view, muteButtonView.superview != nil { - muteButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak muteButtonView] _ in - muteButtonView?.removeFromSuperview() - }) - muteButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - if let playerState = state.playerState { - let playbackContentComponent: AnyComponentWithIdentity - if component.hasAppeared { - playbackContentComponent = AnyComponentWithIdentity( - id: "animatedIcon", - component: AnyComponent( - LottieAnimationComponent( - animation: LottieAnimationComponent.AnimationItem( - name: "anim_storyplayback", - mode: state.playbackDidChange ? .animating(loop: false) : .still(position: .end), - range: playerState.isPlaying ? (0.5, 1.0) : (0.0, 0.5) - ), - colors: ["__allcolors__": .white], - size: CGSize(width: 30.0, height: 30.0) - ).tagged(playbackButtonTag) - ) - ) - } else { - playbackContentComponent = AnyComponentWithIdentity( - id: "staticIcon", - component: AnyComponent( - BundleIconComponent( - name: playerState.isPlaying ? "Media Editor/Pause" : "Media Editor/Play", - tintColor: nil - ) - ) - ) - } - - let playbackButtonSize = self.playbackButton.update( - transition: transition, - component: AnyComponent(CameraButton( - content: playbackContentComponent, - action: { [weak self, weak mediaEditor, weak state] in - guard let environment = self?.environment, let controller = environment.controller() as? MediaEditorScreen else { - return - } - guard !controller.node.recording.isActive else { - return - } - if let mediaEditor { - state?.playbackDidChange = true - mediaEditor.togglePlayback() - } - } - )), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let playbackButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - 20.0 - playbackButtonSize.width - 50.0 - topButtonOffsetX, y: max(environment.statusBarHeight + 10.0, environment.safeInsets.top + 20.0)), - size: playbackButtonSize - ) - if let playbackButtonView = self.playbackButton.view { - if playbackButtonView.superview == nil { - setupButtonShadow(playbackButtonView) - self.addSubview(playbackButtonView) - - playbackButtonView.layer.animateAlpha(from: 0.0, to: playbackButtonView.alpha, duration: self.animatingButtons ? 0.1 : 0.2) - playbackButtonView.layer.animateScale(from: 0.4, to: 1.0, duration: self.animatingButtons ? 0.1 : 0.2) - } - transition.setPosition(view: playbackButtonView, position: playbackButtonFrame.center) - transition.setBounds(view: playbackButtonView, bounds: CGRect(origin: .zero, size: playbackButtonFrame.size)) - transition.setScale(view: playbackButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: playbackButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? topButtonsAlpha : 0.0) - } - - topButtonOffsetX += 50.0 - } else { - if let playbackButtonView = self.playbackButton.view, playbackButtonView.superview != nil { - playbackButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak playbackButtonView] _ in - playbackButtonView?.removeFromSuperview() - }) - playbackButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) - } - } - - let switchCameraButtonSize = self.switchCameraButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - FlipButtonContentComponent(tag: switchCameraButtonTag) - ), - action: { [weak self] in - if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen { - controller.node.recording.togglePosition() - - if let view = self.switchCameraButton.findTaggedView(tag: switchCameraButtonTag) as? FlipButtonContentComponent.View { - view.playAnimation() - } - } - } - ).withIsExclusive(false)), - environment: {}, - containerSize: CGSize(width: 48.0, height: 48.0) - ) - let switchCameraButtonFrame = CGRect( - origin: CGPoint(x: 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - switchCameraButtonSize.height - 3.0)), - size: switchCameraButtonSize - ) - if let switchCameraButtonView = self.switchCameraButton.view { - if switchCameraButtonView.superview == nil { - self.addSubview(switchCameraButtonView) - } - transition.setPosition(view: switchCameraButtonView, position: switchCameraButtonFrame.center) - transition.setBounds(view: switchCameraButtonView, bounds: CGRect(origin: .zero, size: switchCameraButtonFrame.size)) - transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) - transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } let textCancelButtonSize = self.textCancelButton.update( @@ -1977,6 +2045,11 @@ let storyDimensions = CGSize(width: 1080.0, height: 1920.0) let storyMaxVideoDuration: Double = 60.0 public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate { + public enum Mode { + case storyEditor + case stickerEditor + } + public enum TransitionIn { public final class GalleryTransitionIn { public weak var sourceView: UIView? @@ -2057,6 +2130,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private let gradientView: UIImageView private var gradientColorsDisposable: Disposable? + private let stickerTransparentView: UIImageView + fileprivate let entitiesContainerView: UIView let entitiesView: DrawingEntitiesView fileprivate let selectionContainerView: DrawingSelectionContainerView @@ -2085,6 +2160,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissed = false private var isDismissBySwipeSuppressed = false + fileprivate var canCutout = false + private (set) var hasAnyChanges = false private var playbackPositionDisposable: Disposable? @@ -2122,9 +2199,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.gradientView = UIImageView() + self.stickerTransparentView = UIImageView() + self.stickerTransparentView.clipsToBounds = true self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) - self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true) + self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: controller.mode == .stickerEditor) self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } @@ -2132,6 +2211,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return UIEdgeInsets(top: 160.0, left: 36.0, bottom: storyDimensions.height - 160.0, right: storyDimensions.width - 36.0) } self.previewView = MediaEditorPreviewView(frame: .zero) + if case .stickerEditor = controller.mode { + self.previewView.isOpaque = false + self.previewView.backgroundColor = .clear + } self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false @@ -2147,7 +2230,31 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.containerView) self.containerView.addSubview(self.previewContainerView) - self.previewContainerView.addSubview(self.gradientView) + + if case .stickerEditor = controller.mode { + let rowsCount = 40 + self.stickerTransparentView.image = generateImage(CGSize(width: rowsCount, height: rowsCount), opaque: true, scale: 1.0, rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor(rgb: 0x2b2b2d).cgColor) + + for row in 0 ..< rowsCount { + for column in 0 ..< rowsCount { + if (row + column).isMultiple(of: 2) { + context.addRect(CGRect(x: column, y: row, width: 1, height: 1)) + } + } + } + context.fillPath() + }) + self.stickerTransparentView.layer.magnificationFilter = .nearest + self.stickerTransparentView.layer.shouldRasterize = true + self.stickerTransparentView.layer.rasterizationScale = UIScreenScale + self.previewContainerView.addSubview(self.stickerTransparentView) + } else { + self.previewContainerView.addSubview(self.gradientView) + } + self.previewContainerView.addSubview(self.previewView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) @@ -2275,8 +2382,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setup(with subject: MediaEditorScreen.Subject) { - self.actualSubject = subject + 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 { @@ -2288,10 +2399,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.subject = effectiveSubject - guard let controller = self.controller else { - return - } - Queue.mainQueue().justDispatch { controller.setupAudioSessionIfNeeded() } @@ -2321,10 +2428,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide)) let mediaEntity = DrawingMediaEntity(size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) - if fittedSize.height > fittedSize.width { - mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) - } else { - mediaEntity.scale = storyDimensions.width / fittedSize.width + switch controller.mode { + case .storyEditor: + if fittedSize.height > fittedSize.width { + mediaEntity.scale = max(storyDimensions.width / fittedSize.width, storyDimensions.height / fittedSize.height) + } else { + mediaEntity.scale = storyDimensions.width / fittedSize.width + } + case .stickerEditor: + if fittedSize.height > fittedSize.width { + mediaEntity.scale = storyDimensions.width / fittedSize.width + } else { + mediaEntity.scale = storyDimensions.width / fittedSize.height + } } let initialPosition = mediaEntity.position @@ -2370,7 +2486,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - let mediaEditor = MediaEditor(context: self.context, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) + let mediaEditor = MediaEditor(context: self.context, mode: controller.mode == .stickerEditor ? .sticker : .default, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true) if let initialVideoPosition = controller.initialVideoPosition { mediaEditor.seek(initialVideoPosition, andPlay: true) } @@ -2389,8 +2505,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } + } + mediaEditor.canCutoutUpdated = { [weak self] canCutout in + guard let self, let controller = self.controller else { + return + } + self.canCutout = canCutout + controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } + if case .message = effectiveSubject { } else { self.readyValue.set(.single(true)) @@ -2765,6 +2889,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate hasSwipeToDismiss = true } } + + var hasSwipeToEnhance = true + if case .stickerEditor = controller.mode { + hasSwipeToDismiss = false + hasSwipeToEnhance = false + } let translation = gestureRecognizer.translation(in: self.view) let velocity = gestureRecognizer.velocity(in: self.view) @@ -2776,7 +2906,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isDismissBySwipeSuppressed = controller.isEligibleForDraft() controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } - } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing && self.canEnhance { + } else if abs(translation.x) > 10.0 && !self.isDismissing && !self.isEnhancing && self.canEnhance && hasSwipeToEnhance { self.isEnhancing = true controller.requestLayout(transition: .animated(duration: 0.3, curve: .easeInOut)) } @@ -2855,7 +2985,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } @objc func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard !self.recording.isActive else { + guard !self.recording.isActive, let controller = self.controller else { return } let location = gestureRecognizer.location(in: self.view) @@ -2872,7 +3002,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let layout = self.validLayout, (layout.inputHeight ?? 0.0) > 0.0 { self.view.endEditing(true) } else { - self.insertTextEntity() + if case .storyEditor = controller.mode { + self.insertTextEntity() + } } } } @@ -2900,16 +3032,29 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func setupTransitionImage(_ image: UIImage) { + guard let controller = self.controller else { + return + } self.previewContainerView.alpha = 1.0 let transitionInView = UIImageView(image: image) transitionInView.contentMode = .scaleAspectFill var initialScale: CGFloat - if image.size.height > image.size.width { - initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) - } else { - initialScale = self.previewContainerView.bounds.width / image.size.width + switch controller.mode { + case .storyEditor: + if image.size.height > image.size.width { + initialScale = max(self.previewContainerView.bounds.width / image.size.width, self.previewContainerView.bounds.height / image.size.height) + } else { + initialScale = self.previewContainerView.bounds.width / image.size.width + } + case .stickerEditor: + if image.size.height > image.size.width { + initialScale = self.previewContainerView.bounds.width / image.size.width + } else { + initialScale = self.previewContainerView.bounds.width / image.size.height + } } + transitionInView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) transitionInView.transform = CGAffineTransformMakeScale(initialScale, initialScale) self.previewContainerView.addSubview(transitionInView) @@ -3199,22 +3344,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func animateOutToTool() { + func animateOutToTool(inPlace: Bool = false) { self.isDisplayingTool = true let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToTool(transition: transition) + view.animateOutToTool(inPlace: inPlace, transition: transition) } self.requestUpdate(transition: transition) } - func animateInFromTool() { + func animateInFromTool(inPlace: Bool = false) { self.isDisplayingTool = false let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromTool(transition: transition) + view.animateInFromTool(inPlace: inPlace, transition: transition) } self.requestUpdate(transition: transition) } @@ -3378,7 +3523,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let entity = DrawingStickerEntity(content: .image(updatedImage, .rectangle)) entity.canCutOut = false - let _ = (cutoutStickerImage(from: image) + let _ = (cutoutStickerImage(from: image, onlyCheck: true) |> deliverOnMainQueue).start(next: { [weak entity] result in if result != nil, let entity { entity.canCutOut = true @@ -4079,6 +4224,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .window(.root)) self.animateOutToTool() } + }, + openCutout: { [weak self] in + if let self, let mediaEditor = self.mediaEditor { + if self.entitiesView.hasSelection { + self.entitiesView.selectEntity(nil) + } + + let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor) + controller.dismissed = { [weak self] in + if let self { + self.animateInFromTool(inPlace: true) + } + } + self.controller?.present(controller, in: .window(.root)) + self.animateOutToTool(inPlace: true) + } } ) ), @@ -4172,6 +4333,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + let stickerFrameWidth = floor(previewSize.width * 0.97) + transition.setFrame(view: self.stickerTransparentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth))) + self.stickerTransparentView.layer.cornerRadius = stickerFrameWidth / 8.0 + self.interaction?.containerLayoutUpdated(layout: layout, transition: transition) var layout = layout @@ -4292,6 +4457,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let context: AccountContext + let mode: Mode let subject: Signal let isEditingStory: Bool fileprivate let customTarget: EnginePeer.Id? @@ -4322,6 +4488,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate public init( context: AccountContext, + mode: Mode, subject: Signal, customTarget: EnginePeer.Id? = nil, isEditing: Bool = false, @@ -4335,6 +4502,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate completion: @escaping (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void ) { self.context = context + self.mode = mode self.subject = subject self.customTarget = customTarget self.isEditingStory = isEditing @@ -5012,7 +5180,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private var didComplete = false - func requestCompletion(animated: Bool) { + func requestStoryCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { return } @@ -5364,6 +5532,55 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } + func requestStickerCompletion(animated: Bool) { + guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { + return + } + + self.didComplete = true + + self.dismissAllTooltips() + + mediaEditor.stop() + mediaEditor.invalidate() + self.node.entitiesView.invalidate() + + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } + let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + + if let image = mediaEditor.resultImage { + makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in + if let self, let resultImage { + Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") + + let scaledImage = generateImage(CGSize(width: 512, height: 512), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.addPath(CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: size.width / 8.0, cornerHeight: size.width / 8.0, transform: nil)) + context.clip() + + let scaledSize = resultImage.size.aspectFilled(size) + context.draw(resultImage.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - scaledSize.width) / 2.0), y: floor((size.height - scaledSize.height) / 2.0)), size: scaledSize)) + }, opaque: false, scale: 1.0)! + + self.completion(MediaEditorScreen.Result(media: .image(image: scaledImage, dimensions: PixelDimensions(scaledImage.size)), mediaAreas: [], caption: NSAttributedString(), options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), stickers: [], randomId: 0), { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + } + }) + } + } + private var videoExport: MediaEditorVideoExport? private var exportDisposable = MetaDisposable() @@ -5615,7 +5832,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } -final class DoneButtonContentComponent: CombinedComponent { +private final class DoneButtonContentComponent: CombinedComponent { let backgroundColor: UIColor let icon: UIImage let title: String? @@ -5646,8 +5863,9 @@ final class DoneButtonContentComponent: CombinedComponent { let text = Child(Text.self) return { context in + let iconSize = context.component.icon.size let icon = icon.update( - component: Image(image: context.component.icon, tintColor: .white, size: CGSize(width: 10.0, height: 16.0)), + component: Image(image: context.component.icon, tintColor: .white, size: iconSize), availableSize: CGSize(width: 180.0, height: 100.0), transition: .immediate ) @@ -5705,6 +5923,91 @@ final class DoneButtonContentComponent: CombinedComponent { } } +private final class CutoutButtonContentComponent: CombinedComponent { + let backgroundColor: UIColor + let icon: UIImage + let title: String? + + init( + backgroundColor: UIColor, + icon: UIImage, + title: String? + ) { + self.backgroundColor = backgroundColor + self.icon = icon + self.title = title + } + + static func ==(lhs: CutoutButtonContentComponent, rhs: CutoutButtonContentComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.title != rhs.title { + return false + } + return true + } + + static var body: Body { + let background = Child(BlurredBackgroundComponent.self) + let icon = Child(Image.self) + let text = Child(Text.self) + + return { context in + let iconSize = context.component.icon.size + let icon = icon.update( + component: Image(image: context.component.icon, tintColor: .white, size: iconSize), + availableSize: CGSize(width: 180.0, height: 40.0), + transition: .immediate + ) + + let backgroundHeight: CGFloat = 40.0 + var backgroundSize = CGSize(width: backgroundHeight, height: backgroundHeight) + + let textSpacing: CGFloat = 8.0 + + var title: _UpdatedChildComponent? + if let titleText = context.component.title { + title = text.update( + component: Text( + text: titleText, + font: Font.with(size: 17.0, weight: .semibold), + color: .white + ), + availableSize: CGSize(width: 240.0, height: 100.0), + transition: .immediate + ) + + let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width + backgroundSize.width = updatedBackgroundWidth + 32.0 + } + + let background = background.update( + component: BlurredBackgroundComponent(color: context.component.backgroundColor, tintContainerView: nil, cornerRadius: backgroundHeight / 2.0), + availableSize: backgroundSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) + .clipsToBounds(true) + ) + + if let title { + context.add(title + .position(CGPoint(x: title.size.width / 2.0 + 54.0, y: backgroundHeight / 2.0)) + ) + } + + context.add(icon + .position(CGPoint(x: 36.0, y: backgroundSize.height / 2.0)) + ) + + return backgroundSize + } + } +} + private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c9ddbd4551..76c9930e79 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -3074,6 +3074,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -8795,7 +8796,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } }) case .chatFolders: - let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, dismissed: nil) + let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil) push(controller) case .notificationsAndSounds: if let settings = self.data?.globalSettings { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 8dba199363..1ae74c8100 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -295,7 +295,23 @@ final class PeerInfoStoryGridScreenComponent: Component { let _ = paneNode.scrollToTop() } + func openCreateStory() { + guard let component = self.component else { + return + } + if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + let coordinator = rootController.openStoryCamera(customTarget: nil, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil }) + coordinator?.animateIn() + } + } + + private var isUpdating = false func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.component = component self.state = state @@ -313,7 +329,7 @@ final class PeerInfoStoryGridScreenComponent: Component { var bottomInset: CGFloat = environment.safeInsets.bottom - if self.selectedCount != 0 { + if self.selectedCount != 0 || (component.scope == .saved && self.paneNode?.isEmpty == false) { let selectionPanel: ComponentView var selectionPanelTransition = transition if let current = self.selectionPanel { @@ -327,7 +343,7 @@ final class PeerInfoStoryGridScreenComponent: Component { let buttonText: String switch component.scope { case .saved: - buttonText = environment.strings.ChatList_Context_Archive + buttonText = self.selectedCount > 0 ? environment.strings.ChatList_Context_Archive : environment.strings.StoryList_SavedAddAction case .archive: buttonText = environment.strings.StoryList_SaveToProfile } @@ -344,7 +360,7 @@ final class PeerInfoStoryGridScreenComponent: Component { guard let self, let component = self.component, let environment = self.environment else { return } - guard let paneNode = self.paneNode, !paneNode.selectedIds.isEmpty else { + guard let paneNode = self.paneNode else { return } @@ -361,21 +377,25 @@ final class PeerInfoStoryGridScreenComponent: Component { switch component.scope { case .saved: let selectedCount = paneNode.selectedItems.count - let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start() - - paneNode.setIsSelectionModeActive(false) - (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - - let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) - environment.controller()?.present(UndoOverlayController( - presentationData: presentationData, - content: .info(title: nil, text: title, timeout: nil, customUndoText: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) + if selectedCount == 0 { + self.openCreateStory() + } else { + let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: false).start() + + paneNode.setIsSelectionModeActive(false) + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: title, timeout: nil, customUndoText: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + } case .archive: let _ = component.context.engine.messages.updateStoriesArePinned(peerId: component.peerId, ids: paneNode.selectedItems, isPinned: true).start() @@ -449,10 +469,28 @@ final class PeerInfoStoryGridScreenComponent: Component { }, listContext: nil ) + paneNode.isEmptyUpdated = { [weak self] _ in + guard let self else { + return + } + if !self.isUpdating { + self.state?.updated(transition: .immediate) + } + } self.paneNode = paneNode - self.addSubview(paneNode.view) + if let selectionPanelView = self.selectionPanel?.view { + self.insertSubview(paneNode.view, belowSubview: selectionPanelView) + } else { + self.addSubview(paneNode.view) + } paneNode.emptyAction = { [weak self] in + guard let self else { + return + } + self.openCreateStory() + } + paneNode.additionalEmptyAction = { [weak self] in guard let self, let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index b7d525ded0..99d64be443 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -948,6 +948,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + public var isEmptyUpdated: (Bool) -> Void = { _ in } + public private(set) var isSelectionModeActive: Bool private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData)? @@ -985,6 +987,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr public var openCurrentDate: (() -> Void)? public var paneDidScroll: (() -> Void)? public var emptyAction: (() -> Void)? + public var additionalEmptyAction: (() -> Void)? public var ensureRectVisible: ((UIView, CGRect) -> Void)? @@ -1729,6 +1732,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr private func updateHistory(items: SparseItemGrid.Items, synchronous: Bool, reloadAtTop: Bool) { self.items = items + self.isEmptyUpdated(self.isEmpty) if let (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData) = self.currentParams { var gridSnapshot: UIView? @@ -2027,14 +2031,21 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr context: self.context, theme: presentationData.theme, animationName: "StoryListEmpty", - title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyState_Title, - text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyState_Text, - actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction, + title: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Title : presentationData.strings.StoryList_SavedEmptyPosts_Title, + text: self.isArchive ? presentationData.strings.StoryList_ArchivedEmptyState_Text : presentationData.strings.StoryList_SavedEmptyPosts_Text, + actionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedAddAction, action: { [weak self] in guard let self else { return } self.emptyAction?() + }, + additionalActionTitle: self.isArchive ? nil : presentationData.strings.StoryList_SavedEmptyAction, + additionalAction: { [weak self] in + guard let self else { + return + } + self.additionalEmptyAction?() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift index 810596bab8..3f3fe6dbcb 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryAuthorInfoComponent.swift @@ -134,6 +134,9 @@ final class StoryAuthorInfoComponent: Component { if timeString.count < 6 { combinedString.append(NSAttributedString(string: " • \(timeString)", font: Font.regular(11.0), textColor: subtitleColor)) } + if component.isEdited { + combinedString.append(NSAttributedString(string: " • \(component.strings.Story_HeaderEdited)", font: Font.regular(11.0), textColor: subtitleColor)) + } subtitle = combinedString subtitleTruncationType = .middle } else { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index c25fb3cb10..412e4c50ff 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -5420,6 +5420,7 @@ public final class StoryItemSetContainerComponent: Component { var updateProgressImpl: ((Float) -> Void)? let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, isEditing: !repost, forwardSource: repost ? (component.slice.peer, item) : nil, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index d374b63b52..d257ff5efa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -179,6 +179,7 @@ final class StoryItemSetContainerSendMessage { self.inputPanelExternalState?.deleteBackward() } }, + openStickerEditor: {}, presentController: { [weak self] c, a in if let self { self.view?.component?.controller()?.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json index 37accc5389..cad3335c66 100644 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "check.pdf", + "filename" : "arrowhead_30.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf new file mode 100644 index 0000000000..c646e62924 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/arrowhead_30.pdf @@ -0,0 +1,93 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 10.000000 9.771912 cm +1.000000 1.000000 1.000000 scn +0.742462 6.970551 m +0.332412 7.380601 -0.332412 7.380601 -0.742462 6.970551 c +-1.152513 6.560500 -1.152513 5.895677 -0.742462 5.485626 c +0.742462 6.970551 l +h +4.000000 2.228088 m +3.257538 1.485626 l +3.462021 1.281143 3.741785 1.170047 4.030842 1.178541 c +4.319900 1.187036 4.592658 1.314369 4.784780 1.530506 c +4.000000 2.228088 l +h +12.784780 10.530506 m +13.170044 10.963928 13.131004 11.627604 12.697582 12.012868 c +12.264160 12.398132 11.600484 12.359093 11.215220 11.925671 c +12.784780 10.530506 l +h +-0.742462 5.485626 m +3.257538 1.485626 l +4.742462 2.970551 l +0.742462 6.970551 l +-0.742462 5.485626 l +h +4.784780 1.530506 m +12.784780 10.530506 l +11.215220 11.925671 l +3.215220 2.925671 l +4.784780 1.530506 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 839 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 33.000000 33.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000929 00000 n +0000000951 00000 n +0000001124 00000 n +0000001198 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1257 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf deleted file mode 100644 index c4dfcbde1b..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf +++ /dev/null @@ -1,150 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /Type /XObject - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 40.000000 40.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm -0.000000 0.000000 0.000000 scn -16.557842 15.662290 m -17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c -8.443170 0.821279 l -8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c -7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c -0.389548 6.608668 l --0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c -0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c -7.112782 3.647242 l -14.716830 15.276961 l -15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c -h -f* -n -Q - -endstream -endobj - -2 0 obj - 584 -endobj - -3 0 obj - << /Type /XObject - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 40.000000 40.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 20.000000 m -0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c -20.000000 40.000000 l -31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c -40.000000 20.000000 l -40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c -20.000000 0.000000 l -8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c -0.000000 20.000000 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 472 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 40.000000 40.000000 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Pages 9 0 R - /Type /Catalog - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000000842 00000 n -0000000864 00000 n -0000001584 00000 n -0000001606 00000 n -0000001904 00000 n -0000002006 00000 n -0000002027 00000 n -0000002200 00000 n -0000002274 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -2334 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json new file mode 100644 index 0000000000..bc673b85bd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "magic_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf new file mode 100644 index 0000000000..2cfdca5431 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Cutout.imageset/magic_30.pdf @@ -0,0 +1,149 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.694702 2.078613 cm +0.000000 0.000000 0.000000 scn +9.817018 17.062620 m +9.747100 17.099379 9.663571 17.099379 9.593654 17.062620 c +8.489970 16.482380 l +8.313900 16.389814 8.108116 16.539326 8.141742 16.735382 c +8.352527 17.964352 l +8.365880 18.042206 8.340069 18.121647 8.283504 18.176785 c +7.390605 19.047144 l +7.248161 19.185993 7.326764 19.427906 7.523617 19.456511 c +8.757572 19.635815 l +8.835742 19.647175 8.903318 19.696270 8.938277 19.767105 c +9.490119 20.885258 l +9.578154 21.063637 9.832517 21.063639 9.920552 20.885260 c +10.472394 19.767105 l +10.507353 19.696270 10.574928 19.647175 10.653099 19.635815 c +11.887054 19.456511 l +12.083907 19.427906 12.162510 19.185993 12.020066 19.047144 c +11.127168 18.176785 l +11.070602 18.121647 11.044791 18.042206 11.058144 17.964352 c +11.268929 16.735382 l +11.302555 16.539326 11.096771 16.389814 10.920701 16.482380 c +9.817018 17.062620 l +h +1.785109 15.879396 m +1.735167 15.905652 1.675504 15.905652 1.625563 15.879396 c +0.837217 15.464938 l +0.711453 15.398820 0.564465 15.505613 0.588483 15.645655 c +0.739044 16.523491 l +0.748582 16.579102 0.730145 16.635843 0.689742 16.675226 c +0.051957 17.296913 l +-0.049789 17.396091 0.006356 17.568886 0.146965 17.589317 c +1.028362 17.717392 l +1.084198 17.725506 1.132466 17.760574 1.157437 17.811171 c +1.551610 18.609852 l +1.614492 18.737267 1.796180 18.737267 1.859062 18.609852 c +2.253235 17.811171 l +2.278205 17.760574 2.326474 17.725506 2.382310 17.717392 c +3.263707 17.589317 l +3.404316 17.568886 3.460460 17.396091 3.358715 17.296913 c +2.720930 16.675226 l +2.680526 16.635843 2.662090 16.579100 2.671628 16.523491 c +2.822188 15.645655 l +2.846207 15.505613 2.699219 15.398820 2.573454 15.464939 c +1.785109 15.879396 l +h +3.225563 7.079397 m +3.275504 7.105653 3.335167 7.105653 3.385108 7.079397 c +4.173454 6.664939 l +4.299219 6.598821 4.446208 6.705614 4.422188 6.845654 c +4.271628 7.723491 l +4.262090 7.779101 4.280527 7.835844 4.320930 7.875227 c +4.958715 8.496914 l +5.060461 8.596091 5.004316 8.768887 4.863707 8.789319 c +3.982310 8.917393 l +3.926474 8.925507 3.878206 8.960575 3.853235 9.011171 c +3.459062 9.809853 l +3.396180 9.937266 3.214492 9.937266 3.151610 9.809853 c +2.757437 9.011171 l +2.732466 8.960575 2.684198 8.925507 2.628362 8.917393 c +1.746965 8.789319 l +1.606356 8.768887 1.550211 8.596092 1.651957 8.496914 c +2.289742 7.875228 l +2.330145 7.835844 2.348582 7.779101 2.339044 7.723491 c +2.188483 6.845655 l +2.164464 6.705614 2.311453 6.598821 2.437217 6.664939 c +3.225563 7.079397 l +h +5.069424 14.825876 m +5.641542 15.397993 6.569129 15.397994 7.141247 14.825876 c +18.809877 3.157246 l +19.381996 2.585127 19.381994 1.657541 18.809877 1.085423 c +18.341248 0.616795 l +17.769131 0.044676 16.841543 0.044676 16.269424 0.616795 c +4.600795 12.285424 l +4.028677 12.857543 4.028677 13.785130 4.600795 14.357246 c +5.069424 14.825876 l +h +8.835109 11.251109 m +8.175561 10.591561 l +17.209877 1.557245 l +17.262598 1.504526 17.348076 1.504526 17.400797 1.557245 c +17.869425 2.025875 l +17.922146 2.078596 17.922146 2.164074 17.869425 2.216793 c +8.835109 11.251109 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 3139 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000003229 00000 n +0000003252 00000 n +0000003425 00000 n +0000003499 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3558 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c5959de0c8..a5d4429d34 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4584,6 +4584,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G ) self.push(boostController) }) + }, openStickerEditor: { [weak self] in + guard let self else { + return + } + self.openStickerEditor() }, requestMessageUpdate: { [weak self] id, scroll in if let self { self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) @@ -16857,6 +16862,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, transitionIn: nil, transitionOut: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index c3b7ac9db7..73b19737f5 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -30,6 +30,7 @@ import PremiumUI import PremiumGiftAttachmentScreen import TelegramCallsUI import AutomaticBusinessMessageSetupScreen +import MediaEditorScreen extension ChatControllerImpl { enum AttachMenuSubject { @@ -1698,4 +1699,71 @@ extension ChatControllerImpl { }) }) } + + func openStickerEditor() { + let mainController = AttachmentController(context: self.context, updatedPresentationData: self.updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { + return nil + }) +// controller.forceSourceRect = true +// controller.getSourceRect = getSourceRect + mainController.requestController = { [weak self, weak mainController] _, present in + guard let self else { + return + } + let mediaPickerController = MediaPickerScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, subject: .assets(nil, .createSticker)) + mediaPickerController.customSelection = { [weak self, weak mainController] controller, result in + guard let self else { + return + } + if let result = result as? PHAsset { + controller.updateHiddenMediaId(result.localIdentifier) + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + let editorController = MediaEditorScreen( + context: self.context, + mode: .stickerEditor, + subject: .single(.asset(result)), + transitionIn: .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: transitionView, + sourceRect: transitionView.bounds, + sourceImage: controller.transitionImage(for: result.localIdentifier) + ) + ), + transitionOut: { finished, isNew in + if !finished { + return MediaEditorScreen.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) + } + return nil + }, completion: { [weak self, weak mainController] result, commit in + mainController?.dismiss() + + Queue.mainQueue().after(0.1) { + commit({}) + if let mediaResult = result.media, case let .image(image, _) = mediaResult { + self?.enqueueStickerImage(image, isMemoji: false) + } + } + } as (MediaEditorScreen.Result, @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.dismissed = { [weak controller] in + controller?.updateHiddenMediaId(nil) + } + self.push(editorController) + +// completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut, { [weak controller] in +// controller?.updateHiddenMediaId(nil) +// }) + } + } + } + present(mediaPickerController, mediaPickerController.mediaPickerContext) + } + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.push(mainController) + } } diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index ba27451ada..e7be8e9ccd 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -173,6 +173,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 026e9f34a3..72ec09c895 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1754,6 +1754,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openPremiumStatusInfo: { _, _, _, _ in }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in + }, openStickerEditor: { }, requestMessageUpdate: { _, _ in }, cancelInteractiveKeyboardGestures: { }, dismissTextInput: { @@ -1882,8 +1883,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return archiveSettingsController(context: context) } - public func makeFilterSettingsController(context: AccountContext, modal: Bool, dismissed: (() -> Void)?) -> ViewController { - return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, dismissed: dismissed) + public func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController { + return chatListFilterPresetListController(context: context, mode: modal ? .modal : .default, scrollToTags: scrollToTags, dismissed: dismissed) } public func makeBusinessSetupScreen(context: AccountContext) -> ViewController { @@ -2048,6 +2049,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .lastSeen case .messagePrivacy: mappedSubject = .messagePrivacy + default: + mappedSubject = .doubleLimits } return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index b753edea70..2c6b97fd5b 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -28,6 +28,8 @@ import ImageCompression import TextFormat import MediaEditor import PeerInfoScreen +import PeerInfoStoryGridScreen +import ShareWithPeersScreen private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -354,6 +356,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let controller = MediaEditorScreen( context: context, + mode: .storyEditor, subject: subject, customTarget: customTarget, transitionIn: transitionIn, @@ -509,6 +512,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon viewControllers.removeSubrange(range) self.setViewControllers(viewControllers, animated: false) } + } else if self.viewControllers.contains(where: { $0 is PeerInfoStoryGridScreen }) { + var viewControllers: [UIViewController] = [] + for i in (0 ..< self.viewControllers.count) { + let controller = self.viewControllers[i] + if i == 0 { + viewControllers.append(controller) + } else if controller is MediaEditorScreen { + viewControllers.append(controller) + } else if controller is ShareWithPeersScreen { + viewControllers.append(controller) + } + } + self.setViewControllers(viewControllers, animated: false) } }