diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d24e4b3073..b214568696 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11892,3 +11892,6 @@ Sorry for the inconvenience."; "Business.DontHideAds" = "Do Not Hide Ads"; "Business.AdsInfo" = "As a Premium subscriber, you don't see any ads on Telegram, but you can turn them on, for example, to view your own ads that you launched on the [Telegram Ad Platform >]()"; "Business.AdsInfo_URL" = "https://promote.telegram.org"; + +"ChatList.PremiumGraceTitle" = "⚠️ Don't lose access to Telegram Premium!"; +"ChatList.PremiumGraceText" = "Your exclusive benefits are about to expire."; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 18c1693ec9..02dfa7dec2 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -120,6 +120,7 @@ public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { return PremiumConfiguration( isPremiumDisabled: false, + subscriptionManagementUrl: "", showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, @@ -144,6 +145,7 @@ public struct PremiumConfiguration { } public let isPremiumDisabled: Bool + public let subscriptionManagementUrl: String public let showPremiumGiftInAttachMenu: Bool public let showPremiumGiftInTextField: Bool public let giveawayGiftsPurchaseAvailable: Bool @@ -158,7 +160,6 @@ public struct PremiumConfiguration { public let minChannelWallpaperLevel: Int32 public let minChannelCustomWallpaperLevel: Int32 public let minChannelRestrictAdsLevel: Int32 - public let minGroupProfileIconLevel: Int32 public let minGroupEmojiStatusLevel: Int32 public let minGroupWallpaperLevel: Int32 @@ -168,6 +169,7 @@ public struct PremiumConfiguration { fileprivate init( isPremiumDisabled: Bool, + subscriptionManagementUrl: String, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, @@ -190,6 +192,7 @@ public struct PremiumConfiguration { minGroupAudioTranscriptionLevel: Int32 ) { self.isPremiumDisabled = isPremiumDisabled + self.subscriptionManagementUrl = subscriptionManagementUrl self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu self.showPremiumGiftInTextField = showPremiumGiftInTextField self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable @@ -220,6 +223,7 @@ public struct PremiumConfiguration { } return PremiumConfiguration( isPremiumDisabled: data["premium_purchase_blocked"] as? Bool ?? defaultValue.isPremiumDisabled, + subscriptionManagementUrl: data["premium_manage_subscription_url"] as? String ?? "", showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? defaultValue.showPremiumGiftInAttachMenu, showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? defaultValue.showPremiumGiftInTextField, giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? defaultValue.giveawayGiftsPurchaseAvailable, diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2786f064f7..3938229ae4 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1171,6 +1171,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.openBirthdaySetup() } + self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in + guard let self else { + return + } + let context = self.context + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let url = premiumConfiguration.subscriptionManagementUrl + guard !url.isEmpty else { + return + } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: context.sharedContext.currentPresentationData.with({$0}), navigationController: self.navigationController as? NavigationController, dismissInput: {}) + } self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, threadId, messageId, deactivateOnAction in if let strongSelf = self { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index b1b1ab0bc1..254d87e596 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -345,6 +345,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele itemNode.listNode.openBirthdaySetup = { [weak self] in self?.openBirthdaySetup?() } + itemNode.listNode.openPremiumManagement = { [weak self] in + self?.openPremiumManagement?() + } self.currentItemStateValue.set(itemNode.listNode.state |> map { state in let filterId: Int32? @@ -408,6 +411,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele var shouldStopScrolling: ((ListView, CGFloat) -> Bool)? var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? var openBirthdaySetup: (() -> Void)? + var openPremiumManagement: (() -> Void)? var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)? var didBeginSelectingChats: (() -> Void)? diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 9ef9b72a99..94e7fe3e73 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -2309,6 +2309,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { }, openPasswordSetup: { }, openPremiumIntro: { }, openPremiumGift: { _ in + }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in @@ -3686,7 +3687,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift index f9fc5f30c8..2009fc63d0 100644 --- a/submodules/ChatListUI/Sources/ChatListShimmerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListShimmerNode.swift @@ -156,7 +156,7 @@ public final class ChatListShimmerNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: animationCache, animationRenderer: animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in }, editPeer: { _ in }) interaction.isInlineMode = isInlineMode diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 0da52b7977..da60f570c7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -102,6 +102,7 @@ public final class ChatListNodeInteraction { let openPasswordSetup: () -> Void let openPremiumIntro: () -> Void let openPremiumGift: ([EnginePeer.Id: TelegramBirthday]?) -> Void + let openPremiumManagement: () -> Void let openActiveSessions: () -> Void let openBirthdaySetup: () -> Void let performActiveSessionAction: (NewSessionReview, Bool) -> Void @@ -156,6 +157,7 @@ public final class ChatListNodeInteraction { openPasswordSetup: @escaping () -> Void, openPremiumIntro: @escaping () -> Void, openPremiumGift: @escaping ([EnginePeer.Id: TelegramBirthday]?) -> Void, + openPremiumManagement: @escaping () -> Void, openActiveSessions: @escaping () -> Void, openBirthdaySetup: @escaping () -> Void, performActiveSessionAction: @escaping (NewSessionReview, Bool) -> Void, @@ -197,6 +199,7 @@ public final class ChatListNodeInteraction { self.openPasswordSetup = openPasswordSetup self.openPremiumIntro = openPremiumIntro self.openPremiumGift = openPremiumGift + self.openPremiumManagement = openPremiumManagement self.openActiveSessions = openActiveSessions self.openBirthdaySetup = openBirthdaySetup self.performActiveSessionAction = performActiveSessionAction @@ -736,6 +739,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: nodeInteraction?.openPremiumGift(nil) + case .premiumGrace: + nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() case let .birthdayPremiumGift(_, birthdays): @@ -1072,6 +1077,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL nodeInteraction?.openPremiumIntro() case .xmasPremiumGift: nodeInteraction?.openPremiumGift(nil) + case .premiumGrace: + nodeInteraction?.openPremiumManagement() case .setupBirthday: nodeInteraction?.openBirthdaySetup() case let .birthdayPremiumGift(_, birthdays): @@ -1196,6 +1203,7 @@ public final class ChatListNode: ListView { public var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? public var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)? public var openBirthdaySetup: (() -> Void)? + public var openPremiumManagement: (() -> Void)? private var theme: PresentationTheme @@ -1701,6 +1709,11 @@ public final class ChatListNode: ListView { let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .chatList(birthdays), completion: nil) controller.navigationPresentation = .modal self.push?(controller) + }, openPremiumManagement: { [weak self] in + guard let self else { + return + } + self.openPremiumManagement?() }, openActiveSessions: { [weak self] in guard let self else { return @@ -1813,6 +1826,11 @@ public final class ChatListNode: ListView { self.present?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.ChatList_PremiumGiftInSettingsInfo, timeout: 5.0, customUndoText: nil), elevatedLayout: false, action: { _ in return true })) + case .premiumGrace: + let _ = self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: .gracePremium).startStandalone() +// self.present?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.ChatList_BirthdayInSettingsInfo, timeout: 5.0, customUndoText: nil), elevatedLayout: false, action: { _ in +// return true +// })) default: break } @@ -1946,7 +1964,9 @@ public final class ChatListNode: ListView { todayBirthdayPeerIds = [] } - if suggestions.contains(.setupBirthday) && birthday == nil { + if suggestions.contains(.gracePremium) { + return .single(.premiumGrace) + } else if suggestions.contains(.setupBirthday) && birthday == nil { return .single(.setupBirthday) } else if suggestions.contains(.xmasPremiumGift) { return .single(.xmasPremiumGift) diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift index 8321ea2e67..6ed8117ebf 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeEntries.swift @@ -89,6 +89,7 @@ public enum ChatListNotice: Equatable { case setupBirthday case birthdayPremiumGift(peers: [EnginePeer], birthdays: [EnginePeer.Id: TelegramBirthday]) case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int) + case premiumGrace } enum ChatListNodeEntry: Comparable, Identifiable { diff --git a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift index 21a35a47a5..760b9edd7b 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNoticeItem.swift @@ -224,6 +224,9 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { case .xmasPremiumGift: titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumXmasGiftTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil })) textString = NSAttributedString(string: item.strings.ChatList_PremiumXmasGiftText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) + case .premiumGrace: + titleString = parseMarkdownIntoAttributedString(item.strings.ChatList_PremiumGraceTitle, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.accentTextColor), link: MarkdownAttributeSet(font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor), linkAttribute: { _ in return nil })) + textString = NSAttributedString(string: item.strings.ChatList_PremiumGraceText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) case .setupBirthday: titleString = NSAttributedString(string: item.strings.ChatList_AddBirthdayTitle, font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor) textString = NSAttributedString(string: item.strings.ChatList_AddBirthdayText, font: textFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) @@ -333,6 +336,8 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode { hasCloseButton = true } else if case .birthdayPremiumGift = item.notice { hasCloseButton = true + } else if case .premiumGrace = item.notice { + hasCloseButton = true } if let okButtonLayout, let cancelButtonLayout { diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 73404dd1e1..66de8be0c8 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -286,7 +286,7 @@ final class PeekControllerNode: ViewControllerTracingNode { topAccessoryNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) topAccessoryNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) } - + if case .press = self.content.menuActivation() { self.hapticFeedback.tap() } else { @@ -306,12 +306,11 @@ final class PeekControllerNode: ViewControllerTracingNode { var scaleCompleted = false var positionCompleted = false - let outCompletion = { [weak self] in + let outCompletion = { if scaleCompleted && positionCompleted { - self?.controller?.disappeared?() } } - + let offset = CGPoint(x: rect.midX - self.containerNode.position.x, y: rect.midY - self.containerNode.position.y) self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: offset), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false, additive: true, completion: { _ in positionCompleted = true @@ -320,6 +319,7 @@ final class PeekControllerNode: ViewControllerTracingNode { }) if let _ = self.controller?.disappeared { + self.controller?.disappeared?() let scale = rect.width / self.contentNode.frame.width self.containerNode.layer.animateScale(from: 1.0, to: scale, duration: 0.25, removeOnCompletion: false, completion: { _ in scaleCompleted = true diff --git a/submodules/DrawingUI/Sources/DrawingGesture.swift b/submodules/DrawingUI/Sources/DrawingGesture.swift index d167a69854..a4f929b050 100644 --- a/submodules/DrawingUI/Sources/DrawingGesture.swift +++ b/submodules/DrawingUI/Sources/DrawingGesture.swift @@ -33,7 +33,7 @@ struct DrawingPoint { } } -class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate { +final class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate { enum DrawingGestureState { case began case changed @@ -46,13 +46,23 @@ class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate { var gestureRecognizer: DrawingGestureRecognizer? var transform: CGAffineTransform = .identity - init(view: DrawingView) { - super.init() + var enabled: Bool = true + + weak var drawingView: DrawingView? + + init(drawingView: DrawingView, gestureView: UIView) { + self.drawingView = drawingView + super.init() + let gestureRecognizer = DrawingGestureRecognizer(target: self, action: #selector(self.handleGesture(_:))) gestureRecognizer.delegate = self self.gestureRecognizer = gestureRecognizer - view.addGestureRecognizer(gestureRecognizer) + gestureView.addGestureRecognizer(gestureRecognizer) + } + + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return self.enabled } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -82,9 +92,9 @@ class DrawingGesturePipeline: NSObject, UIGestureRecognizerDelegate { state = .cancelled } - let originalLocation = gestureRecognizer.location(in: gestureRecognizer.view) + let originalLocation = gestureRecognizer.location(in: self.drawingView) let location = originalLocation.applying(self.transform) - let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view).applying(self.transform) + let velocity = gestureRecognizer.velocity(in: self.drawingView).applying(self.transform) let velocityValue = velocity.length let point = DrawingPoint(location: location, velocity: velocityValue) diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntityView.swift b/submodules/DrawingUI/Sources/DrawingMediaEntityView.swift index 9bd7aedcc5..e8b110d21d 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntityView.swift @@ -16,18 +16,6 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia private var isVisible = true private var isPlaying = false - public weak var previewView: MediaEditorPreviewView? { - didSet { - if let previewView = self.previewView { - previewView.isUserInteractionEnabled = false - previewView.layer.allowsEdgeAntialiasing = true - self.addSubview(previewView) - } else { - oldValue?.removeFromSuperview() - } - } - } - private let snapTool = DrawingEntitySnapTool() init(context: AccountContext, entity: DrawingMediaEntity) { @@ -45,14 +33,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - deinit { - if let previewView = self.previewView { - previewView.removeFromSuperview() - self.previewView = nil - } - } - + public override func play() { self.isVisible = true self.applyVisibility() @@ -66,7 +47,6 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia public override func seek(to timestamp: Double) { self.isVisible = false self.isPlaying = false - } override func resetToStart() { @@ -83,7 +63,6 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia let isPlaying = self.isVisible if self.isPlaying != isPlaying { self.isPlaying = isPlaying - } } @@ -95,9 +74,6 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia if size.width > 0 && self.currentSize != size { self.currentSize = size - if self.previewView?.superview === self { - self.previewView?.frame = CGRect(origin: .zero, size: size) - } self.update(animated: false) } } @@ -112,11 +88,6 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia self.bounds = CGRect(origin: .zero, size: size) self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.mediaEntity.rotation), scale, scale) -// if self.previewView?.superview === self { -// self.previewView?.layer.transform = CATransform3DMakeScale(self.mediaEntity.mirrored ? -1.0 : 1.0, 1.0, 1.0) -// self.previewView?.frame = self.bounds -// } - super.update(animated: animated) self.updated?() diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 902a23e3e6..15232b7a07 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -32,7 +32,7 @@ public struct DrawingResultData { public let entities: [CodableDrawingEntity] } -enum DrawingToolState: Equatable, Codable { +public enum DrawingToolState: Equatable, Codable { private enum CodingKeys: String, CodingKey { case type case brushState @@ -48,27 +48,27 @@ enum DrawingToolState: Equatable, Codable { case eraser = 5 } - struct BrushState: Equatable, Codable { + public struct BrushState: Equatable, Codable { private enum CodingKeys: String, CodingKey { case color case size } - let color: DrawingColor - let size: CGFloat + public let color: DrawingColor + public let size: CGFloat - init(color: DrawingColor, size: CGFloat) { + public init(color: DrawingColor, size: CGFloat) { self.color = color self.size = size } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.color = try container.decode(DrawingColor.self, forKey: .color) self.size = try container.decode(CGFloat.self, forKey: .size) } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.color, forKey: .color) try container.encode(self.size, forKey: .size) @@ -83,23 +83,23 @@ enum DrawingToolState: Equatable, Codable { } } - struct EraserState: Equatable, Codable { + public struct EraserState: Equatable, Codable { private enum CodingKeys: String, CodingKey { case size } - let size: CGFloat + public let size: CGFloat - init(size: CGFloat) { + public init(size: CGFloat) { self.size = size } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.size = try container.decode(CGFloat.self, forKey: .size) } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.size, forKey: .size) } @@ -116,7 +116,7 @@ enum DrawingToolState: Equatable, Codable { case blur(EraserState) case eraser(EraserState) - func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { + public func withUpdatedColor(_ color: DrawingColor) -> DrawingToolState { switch self { case let .pen(state): return .pen(state.withUpdatedColor(color)) @@ -131,7 +131,7 @@ enum DrawingToolState: Equatable, Codable { } } - func withUpdatedSize(_ size: CGFloat) -> DrawingToolState { + public func withUpdatedSize(_ size: CGFloat) -> DrawingToolState { switch self { case let .pen(state): return .pen(state.withUpdatedSize(size)) @@ -148,7 +148,7 @@ enum DrawingToolState: Equatable, Codable { } } - var color: DrawingColor? { + public var color: DrawingColor? { switch self { case let .pen(state), let .arrow(state), let .marker(state), let .neon(state): return state.color @@ -157,7 +157,7 @@ enum DrawingToolState: Equatable, Codable { } } - var size: CGFloat? { + public var size: CGFloat? { switch self { case let .pen(state), let .arrow(state), let .marker(state), let .neon(state): return state.size @@ -183,7 +183,7 @@ enum DrawingToolState: Equatable, Codable { } } - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let typeValue = try container.decode(Int32.self, forKey: .type) if let type = DrawingToolState.Key(rawValue: typeValue) { @@ -206,7 +206,7 @@ enum DrawingToolState: Equatable, Codable { } } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case let .pen(state): diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 3351c8ae67..5321abea5a 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -133,13 +133,13 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate return false } - if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage { + if isPNG && images.count == 1, let image = images.first { let maxSide = max(image.size.width, image.size.height) if maxSide.isZero { return false } let aspectRatio = min(image.size.width, image.size.height) / maxSide - if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) { + if isMemoji || (imageHasTransparency(image) && aspectRatio > 0.2) { self.endEditing(reset: true) self.replaceWithImage(image, true) return false diff --git a/submodules/DrawingUI/Sources/DrawingView.swift b/submodules/DrawingUI/Sources/DrawingView.swift index b307ef90ac..aed105e3c9 100644 --- a/submodules/DrawingUI/Sources/DrawingView.swift +++ b/submodules/DrawingUI/Sources/DrawingView.swift @@ -39,15 +39,15 @@ private enum DrawingOperation { public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInteractionDelegate, TGPhotoDrawingView { public var zoomOut: () -> Void = {} - struct NavigationState { - let canUndo: Bool - let canRedo: Bool - let canClear: Bool - let canZoomOut: Bool - let isDrawing: Bool + public struct NavigationState { + public let canUndo: Bool + public let canRedo: Bool + public let canClear: Bool + public let canZoomOut: Bool + public let isDrawing: Bool } - enum Action { + public enum Action { case undo case redo case clear @@ -67,7 +67,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt var toolColor: DrawingColor = DrawingColor(color: .white) var toolBrushSize: CGFloat = 0.25 - var stateUpdated: (NavigationState) -> Void = { _ in } + public var stateUpdated: (NavigationState) -> Void = { _ in } var shouldBegin: (CGPoint) -> Bool = { _ in return true } var getFullImage: () -> UIImage? = { return nil } @@ -80,7 +80,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt private var redoStack: [DrawingOperation] = [] fileprivate var uncommitedElement: DrawingElement? - private(set) var drawingImage: UIImage? + public private(set) var drawingImage: UIImage? private let renderer: UIGraphicsImageRenderer private var currentDrawingViewContainer: UIImageView @@ -104,7 +104,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt private var isDrawing = false private var drawingGestureStartTimestamp: Double? - var animationsEnabled = true + public var animationsEnabled = true private func loadTemplates() { func load(_ name: String) { @@ -138,7 +138,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt private let pencilInteraction: UIInteraction? - public init(size: CGSize) { + public init(size: CGSize, gestureView: UIView? = nil) { self.imageSize = size self.screenSize = size @@ -189,7 +189,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt self.layer.addSublayer(self.brushSizePreviewLayer) - let drawingGesturePipeline = DrawingGesturePipeline(view: self) + let drawingGesturePipeline = DrawingGesturePipeline(drawingView: self, gestureView: gestureView ?? self) drawingGesturePipeline.gestureRecognizer?.shouldBegin = { [weak self] point in if let strongSelf = self { if !strongSelf.shouldBegin(point) { @@ -381,6 +381,16 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt self.longPressGestureRecognizer = longPressGestureRecognizer } + public override var isUserInteractionEnabled: Bool { + get { + return super.isUserInteractionEnabled + } + set { + super.isUserInteractionEnabled = newValue + self.drawingGesturePipeline?.enabled = newValue + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -390,12 +400,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt self.strokeRecognitionTimer?.invalidate() } - public func setup(withDrawing drawingData: Data?) { + private var clearImage: UIImage? + public func setup(withDrawing drawingData: Data?, storeAsClear: Bool = false) { self.undoStack = [] self.redoStack = [] if let drawingData = drawingData, let image = UIImage(data: drawingData) { - self.hasOpaqueData = true - if let context = DrawingContext(size: image.size, scale: 1.0, opaque: false) { context.withFlippedContext { context in context.clear(CGRect(origin: .zero, size: image.size)) @@ -407,6 +416,11 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt } else { self.drawingImage = image } + if storeAsClear { + self.clearImage = self.drawingImage + } else { + self.hasOpaqueData = true + } self.layer.contents = image.cgImage self.updateInternalState() } else { @@ -416,6 +430,27 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt } } + public var emptyColor: UIColor? + public func clearWithEmptyColor() { + if let clearImage = self.clearImage { + self.drawingImage = clearImage + } else { + if let context = DrawingContext(size: self.imageSize, scale: 1.0, opaque: false) { + context.withFlippedContext { context in + if let emptyColor = self.emptyColor { + context.setFillColor(emptyColor.cgColor) + context.fill(CGRect(origin: .zero, size: self.imageSize)) + } else { + context.clear(CGRect(origin: .zero, size: self.imageSize)) + } + } + self.drawingImage = context.generateImage() ?? nil + } + } + self.layer.contents = self.drawingImage?.cgImage + self.updateInternalState() + } + var hasOpaqueData = false public var drawingData: Data? { guard !self.undoStack.isEmpty || self.hasOpaqueData else { @@ -736,8 +771,12 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt self.redoStack.append(.slice(slice)) } UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) { - self.drawingImage = nil - self.layer.contents = nil + if let _ = self.emptyColor { + self.clearWithEmptyColor() + } else { + self.drawingImage = nil + self.layer.contents = nil + } } self.updateBlurredImage() case let .slice(slice): @@ -822,7 +861,24 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt private var preparedBlurredImage: UIImage? - func updateToolState(_ state: DrawingToolState) { + public var appliedToolState: DrawingToolState? { + switch self.tool { + case .pen: + return .pen(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) + case .arrow: + return .arrow(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) + case .marker: + return .marker(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) + case .neon: + return .neon(DrawingToolState.BrushState(color: self.toolColor, size: self.toolBrushSize)) + case .blur: + return .blur(DrawingToolState.EraserState(size: self.toolBrushSize)) + case .eraser: + return .eraser(DrawingToolState.EraserState(size: self.toolBrushSize)) + } + } + + public func updateToolState(_ state: DrawingToolState) { let previousTool = self.tool switch state { case let .pen(brushState): @@ -885,7 +941,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt } } - func performAction(_ action: Action) { + public func performAction(_ action: Action) { switch action { case .undo: self.undo() @@ -898,14 +954,18 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt } } - private func updateInternalState() { - self.stateUpdated(NavigationState( + public var internalState: NavigationState { + return NavigationState( canUndo: !self.undoStack.isEmpty, canRedo: !self.redoStack.isEmpty, canClear: !self.undoStack.isEmpty || self.hasOpaqueData || (self.entitiesView?.hasEntities ?? false), canZoomOut: self.zoomScale > 1.0 + .ulpOfOne, isDrawing: self.isDrawing - )) + ) + } + + private func updateInternalState() { + self.stateUpdated(self.internalState) } public func updateZoomScale(_ scale: CGFloat) { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 3ef03cf0d4..dc757a6543 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -96,8 +96,9 @@ public final class HashtagSearchController: TelegramBaseController { }, openPasswordSetup: { }, openPremiumIntro: { }, openPremiumGift: { _ in + }, openPremiumManagement: { }, openActiveSessions: { - }, openBirthdaySetup: { + }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: { }, hideChatFolderUpdates: { diff --git a/submodules/ImageTransparency/Sources/ImageTransparency.swift b/submodules/ImageTransparency/Sources/ImageTransparency.swift index 8c8c17f4af..39d6b32450 100644 --- a/submodules/ImageTransparency/Sources/ImageTransparency.swift +++ b/submodules/ImageTransparency/Sources/ImageTransparency.swift @@ -56,8 +56,8 @@ private func generateHistogram(cgImage: CGImage) -> ([[vImagePixelCount]], Int)? return ([histogramBinZero, histogramBinOne, histogramBinTwo, histogramBinThree], alphaBinIndex) } -public func imageHasTransparency(_ cgImage: CGImage) -> Bool { - guard cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { +public func imageHasTransparency(_ image: UIImage) -> Bool { + guard let cgImage = image.cgImage, cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { return false } guard [.first, .last, .premultipliedFirst, .premultipliedLast].contains(cgImage.alphaInfo) else { diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index 73bd3744ea..a5c7062152 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -122,7 +122,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController func ready() -> Signal { return self._ready.get() } - + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let boundingSize = CGSize(width: 180.0, height: 180.0).fitted(size) let imageFrame = CGRect(origin: CGPoint(), size: boundingSize) diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 3a03b07d7e..2fa6bbdc1d 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -106,6 +106,10 @@ public struct ItemListControllerState { } open class ItemListController: ViewController, KeyShortcutResponder, PresentableController { + var controllerNode: ItemListControllerNode { + return (self.displayNode as! ItemListControllerNode) + } + private let state: Signal<(ItemListControllerState, (ItemListNodeState, Any)), NoError> private var leftNavigationButtonTitleAndStyle: (ItemListNavigationButtonContent, ItemListNavigationButtonStyle)? @@ -139,7 +143,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var experimentalSnapScrollToItem: Bool = false { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem + self.controllerNode.listNode.experimentalSnapScrollToItem = self.experimentalSnapScrollToItem } } } @@ -147,7 +151,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var enableInteractiveDismiss = false { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).enableInteractiveDismiss = self.enableInteractiveDismiss + self.controllerNode.enableInteractiveDismiss = self.enableInteractiveDismiss } } } @@ -155,7 +159,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var alwaysSynchronous = false { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).alwaysSynchronous = self.alwaysSynchronous + self.controllerNode.alwaysSynchronous = self.alwaysSynchronous } } } @@ -163,7 +167,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var visibleEntriesUpdated: ((ItemListNodeVisibleEntries) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).visibleEntriesUpdated = self.visibleEntriesUpdated + self.controllerNode.visibleEntriesUpdated = self.visibleEntriesUpdated } } } @@ -171,14 +175,14 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var beganInteractiveDragging: (() -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).beganInteractiveDragging = self.beganInteractiveDragging + self.controllerNode.beganInteractiveDragging = self.beganInteractiveDragging } } } public var visibleBottomContentOffset: ListViewVisibleContentOffset { if self.isNodeLoaded { - return (self.displayNode as! ItemListControllerNode).listNode.visibleBottomContentOffset() + return self.controllerNode.listNode.visibleBottomContentOffset() } else { return .unknown } @@ -186,7 +190,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged + self.controllerNode.visibleBottomContentOffsetChanged = self.visibleBottomContentOffsetChanged } } } @@ -194,7 +198,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var contentOffsetChanged: ((ListViewVisibleContentOffset, Bool) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).contentOffsetChanged = self.contentOffsetChanged + self.controllerNode.contentOffsetChanged = self.contentOffsetChanged } } } @@ -202,7 +206,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var contentScrollingEnded: ((ListView) -> Bool)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).contentScrollingEnded = self.contentScrollingEnded + self.controllerNode.contentScrollingEnded = self.contentScrollingEnded } } } @@ -210,7 +214,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var searchActivated: ((Bool) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).searchActivated = self.searchActivated + self.controllerNode.searchActivated = self.searchActivated } } } @@ -218,7 +222,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var didScrollWithOffset: ((CGFloat, ContainedViewLayoutTransition, ListViewItemNode?, Bool) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).listNode.didScrollWithOffset = self.didScrollWithOffset + self.controllerNode.listNode.didScrollWithOffset = self.didScrollWithOffset } } } @@ -233,7 +237,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable private var reorderEntry: ((Int, Int, [ItemListNodeAnyEntry]) -> Signal)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).reorderEntry = self.reorderEntry + self.controllerNode.reorderEntry = self.reorderEntry } } } @@ -246,7 +250,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable private var reorderCompleted: (([ItemListNodeAnyEntry]) -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).reorderCompleted = self.reorderCompleted + self.controllerNode.reorderCompleted = self.reorderCompleted } } } @@ -256,7 +260,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable public var afterTransactionCompleted: (() -> Void)? { didSet { if self.isNodeLoaded { - (self.displayNode as! ItemListControllerNode).afterTransactionCompleted = self.afterTransactionCompleted + self.controllerNode.afterTransactionCompleted = self.afterTransactionCompleted } } } @@ -321,8 +325,10 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable strongSelf.navigationItem.titleView = nil strongSelf.segmentedTitleView = nil strongSelf.navigationBar?.setContentNode(nil, animated: false) + strongSelf.controllerNode.panRecognizer?.isEnabled = false case let .textWithSubtitle(title, subtitle): strongSelf.title = "" + strongSelf.controllerNode.panRecognizer?.isEnabled = false strongSelf.navigationItem.titleView = ItemListTextWithSubtitleTitleView(theme: controllerState.presentationData.theme, title: title, subtitle: subtitle) strongSelf.segmentedTitleView = nil strongSelf.navigationBar?.setContentNode(nil, animated: false) @@ -334,13 +340,14 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable let segmentedTitleView = ItemListControllerSegmentedTitleView(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index) strongSelf.segmentedTitleView = segmentedTitleView strongSelf.navigationItem.titleView = strongSelf.segmentedTitleView - segmentedTitleView.indexUpdated = { index in + segmentedTitleView.indexUpdated = { [weak self] index in if let strongSelf = self { strongSelf.titleControlValueChanged?(index) } } } strongSelf.navigationBar?.setContentNode(nil, animated: false) + strongSelf.controllerNode.panRecognizer?.isEnabled = false case let .textWithTabs(title, sections, index): strongSelf.title = title if let tabsNavigationContentNode = strongSelf.tabsNavigationContentNode, tabsNavigationContentNode.segments == sections { @@ -349,7 +356,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable let tabsNavigationContentNode = ItemListControllerTabsContentNode(theme: controllerState.presentationData.theme, segments: sections, selectedIndex: index) strongSelf.tabsNavigationContentNode = tabsNavigationContentNode strongSelf.navigationBar?.setContentNode(tabsNavigationContentNode, animated: false) - tabsNavigationContentNode.indexUpdated = { index in + tabsNavigationContentNode.indexUpdated = { [weak self] index in if let strongSelf = self { strongSelf.titleControlValueChanged?(index) } @@ -359,6 +366,21 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } strongSelf.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate) } + strongSelf.controllerNode.panTransitionFractionChanged = { [weak self] transitionFraction in + if let strongSelf = self { + strongSelf.tabsNavigationContentNode?.transitionFraction = transitionFraction + } + } + strongSelf.controllerNode.panGestureAllowedDirections = { + if index == 0 { + return [.leftCenter] + } else if index == sections.count - 1 { + return [.rightCenter] + } else { + return [.leftCenter, .rightCenter] + } + } + strongSelf.controllerNode.panRecognizer?.isEnabled = true } } strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action) @@ -524,7 +546,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } self.displayNode = displayNode super.displayNodeDidLoad() - self._ready.set((self.displayNode as! ItemListControllerNode).ready) + self._ready.set(self.controllerNode.ready) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -532,7 +554,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable self.validLayout = layout - (self.displayNode as! ItemListControllerNode).containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition, additionalInsets: self.additionalInsets) + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition, additionalInsets: self.additionalInsets) } @objc func leftNavigationButtonPressed() { @@ -554,12 +576,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } public func viewDidAppear(completion: @escaping () -> Void) { - (self.displayNode as! ItemListControllerNode).listNode.preloadPages = true + self.controllerNode.listNode.preloadPages = true if let presentationArguments = self.presentationArguments as? ViewControllerPresentationArguments, !self.didPlayPresentationAnimation { self.didPlayPresentationAnimation = true if case .modalSheet = presentationArguments.presentationAnimation { - (self.displayNode as! ItemListControllerNode).animateIn(completion: { + self.controllerNode.animateIn(completion: { presentationArguments.completion?() completion() }) @@ -589,12 +611,12 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } public var listInsets: UIEdgeInsets { - return (self.displayNode as! ItemListControllerNode).listNode.insets + return self.controllerNode.listNode.insets } public func frameForItemNode(_ predicate: (ListViewItemNode) -> Bool) -> CGRect? { var result: CGRect? - (self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in + self.controllerNode.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListViewItemNode { if predicate(itemNode) { result = itemNode.convert(itemNode.bounds, to: self.displayNode) @@ -605,7 +627,7 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } public func forEachItemNode(_ f: (ListViewItemNode) -> Void) { - (self.displayNode as! ItemListControllerNode).listNode.forEachItemNode { itemNode in + self.controllerNode.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ListViewItemNode { f(itemNode) } @@ -613,15 +635,15 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) { - (self.displayNode as! ItemListControllerNode).listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve) + self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve) } public func afterLayout(_ f: @escaping () -> Void) { - (self.displayNode as! ItemListControllerNode).afterLayout(f) + self.controllerNode.afterLayout(f) } public func clearItemNodesHighlight(animated: Bool = false) { - (self.displayNode as! ItemListControllerNode).listNode.clearHighlightAnimated(animated) + self.controllerNode.listNode.clearHighlightAnimated(animated) } public var keyShortcuts: [KeyShortcut] { diff --git a/submodules/ItemListUI/Sources/ItemListControllerNode.swift b/submodules/ItemListUI/Sources/ItemListControllerNode.swift index 5a54d9dfa3..bfa5769fbb 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerNode.swift @@ -232,7 +232,7 @@ public final class ItemListControllerNodeView: UITracingLayerView { weak var controller: ItemListController? } -open class ItemListControllerNode: ASDisplayNode { +open class ItemListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { private weak var controller: ItemListController? private var _ready = ValuePromise() @@ -243,6 +243,7 @@ open class ItemListControllerNode: ASDisplayNode { private let navigationBar: NavigationBar + private let listNodeContainer: ASDisplayNode public let listNode: ListView private let leftOverlayNode: ASDisplayNode private let rightOverlayNode: ASDisplayNode @@ -273,6 +274,8 @@ open class ItemListControllerNode: ASDisplayNode { private var appliedFocusItemTag: ItemListItemTag? private var appliedEnsureVisibleItemTag: ItemListItemTag? + private(set) var panRecognizer: InteractiveTransitionGestureRecognizer? + private var afterLayoutActions: [() -> Void] = [] public var dismiss: (() -> Void)? @@ -301,6 +304,7 @@ open class ItemListControllerNode: ASDisplayNode { self.controller = controller self.navigationBar = navigationBar + self.listNodeContainer = ASDisplayNode() self.listNode = ListView() self.leftOverlayNode = ASDisplayNode() self.leftOverlayNode.isUserInteractionEnabled = false @@ -316,7 +320,8 @@ open class ItemListControllerNode: ASDisplayNode { self.backgroundColor = nil self.isOpaque = false - self.addSubnode(self.listNode) + self.addSubnode(self.listNodeContainer) + self.listNodeContainer.addSubnode(self.listNode) self.listNode.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in if let strongSelf = self, let visibleEntriesUpdated = strongSelf.visibleEntriesUpdated, let mergedEntries = (opaqueTransactionState as? ItemListNodeOpaqueState)?.mergedEntries { @@ -483,6 +488,130 @@ open class ItemListControllerNode: ASDisplayNode { (self.view as? ItemListControllerNodeView)?.hitTestImpl = { [weak self] point, event in return self?.hitTest(point, with: event) } + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in + guard let self, let directions = self.panGestureAllowedDirections?() else { + return [] + } + return directions + }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panRecognizer = panRecognizer + self.view.addGestureRecognizer(panRecognizer) + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + var panGestureAllowedDirections: (() -> InteractiveTransitionGestureRecognizerDirections)? + var panTransitionFractionChanged: ((CGFloat?) -> Void)? + + private var panSnapshotView: UIView? + private var panTransitionFraction: CGFloat = 0.0 + private var panCurrentAllowedDirections: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter] + + @objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { + let translation = gestureRecognizer.translation(in: self.view).x + let velocity = gestureRecognizer.velocity(in: self.view).x + + switch gestureRecognizer.state { + case .began, .changed: + if case .began = gestureRecognizer.state { + self.panCurrentAllowedDirections = self.panGestureAllowedDirections?() ?? [.leftCenter, .rightCenter] + } + + if self.panSnapshotView == nil, let panSnapshotView = self.listNodeContainer.view.snapshotView(afterScreenUpdates: false) { + self.panSnapshotView = panSnapshotView + self.listNodeContainer.view.superview?.insertSubview(panSnapshotView, aboveSubview: self.listNodeContainer.view) + } + self.panTransitionFraction = -translation / self.view.bounds.width + if !self.panCurrentAllowedDirections.contains(.leftCenter) { + self.panTransitionFraction = min(0.0, self.panTransitionFraction) + } + if !self.panCurrentAllowedDirections.contains(.rightCenter) { + self.panTransitionFraction = max(0.0, self.panTransitionFraction) + } + + if let panSnapshotView = self.panSnapshotView { + panSnapshotView.frame = panSnapshotView.bounds.offsetBy(dx: -self.panTransitionFraction * self.view.bounds.width, dy: 0.0) + } + + var initialOffset: CGFloat = 0.0 + if self.panTransitionFraction > 0.0 { + initialOffset = self.view.bounds.width + } else { + initialOffset = -self.view.bounds.width + } + + self.listNodeContainer.frame = CGRect(origin: CGPoint(x: initialOffset - self.view.bounds.width * self.panTransitionFraction, y: 0.0), size: self.listNodeContainer.frame.size) + + self.panTransitionFractionChanged?(self.panTransitionFraction) + case .ended, .cancelled: + if let panSnapshotView = self.panSnapshotView { + self.panSnapshotView = nil + + var directionIsToRight: Bool? + if abs(velocity) > 10.0 { + if translation > 0.0 { + if velocity <= 0.0 { + directionIsToRight = nil + } else { + directionIsToRight = true + } + } else { + if velocity >= 0.0 { + directionIsToRight = nil + } else { + directionIsToRight = false + } + } + } else { + if abs(translation) > self.view.bounds.width / 2.0 { + directionIsToRight = translation > self.view.bounds.width / 2.0 + } + } + if !self.panCurrentAllowedDirections.contains(.rightCenter) && directionIsToRight == true { + directionIsToRight = nil + } + if !self.panCurrentAllowedDirections.contains(.leftCenter) && directionIsToRight == false { + directionIsToRight = nil + } + + let center = CGPoint(x: self.view.bounds.width / 2.0, y: self.view.bounds.height / 2.0) + let previousPosition = self.listNodeContainer.position + self.listNodeContainer.position = center + if let directionIsToRight { + let targetPosition: CGFloat + if directionIsToRight { + targetPosition = self.view.bounds.width + self.view.bounds.width / 2.0 + } else { + targetPosition = -self.view.bounds.width + self.view.bounds.width / 2.0 + } + panSnapshotView.layer.animatePosition(from: panSnapshotView.center, to: CGPoint(x: targetPosition, y: self.view.bounds.height / 2.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + panSnapshotView.removeFromSuperview() + }) + self.listNodeContainer.layer.animatePosition(from: previousPosition, to: center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } else { + let direction = center.x - panSnapshotView.center.x + panSnapshotView.layer.animatePosition(from: panSnapshotView.center, to: center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + panSnapshotView.removeFromSuperview() + }) + self.listNodeContainer.layer.animatePosition(from: previousPosition, to: CGPoint(x: direction > 0.0 ? self.view.bounds.width + self.view.bounds.width / 2.0 : -self.view.bounds.width + self.view.bounds.width / 2.0, y: self.view.bounds.height / 2.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + } + self.panTransitionFractionChanged?(nil) + default: + break + } } open func animateIn(completion: (() -> Void)? = nil) { @@ -526,10 +655,10 @@ open class ItemListControllerNode: ASDisplayNode { } if self.rightOverlayNode.supernode == nil { - self.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode) + self.listNodeContainer.insertSubnode(self.rightOverlayNode, aboveSubnode: self.listNode) } if self.leftOverlayNode.supernode == nil { - self.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) + self.listNodeContainer.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode) } if let toolbarItem = self.toolbarItem { @@ -603,6 +732,7 @@ open class ItemListControllerNode: ASDisplayNode { insets.bottom = max(footerHeight, insets.bottom) } + self.listNodeContainer.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) diff --git a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift index 1982f6053a..e409a0e4d4 100644 --- a/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift +++ b/submodules/ItemListUI/Sources/ItemListControllerTabsContentNode.swift @@ -34,6 +34,14 @@ final class ItemListControllerTabsContentNode: NavigationBarContentNode { } } } + + var transitionFraction: CGFloat? { + didSet { + if self.transitionFraction != oldValue { + self.update(transition: self.transitionFraction == nil ? .animated(duration: 0.35, curve: .spring) : .immediate) + } + } + } var indexUpdated: ((Int) -> Void)? @@ -88,7 +96,8 @@ final class ItemListControllerTabsContentNode: NavigationBarContentNode { return } self.indexUpdated?(index) - } + }, + transitionFraction: self.transitionFraction )), environment: {}, containerSize: CGSize(width: size.width, height: 44.0) diff --git a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift index df81250447..dcbe736b2b 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListInfoItem.swift @@ -24,15 +24,17 @@ public class InfoListItem: ListViewItem { let text: InfoListItemText let style: ItemListStyle let hasDecorations: Bool + let isWarning: Bool let linkAction: ((InfoListItemLinkAction) -> Void)? let closeAction: (() -> Void)? - public init(presentationData: ItemListPresentationData, title: String, text: InfoListItemText, style: ItemListStyle, hasDecorations: Bool = true, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { + public init(presentationData: ItemListPresentationData, title: String, text: InfoListItemText, style: ItemListStyle, hasDecorations: Bool = true, isWarning: Bool = false, linkAction: ((InfoListItemLinkAction) -> Void)? = nil, closeAction: (() -> Void)?) { self.presentationData = presentationData self.title = title self.text = text self.style = style self.hasDecorations = hasDecorations + self.isWarning = isWarning self.linkAction = linkAction self.closeAction = closeAction } @@ -210,16 +212,18 @@ public class InfoItemNode: ListViewItemNode { let rightInset: CGFloat = 16.0 + params.rightInset let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize) + let smallerTextFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 15.0) let textFont = Font.regular(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) let textBoldFont = Font.semibold(item.presentationData.fontSize.itemListBaseLabelFontSize / 14.0 * 16.0) let badgeFont = Font.regular(15.0) + let largeBadgeFont = Font.regular(24.0) var updatedTheme: PresentationTheme? var updatedBadgeImage: UIImage? var updatedCloseIcon: UIImage? - let badgeDiameter: CGFloat = 22.0 + let badgeDiameter: CGFloat = item.isWarning ? 30.0 : 22.0 if currentItem?.presentationData.theme !== item.presentationData.theme { updatedTheme = item.presentationData.theme updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.presentationData.theme.list.itemDestructiveColor) @@ -251,12 +255,12 @@ public class InfoItemNode: ListViewItemNode { case let .plain(text): attributedText = NSAttributedString(string: text, font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) case let .markdown(text): - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: item.isWarning ? smallerTextFont : textFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), bold: MarkdownAttributeSet(font: textBoldFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: item.presentationData.theme.list.itemAccentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) })) } - let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "!", font: badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: badgeDiameter, height: badgeDiameter), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.isWarning ? "⚠️" : "!", font: item.isWarning ? largeBadgeFont : badgeFont, textColor: item.presentationData.theme.list.itemCheckColors.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - badgeDiameter - 8.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -324,6 +328,7 @@ public class InfoItemNode: ListViewItemNode { } } + strongSelf.badgeNode.isHidden = item.isWarning strongSelf.closeButton.isHidden = item.closeAction == nil strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil @@ -344,7 +349,7 @@ public class InfoItemNode: ListViewItemNode { strongSelf.closeButton.setImage(updatedCloseIcon, for: []) } - strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0), size: CGSize(width: badgeDiameter, height: badgeDiameter)) + strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 15.0 + (item.isWarning ? 4.0 : 0.0)), size: CGSize(width: badgeDiameter, height: badgeDiameter)) strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 2.0 + UIScreenPixel), size: labelLayout.size) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h index 61ae441820..4ba1397776 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoPaintStickersContext.h @@ -52,7 +52,7 @@ - (void)updateZoomScale:(CGFloat)scale; -- (void)setupWithDrawingData:(NSData * _Nullable)drawingData; +- (void)setupWithDrawingData:(NSData * _Nullable)drawingData storeAsClear:(BOOL)storeAsClear; @end diff --git a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m index f021b48590..fe26e7f2b1 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m +++ b/submodules/LegacyComponents/Sources/TGPhotoDrawingController.m @@ -226,7 +226,7 @@ const CGSize TGPhotoPaintingMaxSize = { 1920.0f, 1920.0f }; }; [_paintingWrapperView addSubview:_drawingView]; - [_drawingView setupWithDrawingData:_photoEditor.paintingData.drawingData]; + [_drawingView setupWithDrawingData:_photoEditor.paintingData.drawingData storeAsClear:false]; } _entitiesView.hasSelectionChanged = ^(bool hasSelection) { diff --git a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift index 3eadb5ab9b..6e3afa67ce 100644 --- a/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift +++ b/submodules/PeerInfoUI/Sources/PeerAllowedReactionListController.swift @@ -538,7 +538,7 @@ public func peerAllowedReactionListController( } if initialAllowedReactions != .known(updatedValue) { - let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: updatedValue).start() + let _ = context.engine.peers.updatePeerAllowedReactions(peerId: peerId, allowedReactions: updatedValue, reactionsLimit: nil).start() } }) } diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index f730f6c389..2922f6bb25 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -605,7 +605,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { if url.hasPrefix("https://apps.apple.com/account/subscriptions") { controller.context.sharedContext.applicationBindings.openSubscriptions() } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) } else { let context = controller.context let signal: Signal? diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index a91c7c5621..f0a220d341 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2688,7 +2688,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if url.hasPrefix("https://apps.apple.com/account/subscriptions") { controller.context.sharedContext.applicationBindings.openSubscriptions() } else if url.hasPrefix("https://") || url.hasPrefix("tg://") { - controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://"), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) + controller.context.sharedContext.openExternalUrl(context: controller.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: controller.context.sharedContext.currentPresentationData.with({$0}), navigationController: nil, dismissInput: {}) } else { let context = controller.context let signal: Signal? diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index f58c6ef9b8..7b5e40c25d 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -1542,7 +1542,11 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { }) } } - self.isCollapsing = false + if self.isCollapsing { + Queue.mainQueue().justDispatch { + self.isCollapsing = false + } + } } transition.updateFrame(node: self.backgroundNode, frame: visualBackgroundFrame, beginWithCurrentState: true) @@ -2711,7 +2715,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { case .ended: let point = recognizer.location(in: self.view) - if self.isExpanded { + if self.isExpanded || self.isCollapsing { return } if let expandItemView = self.expandItemView, expandItemView.bounds.contains(self.view.convert(point, to: self.expandItemView)) { diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index c324b45db0..2ce353ed87 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -222,7 +222,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: { + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift index 7469514140..4dfabf4ada 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemePreviewControllerNode.swift @@ -371,7 +371,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate { }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index caf044719f..a981865d2b 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -627,5 +627,9 @@ private func getAverageColor(image: UIImage) -> UIColor? { sumA /= UInt64(blurredWidth * blurredHeight) sumA = 255 - return UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) + var color = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0) + if color.lightness > 0.8 { + color = color.withMultipliedBrightnessBy(0.8) + } + return color } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index d8d8c9fcb2..7644877303 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -540,8 +540,11 @@ private final class StickerPackContainer: ASDisplayNode { f(.default) if let strongSelf = self { - let _ = strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred).start(next: { _ in - + let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let self, let contorller = self.controller, case .generic = result { + contorller.present(UndoOverlayController(presentationData: self.presentationData, content: .sticker(context: context, file: item.file, loop: true, title: nil, text: !isStarred ? self.presentationData.strings.Conversation_StickerAddedToFavorites : self.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) + } }) } }))) @@ -967,7 +970,9 @@ private final class StickerPackContainer: ASDisplayNode { let buttonColor: UIColor var buttonFont: UIFont = Font.semibold(17.0) - if let controller = self.controller, let _ = controller.mainActionTitle { + if self.isEditing { + buttonColor = self.presentationData.theme.list.itemCheckColors.foregroundColor + } else if let controller = self.controller, let _ = controller.mainActionTitle { buttonColor = self.presentationData.theme.list.itemCheckColors.foregroundColor } else { switch currentContents { @@ -1037,6 +1042,10 @@ private final class StickerPackContainer: ASDisplayNode { if let (layout, _, _, _) = self.validLayout { self.updateLayout(layout: layout, transition: .animated(duration: 0.3, curve: .easeInOut)) } + + if isEditing { + self.expandIfNeeded(force: true) + } } @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { @@ -2122,14 +2131,18 @@ private final class StickerPackContainer: ASDisplayNode { return } - if self.currentEntries.count >= 15, self.controller?.expandIfNeeded == true, !self.didAutomaticExpansion { - self.didAutomaticExpansion = true - self.gridNode.autoscroll(toOffset: CGPoint(x: 0.0, y: max(0.0, self.gridNode.scrollView.contentSize.height - self.gridNode.scrollView.contentInset.top - self.gridNode.scrollView.bounds.height)), duration: 0.4) - self.skipNextGridLayoutUpdate = true - } + self.expandIfNeeded() }) } + private func expandIfNeeded(force: Bool = false) { + if self.currentEntries.count >= 15, force || (self.controller?.expandIfNeeded == true && !self.didAutomaticExpansion) { + self.didAutomaticExpansion = true + self.gridNode.autoscroll(toOffset: CGPoint(x: 0.0, y: max(0.0, self.gridNode.scrollView.contentSize.height - self.gridNode.scrollView.contentInset.top - self.gridNode.scrollView.bounds.height)), duration: 0.4) + self.skipNextGridLayoutUpdate = true + } + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { if !self.backgroundNode.bounds.contains(self.convert(point, to: self.backgroundNode)) { diff --git a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift index 09de0f7413..2e6b1ad411 100644 --- a/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPeekUI/Sources/StickerPreviewPeekContent.swift @@ -17,9 +17,32 @@ import ReactionSelectionNode import EntityKeyboard public enum StickerPreviewPeekItem: Equatable { + public static func == (lhs: StickerPreviewPeekItem, rhs: StickerPreviewPeekItem) -> Bool { + switch lhs { + case let .pack(lhsPack): + if case let .pack(rhsPack) = rhs, lhsPack == rhsPack { + return true + } else { + return false + } + case let .found(lhsItem): + if case let .found(rhsItem) = rhs, lhsItem == rhsItem { + return true + } else { + return false + } + case let .portal(lhsPortal): + if case let .portal(rhsPortal) = rhs, lhsPortal === rhsPortal { + return true + } else { + return false + } + } + } + case pack(TelegramMediaFile) case found(FoundStickerItem) - case image(UIImage) + case portal(PortalView) public var file: TelegramMediaFile? { switch self { @@ -27,7 +50,7 @@ public enum StickerPreviewPeekItem: Equatable { return file case let .found(item): return item.file - case .image: + case .portal: return nil } } @@ -112,6 +135,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC public var imageNode: TransformImageNode public var animationNode: AnimatedStickerNode? public var additionalAnimationNode: AnimatedStickerNode? + private let portalWrapperNode: ASDisplayNode private let effectDisposable = MetaDisposable() @@ -172,11 +196,14 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC } self.imageNode.setSignal(chatMessageSticker(account: context.account, userLocation: .other, file: file, small: false, fetched: true)) - } else if case let .image(image) = item { - self.imageNode.contents = image.cgImage + } else if case .portal = item { self._ready.set(.single(true)) } + self.portalWrapperNode = ASDisplayNode() + self.portalWrapperNode.clipsToBounds = true + self.portalWrapperNode.isUserInteractionEnabled = false + super.init() self.isUserInteractionEnabled = false @@ -202,6 +229,8 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.addSubnode(additionalAnimationNode) } + self.addSubnode(self.portalWrapperNode) + if let animationNode = self.animationNode { animationNode.started = { [weak self] in guard let strongSelf = self else { @@ -223,6 +252,15 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC self.effectDisposable.dispose() } + public override func didLoad() { + super.didLoad() + + if case let .portal(portalView) = self.item { + self.portalWrapperNode.view.addSubview(portalView.view) + } + } + + public func ready() -> Signal { return self._ready.get() } @@ -271,6 +309,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC centerOffset = imageFrame.minX - originalImageFrame.minX } self.imageNode.frame = imageFrame + if let animationNode = self.animationNode { animationNode.frame = imageFrame animationNode.updateLayout(size: imageSize) @@ -281,6 +320,17 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC } } + if case let .portal(portalView) = self.item { + self.portalWrapperNode.bounds = CGRect(origin: .zero, size: imageFrame.size) + self.portalWrapperNode.position = imageFrame.center + self.portalWrapperNode.cornerRadius = imageFrame.size.width / 8.0 + + portalView.view.center = CGPoint(x: imageFrame.size.width / 2.0, y: imageFrame.size.height / 2.0) + + let scale = 180.0 / (size.width * 1.04) + portalView.view.transform = CGAffineTransformMakeScale(scale, scale) + } + self.textNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - textSize.width) / 2.0) - centerOffset, y: -textSize.height - textSpacing), size: textSize) if self.item.file?.isCustomEmoji == true || textSize.height.isZero { diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index 579fd79fec..9508e23506 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -675,7 +675,7 @@ func _internal_updatePeerAllowedReactions(account: Account, peerId: PeerId, allo } var flags: Int32 = 0 - if let reactionsLimit { + if let _ = reactionsLimit { flags |= (1 << 0) } diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index d06828ba16..5823c214b9 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -15,6 +15,7 @@ public enum ServerProvidedSuggestion: String { case xmasPremiumGift = "PREMIUM_CHRISTMAS" case setupBirthday = "BIRTHDAY_SETUP" case todayBirthdays = "BIRTHDAY_CONTACTS_TODAY" + case gracePremium = "PREMIUM_GRACE" } private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set]>([:]) diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 8f89eb64a8..d82c7bd409 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -622,6 +622,8 @@ public final class ChatInlineSearchResultsListComponent: Component { }, openPremiumGift: { _ in }, + openPremiumManagement: { + }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index febed02523..d0e9c13b51 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2782,7 +2782,7 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) - |> deliverOnMainQueue).start(next: { result in + |> deliverOnMainQueue).start(next: { result in switch result { case .generic: interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift index fe6a48cd38..8340ea81e9 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/PaneSearchContainerNode.swift @@ -124,7 +124,7 @@ public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainer maybeFile = foundItem.file case let .pack(fileValue): maybeFile = fileValue - case .image: + case .portal: break } } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal index 5a9790436c..584c09e7d6 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal @@ -1,5 +1,6 @@ #include #include "EditorCommon.h" +#include "EditorUtils.h" using namespace metal; @@ -18,11 +19,6 @@ typedef struct { } VertexData; -float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius) { - float2 q = abs(uv - position) - size + radius; - return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; -} - fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]], texture2d mask [[texture(1)]], diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorOutline.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorOutline.metal new file mode 100644 index 0000000000..49cc4e835f --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorOutline.metal @@ -0,0 +1,49 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +kernel void morphologyMaximumFilter(texture2d inputTexture [[texture(0)]], + texture2d outputTexture [[texture(1)]], + constant float& radius [[buffer(0)]], + uint2 gid [[thread_position_in_grid]]) { + uint2 size = uint2(inputTexture.get_width(), inputTexture.get_height()); + uint2 pos = gid; + + float maxIntensity = 0.0; + int kernelRadius = int(radius); + + for (int y = -kernelRadius; y <= kernelRadius; ++y) { + for (int x = -kernelRadius; x <= kernelRadius; ++x) { + uint2 samplePos = pos + uint2(x, y); + + if (samplePos.x >= 0 && samplePos.y >= 0 && samplePos.x < size.x && samplePos.y < size.y) { + float intensity = inputTexture.read(samplePos).a; + if (intensity > maxIntensity) { + maxIntensity = intensity; + } + } + } + } + outputTexture.write(maxIntensity, gid); +} + +fragment half4 stickerOutlineFragmentShader(RasterizerData in [[stage_in]], + texture2d sourceTexture [[texture(0)]], + texture2d maskTexture [[texture(1)]] + ) +{ + constexpr sampler colorSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + constexpr sampler maskSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); + + half4 color = sourceTexture.sample(colorSampler, in.texCoord); + half intensity = maskTexture.sample(maskSampler, in.texCoord).r; + + half4 result = half4(intensity, intensity, intensity, max(color.a, intensity)); + result.r = mix(result.r, color.r, color.a); + result.g = mix(result.g, color.g, color.a); + result.b = mix(result.b, color.b, color.a); + + return result; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h index 8e310db2e1..2f77779dbe 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.h @@ -24,3 +24,5 @@ half powerCurve(half inVal, half mag); float pnoise3D(float3 p); float2 coordRot(float2 tc, float angle); + +float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius); diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal index 0069a22274..0ed0409249 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorUtils.metal @@ -202,3 +202,8 @@ float2 coordRot(float2 tc, float angle) { rotY = rotY * 0.5 + 0.5; return float2(rotX, rotY); } + +float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius) { + float2 q = abs(uv - position) - size + radius; + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift index ffaa20618b..35ed01e178 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift @@ -7,52 +7,9 @@ import AccountContext import Photos public final class DrawingMediaEntity: DrawingEntity, Codable { -// public enum Content: Equatable { -// case image(UIImage, PixelDimensions) -// case video(String, PixelDimensions) -// case asset(PHAsset) -// -// var dimensions: PixelDimensions { -// switch self { -// case let .image(_, dimensions), let .video(_, dimensions): -// return dimensions -// case let .asset(asset): -// return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) -// } -// } -// -// public static func == (lhs: Content, rhs: Content) -> Bool { -// switch lhs { -// case let .image(lhsImage, lhsDimensions): -// if case let .image(rhsImage, rhsDimensions) = rhs { -// return lhsImage === rhsImage && lhsDimensions == rhsDimensions -// } else { -// return false -// } -// case let .video(lhsPath, lhsDimensions): -// if case let .video(rhsPath, rhsDimensions) = rhs { -// return lhsPath == rhsPath && lhsDimensions == rhsDimensions -// } else { -// return false -// } -// case let .asset(lhsAsset): -// if case let .asset(rhsAsset) = rhs { -// return lhsAsset.localIdentifier == rhsAsset.localIdentifier -// } else { -// return false -// } -// } -// } -// } - private enum CodingKeys: String, CodingKey { case uuid -// case image -// case videoPath -// case assetId case size -// case width -// case height case referenceDrawingSize case position case scale @@ -61,7 +18,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { } public var uuid: UUID -// public let content: Content public let size: CGSize public var referenceDrawingSize: CGSize @@ -83,14 +39,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { public var isAnimated: Bool { return false -// switch self.content { -// case .image: -// return false -// case .video: -// return true -// case let .asset(asset): -// return asset.mediaType == .video -// } } public var isMedia: Bool { @@ -102,7 +50,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { public init(size: CGSize) { self.uuid = UUID() -// self.content = content self.size = size self.referenceDrawingSize = .zero @@ -116,18 +63,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) self.size = try container.decode(CGSize.self, forKey: .size) -// let width = try container.decode(Int32.self, forKey: .width) -// let height = try container.decode(Int32.self, forKey: .height) -// if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { -// self.content = .video(videoPath, PixelDimensions(width: width, height: height)) -// } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { -// self.content = .image(image, PixelDimensions(width: width, height: height)) -// } else if let _ = try container.decodeIfPresent(String.self, forKey: .assetId) { -// fatalError() -// //self.content = .asset() -// } else { -// fatalError() -// } self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) self.position = try container.decode(CGPoint.self, forKey: .position) self.scale = try container.decode(CGFloat.self, forKey: .scale) @@ -138,18 +73,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.uuid, forKey: .uuid) -// switch self.content { -// case let .video(videoPath, dimensions): -// try container.encode(videoPath, forKey: .videoPath) -// try container.encode(dimensions.width, forKey: .width) -// try container.encode(dimensions.height, forKey: .height) -// case let .image(image, dimensions): -// try container.encodeIfPresent(image.jpegData(compressionQuality: 0.9), forKey: .image) -// try container.encode(dimensions.width, forKey: .width) -// try container.encode(dimensions.height, forKey: .height) -// case let .asset(asset): -// try container.encode(asset.localIdentifier, forKey: .assetId) -// } try container.encode(self.size, forKey: .size) try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) @@ -175,9 +98,6 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { if self.uuid != other.uuid { return false } -// if self.content != other.content { -// return false -// } if self.size != other.size { return false } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index 09856f307f..ae5189ab7a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import Display import Vision import CoreImage import CoreImage.CIFilterBuiltins diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 3bf5d210c6..6b3ab5a0aa 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -193,8 +193,9 @@ public final class MediaEditor { } public private(set) var canCutout: Bool = false - public var canCutoutUpdated: (Bool) -> Void = { _ in } + public var canCutoutUpdated: (Bool, Bool) -> Void = { _, _ in } public var isCutoutUpdated: (Bool) -> Void = { _ in } + public var maskUpdated: (UIImage) -> Void = { _ in } public var classificationUpdated: ([(String, Float)]) -> Void = { _ in } @@ -486,7 +487,8 @@ public final class MediaEditor { if mirror { self.renderer.videoFinishPass.additionalTextureRotation = .rotate0DegreesMirrored } - self.renderer.consume(main: .texture(texture, time), additional: additionalTexture.flatMap { .texture($0, time) }, render: true, displayEnabled: false) + let hasTransparency = imageHasTransparency(image) + self.renderer.consume(main: .texture(texture, time, hasTransparency), additional: additionalTexture.flatMap { .texture($0, time, false) }, render: true, displayEnabled: false) } private func setupSource() { @@ -699,16 +701,24 @@ public final class MediaEditor { textureSource.setMainInput(.image(image)) } - if case .sticker = self.mode, let cgImage = image.cgImage { - if !imageHasTransparency(cgImage) { + if case .sticker = self.mode { + if !imageHasTransparency(image) { let _ = (cutoutStickerImage(from: image, onlyCheck: true) |> deliverOnMainQueue).start(next: { [weak self] result in - guard let self, result != nil else { + guard let self else { return } - self.canCutout = true - self.canCutoutUpdated(true) + let canCutout = result != nil + self.canCutout = canCutout + self.canCutoutUpdated(canCutout, false) }) + } else { + self.canCutout = false + self.canCutoutUpdated(false, true) + + if let maskImage = generateTintedImage(image: image, color: .white, backgroundColor: .black) { + self.maskUpdated(maskImage) + } } let _ = (classifyImage(image) |> deliverOnMainQueue).start(next: { [weak self] classes in @@ -791,6 +801,10 @@ public final class MediaEditor { }) } + public func setOnNextDisplay(_ f: @escaping () -> Void) { + self.renderer.onNextRender = f + } + public func setOnNextAdditionalDisplay(_ f: @escaping () -> Void) { self.renderer.onNextAdditionalRender = f } @@ -916,7 +930,7 @@ public final class MediaEditor { self.updateRenderChain() } - public func setToolValue(_ key: EditorToolKey, value: Any) { + public func setToolValue(_ key: EditorToolKey, value: Any?) { self.updateValues { values in var updatedToolValues = values.toolValues updatedToolValues[key] = value @@ -1706,29 +1720,47 @@ public final class MediaEditor { self.renderer.renderFrame() } + private var mainInputMask: MTLTexture? public func removeSegmentationMask() { self.isCutoutUpdated(false) + self.mainInputMask = nil self.renderer.currentMainInputMask = nil if !self.skipRendering { self.updateRenderChain() } } - public func setSegmentationMask(_ image: UIImage) { + public func setSegmentationMask(_ image: UIImage, andEnable enable: Bool = false, updateCutout: Bool = false) { guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { return } - self.isCutoutUpdated(true) + if updateCutout { + self.isCutoutUpdated(true) + } - //TODO:replace with pixelbuffer - self.renderer.currentMainInputMask = loadTexture(image: image, device: device) + //TODO:replace with pixelbuffer? + self.mainInputMask = loadTexture(image: image, device: device) + if enable { + self.isSegmentationMaskEnabled = true + } + self.renderer.currentMainInputMask = self.isSegmentationMaskEnabled ? self.mainInputMask : nil if !self.skipRendering { self.updateRenderChain() } } + public var isSegmentationMaskEnabled: Bool = true { + didSet { + self.renderer.currentMainInputMask = self.isSegmentationMaskEnabled ? self.mainInputMask : nil + if !self.skipRendering { + self.updateRenderChain() + } + } + } + + public func processImage(with f: @escaping (UIImage, UIImage?) -> Void) { guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { return diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index ef81a688f4..328bdbcf4e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -52,12 +52,12 @@ private func roundedCornersMaskImage(size: CGSize) -> CIImage { final class MediaEditorComposer { enum Input { - case texture(MTLTexture, CMTime) + case texture(MTLTexture, CMTime, Bool) case videoBuffer(VideoPixelBuffer) var timestamp: CMTime { switch self { - case let .texture(_, timestamp): + case let .texture(_, timestamp, _): return timestamp case let .videoBuffer(videoBuffer): return videoBuffer.timestamp @@ -66,8 +66,8 @@ final class MediaEditorComposer { var rendererInput: MediaEditorRenderer.Input { switch self { - case let .texture(texture, time): - return .texture(texture, time) + case let .texture(texture, time, hasTransparency): + return .texture(texture, time, hasTransparency) case let .videoBuffer(videoBuffer): return .videoBuffer(videoBuffer) } @@ -113,7 +113,7 @@ final class MediaEditorComposer { self.renderer.addRenderChain(self.renderChain) if values.isSticker { - self.maskImage = roundedCornersMaskImage(size: CGSize(width: 1080.0, height: 1080.0)) + self.maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) } if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) { @@ -202,7 +202,7 @@ public func makeEditorImageComposition(context: CIContext, postbox: Postbox, inp var maskImage: CIImage? if values.isSticker { - maskImage = roundedCornersMaskImage(size: CGSize(width: 1080.0, height: 1080.0)) + maskImage = roundedCornersMaskImage(size: CGSize(width: floor(1080.0 * 0.97), height: floor(1080.0 * 0.97))) } if let drawing = values.drawing, let image = CIImage(image: drawing, options: [.colorSpace: colorSpace]) { @@ -274,7 +274,8 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII resultImage = resultImage.transformed(by: CGAffineTransform(translationX: dimensions.width / 2.0, y: dimensions.height / 2.0)) if values.isSticker { let minSize = min(dimensions.width, dimensions.height) - resultImage = resultImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -(dimensions.height - minSize) / 2.0)).cropped(to: CGRect(origin: .zero, size: CGSize(width: minSize, height: minSize))) + let scaledSize = CGSize(width: floor(minSize * 0.97), height: floor(minSize * 0.97)) + resultImage = resultImage.transformed(by: CGAffineTransform(translationX: -(dimensions.width - scaledSize.width) / 2.0, y: -(dimensions.height - scaledSize.height) / 2.0)).cropped(to: CGRect(origin: .zero, size: scaledSize)) } else if values.isStory { resultImage = resultImage.cropped(to: CGRect(origin: .zero, size: dimensions)) } else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift index ad228ca79d..db9624258d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift @@ -6,13 +6,15 @@ final class MediaEditorRenderChain { let sharpenPass = SharpenRenderPass() let blurPass = BlurRenderPass() let adjustmentsPass = AdjustmentsRenderPass() + let stickerOutlinePass = StickerOutlineRenderPass() var renderPasses: [RenderPass] { return [ self.enhancePass, self.sharpenPass, self.blurPass, - self.adjustmentsPass + self.adjustmentsPass, + self.stickerOutlinePass ] } @@ -139,6 +141,12 @@ final class MediaEditorRenderChain { } else { self.adjustmentsPass.adjustments.hasCurves = 0.0 } + case .stickerOutline: + if let value = value as? Float { + self.stickerOutlinePass.value = value + } else { + self.stickerOutlinePass.value = 0.0 + } } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index c4bddd4725..0a3cf07c45 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -59,12 +59,12 @@ protocol RenderTarget: AnyObject { final class MediaEditorRenderer { enum Input { - case texture(MTLTexture, CMTime) + case texture(MTLTexture, CMTime, Bool) case videoBuffer(VideoPixelBuffer) var timestamp: CMTime { switch self { - case let .texture(_, timestamp): + case let .texture(_, timestamp, _): return timestamp case let .videoBuffer(videoBuffer): return videoBuffer.timestamp @@ -183,13 +183,18 @@ final class MediaEditorRenderer { private func combinedTextureFromCurrentInputs(device: MTLDevice, commandBuffer: MTLCommandBuffer, textureCache: CVMetalTextureCache) -> MTLTexture? { var mainTexture: MTLTexture? var additionalTexture: MTLTexture? + var hasTransparency = false - func textureFromInput(_ input: MediaEditorRenderer.Input, videoInputPass: VideoInputPass) -> MTLTexture? { + func textureFromInput(_ input: MediaEditorRenderer.Input, videoInputPass: VideoInputPass) -> (MTLTexture, Bool)? { switch input { - case let .texture(texture, _): - return texture + case let .texture(texture, _, hasTransparency): + return (texture, hasTransparency) case let .videoBuffer(videoBuffer): - return videoInputPass.processPixelBuffer(videoBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer) + if let buffer = videoInputPass.processPixelBuffer(videoBuffer, textureCache: textureCache, device: device, commandBuffer: commandBuffer) { + return (buffer, false) + } else { + return nil + } } } @@ -197,13 +202,16 @@ final class MediaEditorRenderer { return nil } - mainTexture = textureFromInput(mainInput, videoInputPass: self.mainVideoInputPass) - if let additionalInput = self.currentAdditionalInput { - additionalTexture = textureFromInput(additionalInput, videoInputPass: self.additionalVideoInputPass) + if let (texture, transparency) = textureFromInput(mainInput, videoInputPass: self.mainVideoInputPass) { + mainTexture = texture + hasTransparency = transparency + } + if let additionalInput = self.currentAdditionalInput, let (texture, _) = textureFromInput(additionalInput, videoInputPass: self.additionalVideoInputPass) { + additionalTexture = texture } if let mainTexture { - return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) + return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, hasTransparency: hasTransparency, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 4406259f0b..2b466434e1 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -21,6 +21,7 @@ public enum EditorToolKey: Int32, CaseIterable { case highlightsTint case blur case curves + case stickerOutline static let adjustmentToolsKeys: [EditorToolKey] = [ .enhance, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 7939395228..638b12d10d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -4,6 +4,7 @@ import MetalKit import SwiftSignalKit import TelegramCore import Postbox +import ImageTransparency enum ExportWriterStatus { case unknown @@ -629,7 +630,7 @@ public final class MediaEditorVideoExport { } } if case let .image(image) = self.subject, let texture = self.composer?.textureForImage(image) { - mainInput = .texture(texture, self.imageArguments?.position ?? .zero) + mainInput = .texture(texture, self.imageArguments?.position ?? .zero, imageHasTransparency(image)) if !updatedProgress, let imageArguments = self.imageArguments, let duration = self.durationValue { let progress = imageArguments.position.seconds / duration.seconds diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift new file mode 100644 index 0000000000..90b2319b04 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift @@ -0,0 +1,62 @@ +import Foundation +import Metal +import simd +import CoreImage + +final class StickerOutlineRenderPass: RenderPass { + var value: simd_float1 = 0.0 + + var context: CIContext? + var maskFilter: CIFilter? + + private var outputTexture: MTLTexture? + + func setup(device: MTLDevice, library: MTLLibrary) { + self.context = CIContext(mtlDevice: device, options: [.workingColorSpace : CGColorSpaceCreateDeviceRGB()]) + } + + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard self.value > 0.005, let context = self.context else { + return input + } + + if self.maskFilter == nil { + self.maskFilter = CIFilter(name: "CIMorphologyMaximum") + } + if self.outputTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = input.width + textureDescriptor.height = input.height + textureDescriptor.pixelFormat = input.pixelFormat + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return nil + } + self.outputTexture = texture + texture.label = "outlineOutputTexture" + } + guard let maskFilter = self.maskFilter, let image = CIImage(mtlTexture: input) else { + return input + } + + maskFilter.setValue(self.value * 30, forKey: kCIInputRadiusKey) + maskFilter.setValue(image, forKey: kCIInputImageKey) + + guard let eroded = maskFilter.outputImage, let outputTexture = self.outputTexture else { + return input + } + + if #available(iOS 13.0, *) { + let colorized = CIBlendKernel.sourceAtop.apply(foreground: .white, background: eroded)!.cropped(to: eroded.extent) + let resultImage = image.composited(over: colorized) + + let renderDestination = CIRenderDestination(mtlTexture: outputTexture, commandBuffer: commandBuffer) + _ = try? context.startTask(toRender: resultImage, to: renderDestination) + return outputTexture + } else { + return input + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index 4e7a97f844..950e47fb5e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -2,6 +2,7 @@ import Foundation import AVFoundation import Metal import MetalKit +import ImageTransparency final class UniversalTextureSource: TextureSource { enum Input { @@ -152,6 +153,7 @@ private protocol InputContext { private class ImageInputContext: InputContext { fileprivate var input: Input private var texture: MTLTexture? + private var hasTransparency = false init(input: Input, renderTarget: RenderTarget, queue: DispatchQueue) { guard case let .image(image) = input else { @@ -161,10 +163,11 @@ private class ImageInputContext: InputContext { if let device = renderTarget.mtlDevice { self.texture = loadTexture(image: image, device: device) } + self.hasTransparency = imageHasTransparency(image) } func output(time: Double) -> Output? { - return self.texture.flatMap { .texture($0, .zero) } + return self.texture.flatMap { .texture($0, .zero, self.hasTransparency) } } func invalidate() { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index 179671fb00..a1ccfeb9aa 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -204,6 +204,7 @@ final class VideoFinishPass: RenderPass { texture: MTLTexture, textureRotation: TextureRotation, maskTexture: MTLTexture?, + hasTransparency: Bool, position: VideoPosition, roundness: Float, alpha: Float, @@ -238,8 +239,7 @@ final class VideoFinishPass: RenderPass { dimensions: simd_float2(Float(size.width), Float(size.height)), roundness: roundness, alpha: alpha, - isOpaque: maskTexture == nil ? 1.0 : 0.0, - empty: 0 + isOpaque: maskTexture == nil ? 1.0 : 0.0 ) encoder.setFragmentBytes(¶meters, length: MemoryLayout.size, index: 0) encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) @@ -494,6 +494,7 @@ final class VideoFinishPass: RenderPass { func process( input: MTLTexture, inputMask: MTLTexture?, + hasTransparency: Bool, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, @@ -574,6 +575,7 @@ final class VideoFinishPass: RenderPass { texture: transitionVideoState.texture, textureRotation: transitionVideoState.textureRotation, maskTexture: nil, + hasTransparency: false, position: transitionVideoState.position, roundness: transitionVideoState.roundness, alpha: transitionVideoState.alpha, @@ -588,6 +590,7 @@ final class VideoFinishPass: RenderPass { texture: mainVideoState.texture, textureRotation: mainVideoState.textureRotation, maskTexture: inputMask, + hasTransparency: hasTransparency, position: mainVideoState.position, roundness: mainVideoState.roundness, alpha: mainVideoState.alpha, @@ -602,6 +605,7 @@ final class VideoFinishPass: RenderPass { texture: additionalVideoState.texture, textureRotation: additionalVideoState.textureRotation, maskTexture: nil, + hasTransparency: false, position: additionalVideoState.position, roundness: additionalVideoState.roundness, alpha: additionalVideoState.alpha, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index c78b21af71..f051ad34eb 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/StickerPeekUI", "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", "//submodules/TelegramUI/Components/StickerPickerScreen", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index fe959363d5..6475f37d9f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -16,6 +16,7 @@ import Photos import LottieAnimationComponent import MessageInputPanelComponent import DustEffect +import PlainButtonComponent private final class MediaCutoutScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -37,6 +38,30 @@ private final class MediaCutoutScreenComponent: Component { } return true } + + final class State: ComponentState { + enum ImageKey: Hashable { + case done + } + private var cachedImages: [ImageKey: UIImage] = [:] + func image(_ key: ImageKey) -> UIImage { + if let image = self.cachedImages[key] { + return image + } else { + var image: UIImage + switch key { + case .done: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)! + } + cachedImages[key] = image + return image + } + } + } + + func makeState() -> State { + return State() + } public final class View: UIView { private let buttonsContainerView = UIView() @@ -45,7 +70,7 @@ private final class MediaCutoutScreenComponent: Component { private let cancelButton = ComponentView() private let label = ComponentView() private let doneButton = ComponentView() - + private let fadeView = UIView() private var outlineViews: [StickerCutoutOutlineView] = [] @@ -90,10 +115,13 @@ private final class MediaCutoutScreenComponent: Component { component.mediaEditor.processImage { [weak self] originalImage, _ in cutoutImage(from: originalImage, values: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in Queue.mainQueue().async { - if let self, let component = self.component, let result = results.first, let maskImage = result.maskImage { + if let self, let _ = self.component, let result = results.first, let maskImage = result.maskImage, let controller = self.environment?.controller() as? MediaCutoutScreen { if case let .image(mask, _) = maskImage { self.playDissolveAnimation() - component.mediaEditor.setSegmentationMask(mask) + component.mediaEditor.setSegmentationMask(mask, updateCutout: true) + if let maskData = mask.pngData() { + controller.drawingView.setup(withDrawing: maskData) + } } } } @@ -103,13 +131,62 @@ private final class MediaCutoutScreenComponent: Component { HapticFeedback().impact(.medium) } + var initialOutlineValue: Float? func animateInFromEditor() { + guard let controller = self.environment?.controller() as? MediaCutoutScreen else { + return + } + + let mediaEditor = controller.mediaEditor + self.initialOutlineValue = mediaEditor.getToolValue(.stickerOutline) as? Float + mediaEditor.setToolValue(.stickerOutline, value: nil) + mediaEditor.isSegmentationMaskEnabled = false + mediaEditor.setOnNextDisplay { [weak controller] in + if let controller { + controller.previewView.mask = controller.maskWrapperView + } + } + 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) + + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } + + guard [.erase, .restore].contains(controller.mode) else { + return + } + controller.drawingView.isUserInteractionEnabled = true + if case .restore = controller.mode { + let overlayView = controller.overlayView + let backgroundView = controller.backgroundView + overlayView.alpha = 1.0 + overlayView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + backgroundView.alpha = 0.0 + backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } } private var animatingOut = false func animateOutToEditor(completion: @escaping () -> Void) { + guard let controller = self.environment?.controller() as? MediaCutoutScreen else { + return + } + + let mediaEditor = controller.mediaEditor + if let drawingImage = controller.drawingView.drawingImage { + mediaEditor.setSegmentationMask(drawingImage, andEnable: true, updateCutout: false) + } + let initialOutlineValue = self.initialOutlineValue + mediaEditor.setOnNextDisplay { [weak controller, weak mediaEditor] in + controller?.previewView.mask = nil + if let initialOutlineValue { + mediaEditor?.setToolValue(.stickerOutline, value: initialOutlineValue) + } + } + self.animatingOut = true self.cancelButton.view?.isHidden = true @@ -123,7 +200,25 @@ private final class MediaCutoutScreenComponent: Component { }) self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + if let view = self.doneButton.view { + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) + } + self.state?.updated() + + guard [.erase, .restore].contains(controller.mode) else { + return + } + controller.drawingView.isUserInteractionEnabled = false + if case .restore = controller.mode { + let overlayView = controller.overlayView + let backgroundView = controller.backgroundView + overlayView.alpha = 0.0 + overlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + backgroundView.alpha = 1.0 + backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } public func playDissolveAnimation() { @@ -147,10 +242,22 @@ private final class MediaCutoutScreenComponent: Component { controller.requestDismiss(animated: true) } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if let controller = self.environment?.controller() as? MediaCutoutScreen, [.erase, .restore].contains(controller.mode), result == self.previewContainerView { + return nil//controller.previewView.superview + } + return result + } + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment + guard let controller = environment.controller() as? MediaCutoutScreen else { + return .zero + } + let isFirstTime = self.component == nil self.component = component self.state = state @@ -199,11 +306,8 @@ private final class MediaCutoutScreenComponent: Component { size: CGSize(width: 33.0, height: 33.0) ) ), - action: { - guard let controller = environment.controller() as? MediaCutoutScreen else { - return - } - controller.requestDismiss(animated: true) + action: { [weak controller] in + controller?.requestDismiss(animated: true) } )), environment: {}, @@ -220,9 +324,47 @@ private final class MediaCutoutScreenComponent: Component { transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) } + if case .cutout = controller.mode { + } else { + let doneButtonSize = self.doneButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Image( + image: state.image(.done), + size: CGSize(width: 33.0, height: 33.0) + )), + action: { [weak controller] in + controller?.requestDismiss(animated: true) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let doneButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: buttonBottomInset), + size: doneButtonSize + ) + if let doneButtonView = self.doneButton.view { + if doneButtonView.superview == nil { + self.buttonsContainerView.addSubview(doneButtonView) + } + transition.setFrame(view: doneButtonView, frame: doneButtonFrame) + } + } + + let helpText: String + switch controller.mode { + case .cutout: + helpText = "Tap on an object to cut it out" + case .erase: + helpText = "Erase parts of this sticker" + case .restore: + helpText = "Restore parts of this sticker" + } + let labelSize = self.label.update( transition: transition, - component: AnyComponent(Text(text: "Tap on an object to cut it out", font: Font.regular(17.0), color: .white)), + component: AnyComponent(Text(text: helpText, font: Font.regular(17.0), color: UIColor(rgb: 0x8d8d93))), environment: {}, containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0) ) @@ -241,38 +383,41 @@ private final class MediaCutoutScreenComponent: Component { transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) - for view in self.outlineViews { - transition.setFrame(view: view, frame: previewContainerFrame) - } - - let frameWidth = floorToScreenPixels(previewContainerFrame.width * 0.97) - - self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth) - self.fadeView.layer.cornerRadius = frameWidth / 8.0 - - if isFirstTime { - let values = component.mediaEditor.values - component.mediaEditor.processImage { originalImage, editedImage in - cutoutImage(from: originalImage, editedImage: editedImage, values: values, target: .all, completion: { results in - Queue.mainQueue().async { - if !results.isEmpty { - for result in results { - if let extractedImage = result.extractedImage, let maskImage = result.maskImage { - if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage { - let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame) - outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values) - self.insertSubview(outlineView, belowSubview: self.previewContainerView) - self.outlineViews.append(outlineView) + + if case .cutout = controller.mode { + for view in self.outlineViews { + transition.setFrame(view: view, frame: previewContainerFrame) + } + + let frameWidth = floorToScreenPixels(previewContainerFrame.width * 0.97) + self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth) + self.fadeView.layer.cornerRadius = frameWidth / 8.0 + + + if isFirstTime { + let values = component.mediaEditor.values + component.mediaEditor.processImage { originalImage, editedImage in + cutoutImage(from: originalImage, editedImage: editedImage, values: values, target: .all, completion: { results in + Queue.mainQueue().async { + if !results.isEmpty { + for result in results { + if let extractedImage = result.extractedImage, let maskImage = result.maskImage { + if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage { + let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame) + outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values) + self.insertSubview(outlineView, belowSubview: self.previewContainerView) + self.outlineViews.append(outlineView) + } } } + self.state?.updated(transition: .easeInOut(duration: 0.4)) } - self.state?.updated(transition: .easeInOut(duration: 0.4)) } - } - }) + }) + } + } else { + transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0) } - } else { - transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0) } return availableSize } @@ -282,12 +427,12 @@ private final class MediaCutoutScreenComponent: Component { return View() } - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } -public final class MediaCutoutScreen: ViewController { +final class MediaCutoutScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate { private weak var controller: MediaCutoutScreen? private let context: AccountContext @@ -336,6 +481,14 @@ public final class MediaCutoutScreen: ViewController { } } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let result = super.hitTest(point, with: event) + if result === self.view { + return nil + } + return result + } + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { guard let controller = self.controller else { return @@ -412,17 +565,42 @@ public final class MediaCutoutScreen: ViewController { } fileprivate let context: AccountContext + fileprivate let mode: Mode fileprivate let mediaEditor: MediaEditor + fileprivate let maskWrapperView: UIView fileprivate let previewView: MediaEditorPreviewView + fileprivate let drawingView: DrawingView + fileprivate let overlayView: UIView + fileprivate let backgroundView: UIView - public var dismissed: () -> Void = {} + var dismissed: () -> Void = {} private var initialValues: MediaEditorValues - public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) { + enum Mode { + case cutout + case erase + case restore + } + + init( + context: AccountContext, + mode: Mode, + mediaEditor: MediaEditor, + previewView: MediaEditorPreviewView, + maskWrapperView: UIView, + drawingView: DrawingView, + overlayView: UIView, + backgroundView: UIView + ) { self.context = context + self.mode = mode self.mediaEditor = mediaEditor self.previewView = previewView + self.maskWrapperView = maskWrapperView + self.drawingView = drawingView + self.overlayView = overlayView + self.backgroundView = backgroundView self.initialValues = mediaEditor.values.makeCopy() super.init(navigationBarPresentationData: nil) @@ -431,13 +609,21 @@ public final class MediaCutoutScreen: ViewController { self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.statusBar.statusBarStyle = .White + + if let toolState = drawingView.appliedToolState { + if case .erase = mode { + drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .black))) + } else if case .restore = mode { + drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .white))) + } + } } - required public init(coder aDecoder: NSCoder) { + required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public func loadDisplayNode() { + override func loadDisplayNode() { self.displayNode = Node(controller: self) super.displayNodeDidLoad() @@ -451,7 +637,7 @@ public final class MediaCutoutScreen: ViewController { }) } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + override 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 01cf56c725..28fdc19a0d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -46,6 +46,7 @@ import StickerResources import StickerPeekUI import StickerPackEditTitleController import StickerPickerScreen +import UIKitRuntimeUtils private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -64,15 +65,19 @@ final class MediaEditorScreenComponent: Component { } } - enum DrawingScreenType { + enum DrawingScreenType: Equatable { case drawing case text case sticker + case tools + case cutout + case cutoutErase + case cutoutRestore } let context: AccountContext let externalState: ExternalState - let isDisplayingTool: Bool + let isDisplayingTool: DrawingScreenType? let isInteractingWithEntities: Bool let isSavingAvailable: Bool let hasAppeared: Bool @@ -84,12 +89,12 @@ final class MediaEditorScreenComponent: Component { let entityViewForEntity: (DrawingEntity) -> DrawingEntityView? let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void - let openCutout: () -> Void + let cutoutUndo: () -> Void init( context: AccountContext, externalState: ExternalState, - isDisplayingTool: Bool, + isDisplayingTool: DrawingScreenType?, isInteractingWithEntities: Bool, isSavingAvailable: Bool, hasAppeared: Bool, @@ -101,7 +106,7 @@ final class MediaEditorScreenComponent: Component { entityViewForEntity: @escaping (DrawingEntity) -> DrawingEntityView?, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void, - openCutout: @escaping () -> Void + cutoutUndo: @escaping () -> Void ) { self.context = context self.externalState = externalState @@ -117,7 +122,7 @@ final class MediaEditorScreenComponent: Component { self.entityViewForEntity = entityViewForEntity self.openDrawing = openDrawing self.openTools = openTools - self.openCutout = openCutout + self.cutoutUndo = cutoutUndo } static func ==(lhs: MediaEditorScreenComponent, rhs: MediaEditorScreenComponent) -> Bool { @@ -160,6 +165,9 @@ final class MediaEditorScreenComponent: Component { case done case cutout case undo + case erase + case restore + case outline } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -177,9 +185,15 @@ final class MediaEditorScreenComponent: Component { case .tools: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! case .cutout: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)! + image = UIImage(bundleImageName: "Media Editor/Cutout")!.withRenderingMode(.alwaysTemplate) case .undo: - image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)! + image = UIImage(bundleImageName: "Media Editor/CutoutUndo")!.withRenderingMode(.alwaysTemplate) + case .erase: + image = UIImage(bundleImageName: "Media Editor/Erase")!.withRenderingMode(.alwaysTemplate) + case .restore: + image = UIImage(bundleImageName: "Media Editor/Restore")!.withRenderingMode(.alwaysTemplate) + case .outline: + image = UIImage(bundleImageName: "Media Editor/Outline")!.withRenderingMode(.alwaysTemplate) case .done: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -215,7 +229,7 @@ final class MediaEditorScreenComponent: Component { var isPremium = false var isPremiumDisposable: Disposable? - + init(context: AccountContext, mediaEditor: Signal) { self.context = context @@ -269,7 +283,12 @@ final class MediaEditorScreenComponent: Component { private let stickerButton = ComponentView() private let toolsButton = ComponentView() private let doneButton = ComponentView() + private let cutoutButton = ComponentView() + private let undoButton = ComponentView() + private let eraseButton = ComponentView() + private let restoreButton = ComponentView() + private let outlineButton = ComponentView() private let fadeView = UIButton() @@ -706,7 +725,7 @@ final class MediaEditorScreenComponent: Component { let openDrawing = component.openDrawing let openTools = component.openTools - let openCutout = component.openCutout + let cutoutUndo = component.cutoutUndo let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 @@ -725,10 +744,11 @@ final class MediaEditorScreenComponent: Component { controlsBottomInset = -50.0 } } + let previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: previewSize) let topButtonsAlpha: CGFloat = isRecordingAdditionalVideo ? 0.3 : 1.0 let bottomButtonsAlpha: CGFloat = isRecordingAdditionalVideo ? 0.3 : 1.0 - let buttonsAreHidden = component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities + let buttonsAreHidden = component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities let cancelButtonSize = self.cancelButton.update( transition: transition, @@ -988,44 +1008,302 @@ final class MediaEditorScreenComponent: Component { } } - if controller.node.canCutout { - let isCutout = controller.node.isCutout - let cutoutButtonSize = self.cutoutButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(CutoutButtonContentComponent( - backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), - icon: state.image(isCutout ? .undo : .cutout), - title: isCutout ? "Undo Cut Out" : "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 isOutlineActive = false + if let value = mediaEditor?.values.toolValues[.stickerOutline] as? Float, value > 0.0 { + isOutlineActive = true } - let mediaEditor = controller.node.mediaEditor - + if case .stickerEditor = controller.mode { + var stickerButtonsHidden = buttonsAreHidden + if let displayingTool = component.isDisplayingTool, [.cutoutErase, .cutoutRestore].contains(displayingTool) { + stickerButtonsHidden = false + } + let stickerButtonsAlpha = stickerButtonsHidden ? 0.0 : bottomButtonsAlpha + + let stickerFrameWidth = floorToScreenPixels(previewSize.width * 0.97) + let stickerFrameRect = CGRect(origin: CGPoint(x: previewFrame.minX + floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: previewFrame.minY + floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)) + + var hasCutoutButton = false + var hasUndoButton = false + var hasEraseButton = false + var hasRestoreButton = false + var hasOutlineButton = false + + if let canCutout = controller.node.canCutout { + if controller.node.isCutout || controller.node.stickerMaskDrawingView?.internalState.canUndo == true { + hasUndoButton = true + } + if canCutout && !controller.node.isCutout { + hasCutoutButton = true + } else { + hasEraseButton = true + if hasUndoButton { + hasRestoreButton = true + } + } + if hasUndoButton || controller.node.hasTransparency { + hasOutlineButton = true + } + } + + if hasUndoButton { + let undoButtonSize = self.undoButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.undo), + title: "Undo" + )), + effectAlignment: .center, + action: { + cutoutUndo() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let undoButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - undoButtonSize.width) / 2.0), y: stickerFrameRect.minY - 35.0 - undoButtonSize.height), + size: undoButtonSize + ) + if let undoButtonView = self.undoButton.view { + var positionTransition = transition + if undoButtonView.superview == nil { + self.addSubview(undoButtonView) + + undoButtonView.alpha = stickerButtonsAlpha + undoButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + undoButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: undoButtonView, position: undoButtonFrame.center) + undoButtonView.bounds = CGRect(origin: .zero, size: undoButtonFrame.size) + transition.setAlpha(view: undoButtonView, alpha: stickerButtonsAlpha) + } + } else { + if let undoButtonView = self.undoButton.view, undoButtonView.superview != nil { + undoButtonView.alpha = 0.0 + undoButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + undoButtonView.removeFromSuperview() + }) + undoButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + + if hasCutoutButton { + 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: { + openDrawing(.cutout) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + let cutoutButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - cutoutButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0), + size: cutoutButtonSize + ) + if let cutoutButtonView = self.cutoutButton.view { + var positionTransition = transition + 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: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + cutoutButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: cutoutButtonView, position: cutoutButtonFrame.center) + cutoutButtonView.bounds = CGRect(origin: .zero, size: cutoutButtonFrame.size) + transition.setAlpha(view: cutoutButtonView, alpha: stickerButtonsAlpha) + } + } else { + if let cutoutButtonView = self.cutoutButton.view, cutoutButtonView.superview != nil { + cutoutButtonView.alpha = 0.0 + cutoutButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + cutoutButtonView.removeFromSuperview() + }) + cutoutButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + + if hasEraseButton { + let buttonSpacing: CGFloat = hasRestoreButton ? 10.0 : 0.0 + var totalButtonsWidth = buttonSpacing + + let eraseButtonSize = self.eraseButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.erase), + title: "Erase", + minWidth: 160.0, + selected: component.isDisplayingTool == .cutoutErase + )), + effectAlignment: .center, + action: { + openDrawing(.cutoutErase) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + totalButtonsWidth += eraseButtonSize.width + + var buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) + + if hasRestoreButton { + let restoreButtonSize = self.restoreButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.restore), + title: "Restore", + minWidth: 160.0, + selected: component.isDisplayingTool == .cutoutRestore + )), + effectAlignment: .center, + action: { + openDrawing(.cutoutRestore) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + totalButtonsWidth += restoreButtonSize.width + + buttonOriginX = floorToScreenPixels((availableSize.width - totalButtonsWidth) / 2.0) + let restoreButtonFrame = CGRect( + origin: CGPoint(x: buttonOriginX + eraseButtonSize.width + buttonSpacing, y: stickerFrameRect.maxY + 35.0), + size: restoreButtonSize + ) + if let restoreButtonView = self.restoreButton.view { + var positionTransition = transition + if restoreButtonView.superview == nil { + self.addSubview(restoreButtonView) + + restoreButtonView.alpha = stickerButtonsAlpha + restoreButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + restoreButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + restoreButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + positionTransition = .immediate + } + positionTransition.setPosition(view: restoreButtonView, position: restoreButtonFrame.center) + restoreButtonView.bounds = CGRect(origin: .zero, size: restoreButtonFrame.size) + transition.setAlpha(view: restoreButtonView, alpha: stickerButtonsAlpha) + } + } + + let eraseButtonFrame = CGRect( + origin: CGPoint(x: buttonOriginX, y: stickerFrameRect.maxY + 35.0), + size: eraseButtonSize + ) + if let eraseButtonView = self.eraseButton.view { + var positionTransition = transition + if eraseButtonView.superview == nil { + self.addSubview(eraseButtonView) + + eraseButtonView.alpha = stickerButtonsAlpha + eraseButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + eraseButtonView.layer.animateAlpha(from: 0.0, to: stickerButtonsAlpha, duration: 0.2, delay: 0.0) + eraseButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + positionTransition = .immediate + } + positionTransition.setPosition(view: eraseButtonView, position: eraseButtonFrame.center) + eraseButtonView.bounds = CGRect(origin: .zero, size: eraseButtonFrame.size) + transition.setAlpha(view: eraseButtonView, alpha: stickerButtonsAlpha) + } + } else { + if let eraseButtonView = self.eraseButton.view, eraseButtonView.superview != nil { + eraseButtonView.alpha = 0.0 + eraseButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + eraseButtonView.removeFromSuperview() + }) + eraseButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + if !hasRestoreButton { + if let restoreButtonView = self.restoreButton.view, restoreButtonView.superview != nil { + restoreButtonView.alpha = 0.0 + restoreButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + restoreButtonView.removeFromSuperview() + }) + restoreButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + + if hasOutlineButton { + let outlineButtonSize = self.outlineButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(CutoutButtonContentComponent( + backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18), + icon: state.image(.outline), + title: "Add Outline", + minWidth: 160.0, + selected: isOutlineActive + )), + effectAlignment: .center, + action: { [weak self, weak controller] in + guard let self, let mediaEditor = controller?.node.mediaEditor else { + return + } + if let value = mediaEditor.values.toolValues[.stickerOutline] as? Float, value > 0.0 { + mediaEditor.setToolValue(.stickerOutline, value: Float(0.0)) + } else { + mediaEditor.setToolValue(.stickerOutline, value: Float(0.5)) + } + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 44.0) + ) + + let outlineButtonFrame = CGRect( + origin: CGPoint(x: floorToScreenPixels((availableSize.width - outlineButtonSize.width) / 2.0), y: stickerFrameRect.maxY + 35.0 + 40.0 + 16.0), + size: outlineButtonSize + ) + if let outlineButtonView = self.outlineButton.view { + let outlineButtonAlpha = buttonsAreHidden ? 0.0 : bottomButtonsAlpha + var positionTransition = transition + if outlineButtonView.superview == nil { + self.addSubview(outlineButtonView) + + outlineButtonView.alpha = outlineButtonAlpha + outlineButtonView.layer.animateAlpha(from: 0.0, to: outlineButtonAlpha, duration: 0.2, delay: 0.0) + outlineButtonView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) + outlineButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: 64.0), to: .zero, duration: 0.3, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + + positionTransition = .immediate + } + positionTransition.setPosition(view: outlineButtonView, position: outlineButtonFrame.center) + outlineButtonView.bounds = CGRect(origin: .zero, size: outlineButtonFrame.size) + transition.setAlpha(view: outlineButtonView, alpha: outlineButtonAlpha) + } + } else { + if let outlineButtonView = self.outlineButton.view, outlineButtonView.superview != nil { + outlineButtonView.alpha = 0.0 + outlineButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.0, completion: { _ in + outlineButtonView.removeFromSuperview() + }) + outlineButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } + } + } + var timeoutValue: String switch component.privacy.timeout { case 21600: @@ -1165,6 +1443,16 @@ final class MediaEditorScreenComponent: Component { sizeSliderVisible = true isEditingTextEntity = entityView.isEditing sizeValue = textEntity.fontSize + } else if [.cutoutErase, .cutoutRestore].contains(component.isDisplayingTool) { + sizeSliderVisible = true + sizeValue = controller.node.stickerMaskDrawingView?.appliedToolState?.size ?? 0.5 + } else if isOutlineActive { + sizeSliderVisible = true + if let value = mediaEditor?.values.toolValues[.stickerOutline] as? Float { + sizeValue = CGFloat(value) + } else { + sizeValue = 0.5 + } } if case .storyEditor = controller.mode { @@ -1396,7 +1684,7 @@ final class MediaEditorScreenComponent: Component { 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) + transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool != nil || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } if let playerState = state.playerState { @@ -1564,7 +1852,7 @@ final class MediaEditorScreenComponent: Component { 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) + transition.setAlpha(view: scrubberView, alpha: component.isDisplayingTool != nil || 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) @@ -1584,7 +1872,7 @@ final class MediaEditorScreenComponent: Component { } } - let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) + let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool != nil) let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { @@ -2010,8 +2298,16 @@ final class MediaEditorScreenComponent: Component { value: sizeValue ?? 0.5, tag: nil, updated: { [weak self] size in - if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen { - controller.node.interaction?.updateEntitySize(size) + if let self, let environment = self.environment, let controller = environment.controller() as? MediaEditorScreen, let component = self.component { + if let _ = component.selectedEntity { + controller.node.interaction?.updateEntitySize(size) + } else if [.cutoutErase, .cutoutRestore].contains(component.isDisplayingTool), let stickerMaskDrawingView = controller.node.stickerMaskDrawingView { + if let appliedState = stickerMaskDrawingView.appliedToolState { + stickerMaskDrawingView.updateToolState(appliedState.withUpdatedSize(size)) + } + } else { + controller.node.mediaEditor?.setToolValue(.stickerOutline, value: max(0.1, Float(size))) + } self.state?.updated() } }, @@ -2141,6 +2437,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let toolValue: ComponentView fileprivate let previewContainerView: UIView + fileprivate let previewContentContainerView: PortalSourceView private var transitionInView: UIImageView? private let gradientView: UIImageView @@ -2155,6 +2452,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate let selectionContainerView: DrawingSelectionContainerView fileprivate let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView + + fileprivate var stickerMaskWrapperView: UIView + fileprivate var stickerMaskDrawingView: DrawingView? + fileprivate var stickerMaskPreviewView: UIView + var mediaEditor: MediaEditor? fileprivate var mediaEditorPromise = Promise() @@ -2166,9 +2468,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var availableReactionsDisposable: Disposable? private var panGestureRecognizer: UIPanGestureRecognizer? + private var pinchGestureRecognizer: UIPinchGestureRecognizer? + private var rotationGestureRecognizer: UIRotationGestureRecognizer? private var dismissPanGestureRecognizer: UIPanGestureRecognizer? - private var isDisplayingTool = false + private var isDisplayingTool: MediaEditorScreenComponent.DrawingScreenType? = nil private var isInteractingWithEntities = false private var isEnhancing = false @@ -2178,7 +2482,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissed = false private var isDismissBySwipeSuppressed = false - fileprivate var canCutout = false + fileprivate var canCutout: Bool? + fileprivate var hasTransparency = false fileprivate var isCutout = false private (set) var hasAnyChanges = false @@ -2217,6 +2522,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.previewContainerView.layer.cornerCurve = .continuous } + self.previewContentContainerView = PortalSourceView() + self.gradientView = UIImageView() var isStickerEditor = false @@ -2225,7 +2532,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } self.entitiesContainerView = UIView(frame: CGRect(origin: .zero, size: storyDimensions)) - self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: true, isStickerEditor: isStickerEditor) + self.entitiesView = DrawingEntitiesView(context: controller.context, size: storyDimensions, hasBin: !isStickerEditor, isStickerEditor: isStickerEditor) self.entitiesView.getEntityCenterPosition = { return CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) } @@ -2243,6 +2550,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.selectionContainerView = DrawingSelectionContainerView(frame: .zero) self.entitiesView.selectionContainerView = self.selectionContainerView + self.stickerMaskWrapperView = UIView(frame: .zero) + self.stickerMaskWrapperView.backgroundColor = .white + self.stickerMaskWrapperView.isUserInteractionEnabled = false + + self.stickerMaskPreviewView = UIView(frame: .zero) + self.stickerMaskPreviewView.alpha = 0.0 + self.stickerMaskPreviewView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.3) + self.stickerMaskPreviewView.isUserInteractionEnabled = false + self.recording = MediaEditorScreen.Recording(controller: controller) super.init() @@ -2280,8 +2596,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.previewContainerView.addSubview(self.gradientView) } - self.previewContainerView.addSubview(self.previewView) - self.previewContainerView.addSubview(self.entitiesContainerView) + self.previewContainerView.addSubview(self.previewContentContainerView) + + self.previewContentContainerView.addSubview(self.previewView) + self.previewContentContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) self.entitiesView.addSubview(self.drawingView) @@ -2493,9 +2811,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isFromCamera && mediaDimensions.width > mediaDimensions.height { mediaEntity.scale = storyDimensions.height / fittedSize.height } - - self.entitiesView.add(mediaEntity, announce: false) - + let initialValues: MediaEditorValues? if case let .draft(draft, _) = subject { initialValues = draft.values @@ -2510,15 +2826,62 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { initialValues = nil } - - if let entityView = self.entitiesView.getView(for: mediaEntity.uuid) as? DrawingMediaEntityView { - self.entitiesView.sendSubviewToBack(entityView) - entityView.updated = { [weak self, weak mediaEntity] in + + if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView { + let mediaEntitySize = mediaEntityView.bounds.size + let scaledDimensions = subject.dimensions.cgSize.aspectFittedOrSmaller(CGSize(width: 1920, height: 1920)) + let maskDrawingSize = scaledDimensions.aspectFilled(mediaEntitySize) + + let stickerMaskDrawingView = DrawingView(size: scaledDimensions, gestureView: self.previewContainerView) + stickerMaskDrawingView.stateUpdated = { [weak self] _ in + if let self { + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) + } + } + stickerMaskDrawingView.emptyColor = .white + stickerMaskDrawingView.updateToolState(.pen(DrawingToolState.BrushState(color: DrawingColor(color: .black), size: 0.5))) + stickerMaskDrawingView.isUserInteractionEnabled = false + stickerMaskDrawingView.animationsEnabled = false + stickerMaskDrawingView.clearWithEmptyColor() + if let filter = makeLuminanceToAlphaFilter() { + self.stickerMaskWrapperView.layer.filters = [filter] + } + self.stickerMaskWrapperView.addSubview(stickerMaskDrawingView) + self.stickerMaskWrapperView.addSubview(self.stickerMaskPreviewView) + self.stickerMaskDrawingView = stickerMaskDrawingView + + var initialMaskPosition = CGPoint() + var initialMaskScale: CGFloat = 1.0 + + func updateStickerMaskDrawing(position: CGPoint, scale: CGFloat, rotation: CGFloat) { + let maskScale = initialMaskPosition.x * 2.0 / 1080.0 + stickerMaskDrawingView.center = initialMaskPosition.offsetBy(dx: position.x * maskScale, dy: position.y * maskScale) + stickerMaskDrawingView.transform = CGAffineTransform(scaleX: initialMaskScale * scale, y: initialMaskScale * scale).rotated(by: rotation) + } + + Queue.mainQueue().after(0.1) { + let previewSize = self.previewView.bounds.size + self.stickerMaskWrapperView.frame = CGRect(origin: .zero, size: previewSize) + self.stickerMaskPreviewView.frame = CGRect(origin: .zero, size: previewSize) + + let filledSize = maskDrawingSize.aspectFitted(previewSize) + let maskScale = filledSize.width / maskDrawingSize.width + initialMaskScale = maskScale + initialMaskPosition = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0) + stickerMaskDrawingView.bounds = CGRect(origin: .zero, size: maskDrawingSize) + + updateStickerMaskDrawing(position: .zero, scale: 1.0, rotation: 0.0) + } + + self.entitiesView.sendSubviewToBack(mediaEntityView) + mediaEntityView.updated = { [weak self, weak mediaEntity] in if let self, let mediaEntity { let rotationDelta = mediaEntity.rotation - initialRotation let positionDelta = CGPoint(x: mediaEntity.position.x - initialPosition.x, y: mediaEntity.position.y - initialPosition.y) let scaleDelta = mediaEntity.scale / initialScale self.mediaEditor?.setCrop(offset: positionDelta, scale: scaleDelta, rotation: rotationDelta, mirroring: false) + + updateStickerMaskDrawing(position: positionDelta, scale: scaleDelta, rotation: rotationDelta) } } @@ -2554,12 +2917,13 @@ 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 { + mediaEditor.canCutoutUpdated = { [weak self] canCutout, hasTransparency in + guard let self else { return } self.canCutout = canCutout - controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) + self.hasTransparency = hasTransparency + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) } mediaEditor.isCutoutUpdated = { [weak self] isCutout in guard let self else { @@ -2568,6 +2932,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isCutout = isCutout self.requestLayout(forceUpdate: true, transition: .immediate) } + mediaEditor.maskUpdated = { [weak self] mask in + guard let self else { + return + } + if let maskData = mask.pngData() { + self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true) + } + } mediaEditor.classificationUpdated = { [weak self] classes in guard let self else { return @@ -2765,10 +3137,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:))) pinchGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate self.previewContainerView.addGestureRecognizer(pinchGestureRecognizer) + self.pinchGestureRecognizer = pinchGestureRecognizer let rotateGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(self.handleRotate(_:))) rotateGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate self.previewContainerView.addGestureRecognizer(rotateGestureRecognizer) + self.rotationGestureRecognizer = rotateGestureRecognizer let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) tapGestureRecognizer.delegate = self.wrappedGestureRecognizerDelegate @@ -2912,12 +3286,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } if gestureRecognizer === self.dismissPanGestureRecognizer { let location = gestureRecognizer.location(in: self.entitiesView) - if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil { + if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil { return false } return true } else if gestureRecognizer === self.panGestureRecognizer { let location = gestureRecognizer.location(in: self.view) + if location.x < 36.0 { + return false + } if location.x > self.view.frame.width - 44.0 && location.y > self.view.frame.height - 180.0 { return false } @@ -2929,6 +3306,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if self.stickerScreen != nil { return false } + if self.stickerMaskDrawingView?.isUserInteractionEnabled == true { + return false + } + return true + } else if gestureRecognizer === self.pinchGestureRecognizer { + if self.stickerScreen != nil { + return false + } + if self.stickerMaskDrawingView?.isUserInteractionEnabled == true { + return false + } + return true + } else if gestureRecognizer === self.rotationGestureRecognizer { + if self.stickerScreen != nil { + return false + } + if self.stickerMaskDrawingView?.isUserInteractionEnabled == true { + return false + } return true } else { return true @@ -3346,9 +3742,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate destinationView.isHidden = false destinationSnapshotView?.removeFromSuperview() completion() - if let view = self.entitiesView.getView(where: { $0 is DrawingMediaEntityView }) as? DrawingMediaEntityView { - view.previewView = nil - } }) self.previewContainerView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) @@ -3422,8 +3815,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func animateOutToTool(inPlace: Bool = false) { - self.isDisplayingTool = true + func animateOutToTool(tool: MediaEditorScreenComponent.DrawingScreenType, inPlace: Bool = false) { + self.isDisplayingTool = tool let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -3433,7 +3826,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } func animateInFromTool(inPlace: Bool = false) { - self.isDisplayingTool = false + self.isDisplayingTool = nil let transition: Transition = .easeInOut(duration: 0.2) if let view = self.componentHost.view as? MediaEditorScreenComponent.View { @@ -4108,7 +4501,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate bottomSafeInset: layout.intrinsicInsets.bottom, mediaEditor: self.mediaEditorPromise.get(), privacy: controller.state.privacy, - selectedEntity: self.isDisplayingTool ? nil : self.entitiesView.selectedEntityView?.entity, + selectedEntity: self.isDisplayingTool != nil ? nil : self.entitiesView.selectedEntityView?.entity, entityViewForEntity: { [weak self] entity in if let self { return self.entitiesView.getView(for: entity.uuid) @@ -4290,7 +4683,62 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.entitiesView.selectEntity(nil) } self.controller?.present(controller, in: .current) - self.animateOutToTool() + self.animateOutToTool(tool: mode) + case .cutout, .cutoutErase, .cutoutRestore: + if self.isDisplayingTool != nil { + guard self.isDisplayingTool != mode else { + return + } + self.isDisplayingTool = mode + if let state = self.stickerMaskDrawingView?.appliedToolState { + switch mode { + case .cutoutErase: + self.stickerMaskDrawingView?.updateToolState(state.withUpdatedColor(DrawingColor(color: .black))) + case .cutoutRestore: + self.stickerMaskDrawingView?.updateToolState(state.withUpdatedColor(DrawingColor(color: .white))) + default: + break + } + } + self.requestUpdate(transition: .easeInOut(duration: 0.2)) + return + } + + guard let mediaEditor = self.mediaEditor, let stickerMaskDrawingView = self.stickerMaskDrawingView, let stickerBackgroundView = self.stickerBackgroundView else { + return + } + let cutoutMode: MediaCutoutScreen.Mode + switch mode { + case .cutout: + cutoutMode = .cutout + case .cutoutErase: + cutoutMode = .erase + case .cutoutRestore: + cutoutMode = .restore + default: + cutoutMode = .cutout + } + let cutoutController = MediaCutoutScreen( + context: self.context, + mode: cutoutMode, + mediaEditor: mediaEditor, + previewView: self.previewView, + maskWrapperView: self.stickerMaskWrapperView, + drawingView: stickerMaskDrawingView, + overlayView: self.stickerMaskPreviewView, + backgroundView: stickerBackgroundView + ) + cutoutController.dismissed = { [weak self] in + if let self { + self.animateInFromTool(inPlace: true) + } + } + controller.present(cutoutController, in: .window(.root)) + self.animateOutToTool(tool: mode, inPlace: true) + + controller.hapticFeedback.impact(.medium) + case .tools: + break } } }, @@ -4314,35 +4762,42 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } self.controller?.present(controller, in: .window(.root)) - self.animateOutToTool() + self.animateOutToTool(tool: .tools) } }, - openCutout: { [weak self, weak controller] in - if let self, let controller, let mediaEditor = self.mediaEditor { + cutoutUndo: { [weak self, weak controller] in + if let self, let controller, let mediaEditor = self.mediaEditor, let stickerMaskDrawingView = self.stickerMaskDrawingView { if self.entitiesView.hasSelection { self.entitiesView.selectEntity(nil) } - if controller.node.isCutout { - let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) - if let snapshotView { - self.previewView.superview?.addSubview(snapshotView) + if stickerMaskDrawingView.internalState.canUndo { + stickerMaskDrawingView.performAction(.undo) + if let drawingImage = stickerMaskDrawingView.drawingImage { + mediaEditor.setSegmentationMask(drawingImage) } - self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in - snapshotView?.removeFromSuperview() - }) - mediaEditor.removeSegmentationMask() - } else { - let cutoutController = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) - cutoutController.dismissed = { [weak self] in - if let self { - self.animateInFromTool(inPlace: true) + } else if controller.node.isCutout { + let action = { + let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) + if let snapshotView { + self.previewView.superview?.insertSubview(snapshotView, aboveSubview: self.previewView) } + self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in + snapshotView?.removeFromSuperview() + }) + mediaEditor.removeSegmentationMask() + self.stickerMaskDrawingView?.clearWithEmptyColor() + } + + if let value = mediaEditor.getToolValue(.stickerOutline) as? Float, value > 0.0 { + mediaEditor.setToolValue(.stickerOutline, value: nil) + mediaEditor.setOnNextDisplay { + action() + } + } else { + action() } - controller.present(cutoutController, in: .window(.root)) - self.animateOutToTool(inPlace: true) } - controller.hapticFeedback.impact(.medium) } } ) @@ -4413,7 +4868,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate var bottomInputOffset: CGFloat = 0.0 if inputHeight > 0.0 { if self.stickerScreen == nil { - if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { + if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool != nil { bottomInputOffset = inputHeight / 2.0 } else { bottomInputOffset = 0.0 @@ -4427,6 +4882,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize) transition.setFrame(view: self.previewContainerView, frame: previewFrame) + transition.setFrame(view: self.previewContentContainerView, frame: CGRect(origin: .zero, size: previewSize)) transition.setFrame(view: self.previewView, frame: CGRect(origin: .zero, size: previewSize)) let entitiesViewScale = previewSize.width / storyDimensions.width @@ -4437,8 +4893,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) - let stickerFrameWidth = floorToScreenPixels(previewSize.width * 0.97) if let stickerBackgroundView = self.stickerBackgroundView, let stickerOverlayLayer = self.stickerOverlayLayer, let stickerFrameLayer = self.stickerFrameLayer { + let stickerFrameWidth = floorToScreenPixels(previewSize.width * 0.97) stickerOverlayLayer.frame = CGRect(origin: .zero, size: previewSize) let stickerFrameRect = CGRect(origin: CGPoint(x: floorToScreenPixels((previewSize.width - stickerFrameWidth) / 2.0), y: floorToScreenPixels((previewSize.height - stickerFrameWidth) / 2.0)), size: CGSize(width: stickerFrameWidth, height: stickerFrameWidth)) @@ -5759,7 +6215,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate 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 { let values = mediaEditor.values.withUpdatedQualityPreset(.sticker) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -5773,12 +6229,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var stickerRecommendedEmoji: [String] = [] private var stickerSelectedEmoji: [String] = [] private func effectiveStickerEmoji() -> [String] { - guard !self.stickerSelectedEmoji.isEmpty else { + let filtered = self.stickerSelectedEmoji.filter { !$0.isEmpty } + guard !filtered.isEmpty else { return ["🫥"] } - return self.stickerSelectedEmoji + return filtered } - private weak var resultController: PeekController? + private weak var stickerResultController: PeekController? func presentStickerPreview(image: UIImage) { guard let mediaEditor = self.node.mediaEditor else { return @@ -5812,7 +6269,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if isVideo { self.uploadSticker(file, action: .send) } else { - self.resultController?.disappeared = nil + self.stickerResultController?.disappeared = nil self.completion(MediaEditorScreen.Result( media: .sticker(file: file, emoji: emoji), mediaAreas: [], @@ -5917,13 +6374,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.node.entitiesView.selectEntity(nil) } - let peekController = PeekController( + guard let portalView = PortalView(matchPosition: false) else { + return + } + portalView.view.layer.rasterizationScale = UIScreenScale + self.node.previewContentContainerView.addPortal(view: portalView) + + let stickerResultController = PeekController( presentationData: presentationData, content: StickerPreviewPeekContent( context: self.context, theme: presentationData.theme, strings: presentationData.strings, - item: .image(image), + item: .portal(portalView), isCreating: true, selectedEmoji: self.stickerSelectedEmoji, selectedEmojiUpdated: { [weak self] selectedEmoji in @@ -5938,7 +6401,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate sourceView: { [weak self] in if let self { let previewContainerFrame = self.node.previewContainerView.frame - let size = CGSize(width: previewContainerFrame.width, height: previewContainerFrame.width) + let size = CGSize(width: floorToScreenPixels(previewContainerFrame.width * 0.97), height: floorToScreenPixels(previewContainerFrame.width * 0.97)) return (self.view, CGRect(origin: CGPoint(x: previewContainerFrame.midX - size.width / 2.0, y: previewContainerFrame.midY - size.height / 2.0), size: size)) } else { return nil @@ -5946,20 +6409,26 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }, activateImmediately: true ) - peekController.appeared = { [weak self] in + stickerResultController.appeared = { [weak self] in if let self { - self.node.entitiesView.alpha = 0.0 - self.node.previewView.alpha = 0.0 + self.node.previewContentContainerView.alpha = 0.0 + self.node.previewContentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + + let scale = 180.0 / (self.node.previewContentContainerView.bounds.width * 1.04) + self.node.previewContentContainerView.layer.animateSpring(from: 1.0 as NSNumber, to: scale as NSNumber, keyPath: "transform.scale", duration: 0.4, initialVelocity: 0.0, damping: 110.0) } } - peekController.disappeared = { [weak self] in + stickerResultController.disappeared = { [weak self] in if let self { - self.node.entitiesView.alpha = 1.0 - self.node.previewView.alpha = 1.0 + self.node.previewContentContainerView.alpha = 1.0 + self.node.previewContentContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + let scale = 180.0 / (self.node.previewContentContainerView.bounds.width * 1.04) + self.node.previewContentContainerView.layer.animateScale(from: scale, to: 1.0, duration: 0.25) } } - self.resultController = peekController - self.present(peekController, in: .window(.root)) + self.stickerResultController = stickerResultController + self.present(stickerResultController, in: .window(.root)) } private enum StickerAction { @@ -6602,19 +7071,25 @@ private final class DoneButtonContentComponent: CombinedComponent { } } -private final class CutoutButtonContentComponent: CombinedComponent { +final class CutoutButtonContentComponent: CombinedComponent { let backgroundColor: UIColor let icon: UIImage let title: String? - + let minWidth: CGFloat? + let selected: Bool + init( backgroundColor: UIColor, icon: UIImage, - title: String? + title: String?, + minWidth: CGFloat? = nil, + selected: Bool = false ) { self.backgroundColor = backgroundColor self.icon = icon self.title = title + self.minWidth = minWidth + self.selected = selected } static func ==(lhs: CutoutButtonContentComponent, rhs: CutoutButtonContentComponent) -> Bool { @@ -6624,18 +7099,27 @@ private final class CutoutButtonContentComponent: CombinedComponent { if lhs.title != rhs.title { return false } + if lhs.minWidth != rhs.minWidth { + return false + } + if lhs.selected != rhs.selected { + return false + } return true } static var body: Body { let background = Child(BlurredBackgroundComponent.self) + let selection = Child(RoundedRectangle.self) let icon = Child(Image.self) let text = Child(Text.self) return { context in + let textColor: UIColor = context.component.selected ? .black : .white + let iconSize = context.component.icon.size let icon = icon.update( - component: Image(image: context.component.icon, tintColor: .white, size: iconSize), + component: Image(image: context.component.icon, tintColor: textColor, size: iconSize), availableSize: CGSize(width: 180.0, height: 40.0), transition: .immediate ) @@ -6651,14 +7135,17 @@ private final class CutoutButtonContentComponent: CombinedComponent { component: Text( text: titleText, font: Font.with(size: 17.0, weight: .semibold), - color: .white + color: textColor ), availableSize: CGSize(width: 240.0, height: 100.0), transition: .immediate ) let updatedBackgroundWidth = backgroundSize.width + textSpacing + title!.size.width - backgroundSize.width = updatedBackgroundWidth + 32.0 + backgroundSize.width = updatedBackgroundWidth + 18.0 + } + if let minWidth = context.component.minWidth { + backgroundSize.width = max(minWidth, backgroundSize.width) } let background = background.update( @@ -6671,16 +7158,35 @@ private final class CutoutButtonContentComponent: CombinedComponent { .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)) + + if context.component.selected { + let selection = selection.update( + component: RoundedRectangle(color: .white, cornerRadius: backgroundHeight / 2.0), + availableSize: backgroundSize, + transition: .immediate + ) + context.add(selection + .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) + .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) + .clipsToBounds(true) ) } - context.add(icon - .position(CGPoint(x: 36.0, y: backgroundSize.height / 2.0)) - ) + if let title { + let spacing: CGFloat = 7.0 + let totalWidth = icon.size.width + spacing + title.size.width + let originX = floorToScreenPixels((backgroundSize.width - totalWidth) / 2.0) + context.add(icon + .position(CGPoint(x: originX + icon.size.width / 2.0, y: backgroundSize.height / 2.0)) + ) + context.add(title + .position(CGPoint(x: originX + icon.size.width + spacing + title.size.width / 2.0, y: backgroundHeight / 2.0)) + ) + } else { + context.add(icon + .position(CGPoint(x: 36.0, y: backgroundSize.height / 2.0)) + ) + } return backgroundSize } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 9275217a69..413b61c9d3 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -66,13 +66,13 @@ final class StickerCutoutOutlineView: UIView { let randomBeginTime = (previousBeginTime + 4) % 6 previousBeginTime = randomBeginTime - let duration = path.length / 2200.0 + let duration = min(5.0, max(2.0, path.length / 2200.0)) let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") outlineAnimation.path = path.path.cgPath outlineAnimation.duration = duration outlineAnimation.repeatCount = .infinity - outlineAnimation.calculationMode = .cubicPaced + outlineAnimation.calculationMode = .paced outlineAnimation.beginTime = Double(randomBeginTime) self.outlineLayer.add(outlineAnimation, forKey: "emitterPosition") @@ -85,7 +85,7 @@ final class StickerCutoutOutlineView: UIView { lineEmitterCell.color = UIColor.white.cgColor lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage lineEmitterCell.lifetime = 2.2 - lineEmitterCell.birthRate = 1000 + lineEmitterCell.birthRate = 120 lineEmitterCell.scale = 0.14 lineEmitterCell.alphaSpeed = -0.4 @@ -147,8 +147,11 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE let minSide = min(size.width, size.height) let scaledImageSize = image.extent.size.aspectFilled(CGSize(width: minSide, height: minSide)) - var contour = findContours(pixelBuffer: pixelBuffer) + guard !contour.isEmpty else { + return nil + } + contour = simplify(contour, tolerance: 1.4) let path = BezierPath(points: contour, smooth: false) diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift index fdedcdbb92..da8ed7c055 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/PeerAllowedReactionsScreen.swift @@ -242,7 +242,7 @@ final class PeerAllowedReactionsScreenComponent: Component { } else { allowedReactions = .empty } - let applyDisposable = (component.context.engine.peers.updatePeerAllowedReactions(peerId: component.peerId, allowedReactions: allowedReactions) + let applyDisposable = (component.context.engine.peers.updatePeerAllowedReactions(peerId: component.peerId, allowedReactions: allowedReactions, reactionsLimit: nil) |> deliverOnMainQueue).start(error: { [weak self] error in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift index 849e4e8c22..23707c5a9e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift @@ -14,17 +14,20 @@ final class PeerInfoScreenInfoItem: PeerInfoScreenItem { let id: AnyHashable let title: String let text: InfoListItemText + let isWarning: Bool let linkAction: ((InfoListItemLinkAction) -> Void)? init( id: AnyHashable, title: String, text: InfoListItemText, + isWarning: Bool = false, linkAction: ((InfoListItemLinkAction) -> Void)? ) { self.id = id self.title = title self.text = text + self.isWarning = isWarning self.linkAction = linkAction } @@ -64,7 +67,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode { self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor - let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .blocks, hasDecorations: false, linkAction: { link in + let infoItem = InfoListItem(presentationData: ItemListPresentationData(presentationData), title: item.title, text: item.text, style: .blocks, hasDecorations: false, isWarning: item.isWarning, linkAction: { link in item.linkAction?(link) }, closeAction: nil) let params = ListViewItemLayoutParams(width: width, leftInset: safeInsets.left, rightInset: safeInsets.right, availableHeight: 1000.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index ce5141dff6..f269e27aec 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -182,7 +182,7 @@ public final class LoadingOverlayNode: ASDisplayNode { let interaction = ChatListNodeInteraction(context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, activateSearch: {}, peerSelected: { _, _, _, _ in }, disabledPeerSelected: { _, _, _ in }, togglePeerSelected: { _, _ in }, togglePeersSelection: { _, _ in }, additionalCategorySelected: { _ in }, messageSelected: { _, _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, setPeerThreadMuted: { _, _, _ in }, deletePeer: { _, _ in }, deletePeerThread: { _, _ in }, setPeerThreadStopped: { _, _, _ in }, setPeerThreadPinned: { _, _, _ in }, setPeerThreadHidden: { _, _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, toggleThreadsSelection: { _, _ in }, hidePsa: { _ in }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() - }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in + }, present: { _ in }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: {}, openBirthdaySetup: {}, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: {}, openStories: { _, _ in }, dismissNotice: { _ in }, editPeer: { _ in }) @@ -508,6 +508,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod }, openPremiumGift: { _ in }, + openPremiumManagement: { + }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index 1d65c49eaf..95955047a4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -190,6 +190,7 @@ final class TelegramGlobalSettings { let suggestPhoneNumberConfirmation: Bool let suggestPasswordConfirmation: Bool let suggestPasswordSetup: Bool + let premiumGracePeriod: Bool let accountsAndPeers: [(AccountContext, EnginePeer, Int32)] let activeSessionsContext: ActiveSessionsContext? let webSessionsContext: WebSessionsContext? @@ -212,6 +213,7 @@ final class TelegramGlobalSettings { suggestPhoneNumberConfirmation: Bool, suggestPasswordConfirmation: Bool, suggestPasswordSetup: Bool, + premiumGracePeriod: Bool, accountsAndPeers: [(AccountContext, EnginePeer, Int32)], activeSessionsContext: ActiveSessionsContext?, webSessionsContext: WebSessionsContext?, @@ -233,6 +235,7 @@ final class TelegramGlobalSettings { self.suggestPhoneNumberConfirmation = suggestPhoneNumberConfirmation self.suggestPasswordConfirmation = suggestPasswordConfirmation self.suggestPasswordSetup = suggestPasswordSetup + self.premiumGracePeriod = premiumGracePeriod self.accountsAndPeers = accountsAndPeers self.activeSessionsContext = activeSessionsContext self.webSessionsContext = webSessionsContext @@ -817,6 +820,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, suggestPhoneNumberConfirmation: suggestions.contains(.validatePhoneNumber), suggestPasswordConfirmation: suggestions.contains(.validatePassword), suggestPasswordSetup: suggestPasswordSetup, + premiumGracePeriod: suggestions.contains(.gracePremium), accountsAndPeers: accountsAndPeers, activeSessionsContext: accountSessions?.0, webSessionsContext: accountSessions?.2, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 69a9adc75c..c50dec94ec 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -519,6 +519,7 @@ private enum PeerInfoSettingsSection { case powerSaving case businessSetup case profile + case premiumManagement } private enum PeerInfoReportType { @@ -789,7 +790,12 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } if let settings = data.globalSettings { - if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser { + if settings.premiumGracePeriod { + items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: "Your access to Telegram Premium will expire soon!", text: .markdown("Unfortunately, your latest payment didn't come through. To keep your access to exclusive features, please renew the subscription."), isWarning: true, linkAction: nil)) + items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: "Restore Subscription", action: { + interaction.openSettings(.premiumManagement) + })) + } else if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser { let phoneNumber = formatPhoneNumber(context: context, number: peer.phone ?? "") items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).string, text: .markdown(presentationData.strings.Settings_CheckPhoneNumberText), linkAction: { link in if case .tap = link { @@ -9359,8 +9365,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro push(usernameSetupController(context: self.context)) case .addAccount: let _ = (activeAccountsAndPeers(context: context) - |> take(1) - |> deliverOnMainQueue + |> take(1) + |> deliverOnMainQueue ).startStandalone(next: { [weak self] accountAndPeer, accountsAndPeers in guard let strongSelf = self else { return @@ -9419,6 +9425,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro push(energySavingSettingsScreen(context: self.context)) case .businessSetup: push(self.context.sharedContext.makeBusinessSetupScreen(context: self.context)) + case .premiumManagement: + guard let controller = self.controller else { + return + } + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + let url = premiumConfiguration.subscriptionManagementUrl + guard !url.isEmpty else { + return + } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: self.context.sharedContext.currentPresentationData.with({$0}), navigationController: controller.navigationController as? NavigationController, dismissInput: {}) } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index 08c3dfa7f6..97ce5455f1 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -188,6 +188,8 @@ final class GreetingMessageListItemComponent: Component { }, openPremiumGift: { _ in }, + openPremiumManagement: { + }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 33c57f92ff..27beabbb90 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -203,6 +203,8 @@ final class QuickReplySetupScreenComponent: Component { }, openPremiumGift: { _ in }, + openPremiumManagement: { + }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift index 21a8986798..7c4c162569 100644 --- a/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift +++ b/submodules/TelegramUI/Components/Settings/ThemeAccentColorScreen/Sources/ThemeAccentColorControllerNode.swift @@ -865,7 +865,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate }, activateChatPreview: { _, _, _, gesture, _ in gesture?.cancel() }, present: { _ in - }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openActiveSessions: { + }, openForumThread: { _, _ in }, openStorageManagement: {}, openPasswordSetup: {}, openPremiumIntro: {}, openPremiumGift: { _ in }, openPremiumManagement: {}, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in }, openChatFolderUpdates: {}, hideChatFolderUpdates: { diff --git a/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift index e8a0ae4137..08309c8eef 100644 --- a/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift +++ b/submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController/Sources/StickerPackEditTitleController.swift @@ -794,7 +794,7 @@ public func stickerPackEditTitleController(context: AccountContext, forceDark: B }) contentNode.actionNodes.last?.actionEnabled = false contentNode.inputFieldNode.textChanged = { [weak contentNode] title in - contentNode?.actionNodes.last?.actionEnabled = !title.trimmingTrailingSpaces().isEmpty + contentNode?.actionNodes.last?.actionEnabled = title.trimmingTrailingSpaces().count >= 3 } controller.willDismiss = { [weak contentNode] in contentNode?.inputFieldNode.deactivateInput() diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index e881f3ad6a..d54666377c 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -48,19 +48,22 @@ public final class TabSelectorComponent: Component { public let items: [Item] public let selectedId: AnyHashable? public let setSelectedId: (AnyHashable) -> Void + public let transitionFraction: CGFloat? public init( colors: Colors, customLayout: CustomLayout? = nil, items: [Item], selectedId: AnyHashable?, - setSelectedId: @escaping (AnyHashable) -> Void + setSelectedId: @escaping (AnyHashable) -> Void, + transitionFraction: CGFloat? = nil ) { self.colors = colors self.customLayout = customLayout self.items = items self.selectedId = selectedId self.setSelectedId = setSelectedId + self.transitionFraction = transitionFraction } public static func ==(lhs: TabSelectorComponent, rhs: TabSelectorComponent) -> Bool { @@ -76,6 +79,9 @@ public final class TabSelectorComponent: Component { if lhs.selectedId != rhs.selectedId { return false } + if lhs.transitionFraction != rhs.transitionFraction { + return false + } return true } @@ -143,9 +149,14 @@ public final class TabSelectorComponent: Component { } var contentWidth: CGFloat = 0.0 + var previousBackgroundRect: CGRect? var selectedBackgroundRect: CGRect? + var nextBackgroundRect: CGRect? + + let selectedIndex = component.items.firstIndex(where: { $0.id == component.selectedId }) var validIds: [AnyHashable] = [] + var index = 0 for item in component.items { var itemTransition = transition let itemView: VisibleItem @@ -160,13 +171,30 @@ public final class TabSelectorComponent: Component { let itemId = item.id validIds.append(itemId) + var selectionFraction: CGFloat = 0.0 + if let transitionFraction = component.transitionFraction, let selectedIndex { + if item.id == component.selectedId { + selectionFraction = 1.0 - abs(transitionFraction) + } else { + if index == selectedIndex - 1 && transitionFraction < 0.0 { + selectionFraction = abs(transitionFraction) + } else if index == selectedIndex + 1 && transitionFraction > 0.0 { + selectionFraction = abs(transitionFraction) + } + } + } else { + selectionFraction = item.id == component.selectedId ? 1.0 : 0.0 + } + let itemSize = itemView.title.update( transition: .immediate, component: AnyComponent(PlainButtonComponent( - content: AnyComponent(Text( + content: AnyComponent(ItemComponent( text: item.title, font: itemFont, - color: item.id == component.selectedId && isLineSelection ? component.colors.selection : component.colors.foreground + color: component.colors.foreground, + selectedColor: component.colors.selection, + selectionFraction: isLineSelection ? selectionFraction : 0.0 )), effectAlignment: .center, minSize: nil, @@ -192,6 +220,11 @@ public final class TabSelectorComponent: Component { if item.id == component.selectedId { selectedBackgroundRect = itemBackgroundRect } + if selectedBackgroundRect == nil { + previousBackgroundRect = itemBackgroundRect + } else if nextBackgroundRect == nil, itemBackgroundRect != selectedBackgroundRect { + nextBackgroundRect = itemBackgroundRect + } if let itemTitleView = itemView.title.view { if itemTitleView.superview == nil { @@ -202,6 +235,7 @@ public final class TabSelectorComponent: Component { itemTransition.setBounds(view: itemTitleView, bounds: CGRect(origin: CGPoint(), size: itemTitleFrame.size)) itemTransition.setAlpha(view: itemTitleView, alpha: item.id == component.selectedId || isLineSelection ? 1.0 : 0.4) } + index += 1 } var removeIds: [AnyHashable] = [] @@ -217,9 +251,22 @@ public final class TabSelectorComponent: Component { if let selectedBackgroundRect { self.selectionView.alpha = 1.0 - + if isLineSelection { - var mappedSelectionFrame = selectedBackgroundRect.insetBy(dx: 12.0, dy: 0.0) + var effectiveBackgroundRect = selectedBackgroundRect + if let transitionFraction = component.transitionFraction { + if transitionFraction < 0.0 { + if let previousBackgroundRect { + effectiveBackgroundRect = effectiveBackgroundRect.interpolate(with: previousBackgroundRect, fraction: abs(transitionFraction)) + } + } else if transitionFraction > 0.0 { + if let nextBackgroundRect { + effectiveBackgroundRect = effectiveBackgroundRect.interpolate(with: nextBackgroundRect, fraction: abs(transitionFraction)) + } + } + } + + var mappedSelectionFrame = effectiveBackgroundRect.insetBy(dx: 12.0, dy: 0.0) mappedSelectionFrame.origin.y = mappedSelectionFrame.maxY + 6.0 mappedSelectionFrame.size.height = 3.0 transition.setFrame(view: self.selectionView, frame: mappedSelectionFrame) @@ -242,3 +289,94 @@ public final class TabSelectorComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +extension CGRect { + func interpolate(with other: CGRect, fraction: CGFloat) -> CGRect { + return CGRect( + x: self.origin.x * (1.0 - fraction) + (other.origin.x) * fraction, + y: self.origin.y * (1.0 - fraction) + (other.origin.y) * fraction, + width: self.size.width * (1.0 - fraction) + (other.size.width) * fraction, + height: self.size.height * (1.0 - fraction) + (other.size.height) * fraction + ) + } +} + +private final class ItemComponent: CombinedComponent { + let text: String + let font: UIFont + let color: UIColor + let selectedColor: UIColor + let selectionFraction: CGFloat + + init( + text: String, + font: UIFont, + color: UIColor, + selectedColor: UIColor, + selectionFraction: CGFloat + ) { + self.text = text + self.font = font + self.color = color + self.selectedColor = selectedColor + self.selectionFraction = selectionFraction + } + + static func ==(lhs: ItemComponent, rhs: ItemComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.font != rhs.font { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.selectedColor != rhs.selectedColor { + return false + } + if lhs.selectionFraction != rhs.selectionFraction { + return false + } + return true + } + + static var body: Body { + let title = Child(Text.self) + let selectedTitle = Child(Text.self) + + return { context in + let component = context.component + + let title = title.update( + component: Text( + text: component.text, + font: component.font, + color: component.color + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(title + .position(CGPoint(x: title.size.width / 2.0, y: title.size.height / 2.0)) + .opacity(1.0 - component.selectionFraction) + ) + + let selectedTitle = selectedTitle.update( + component: Text( + text: component.text, + font: component.font, + color: component.selectedColor + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(selectedTitle + .position(CGPoint(x: selectedTitle.size.width / 2.0, y: selectedTitle.size.height / 2.0)) + .opacity(component.selectionFraction) + ) + + return title.size + } + } +} diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 89f721d996..f0c41d9058 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -381,13 +381,13 @@ public final class TextFieldComponent: Component { } } - if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage { + if isPNG && images.count == 1, let image = images.first { let maxSide = max(image.size.width, image.size.height) if maxSide.isZero { return false } let aspectRatio = min(image.size.width, image.size.height) / maxSide - if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) { + if isMemoji || (imageHasTransparency(image) && aspectRatio > 0.2) { component.paste(.sticker(image: image, isMemoji: isMemoji)) return false } diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/Contents.json new file mode 100644 index 0000000000..94ac879619 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "erase_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/erase_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/erase_30.pdf new file mode 100644 index 0000000000..a5ccaf221a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/erase_30.pdf @@ -0,0 +1,170 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.198730 0 0.093262 -0.149902 1.097656 1.098145 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.198730 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.399994 3.906128 cm +0.000000 0.000000 0.000000 scn +0.487499 1.293823 m +h +3.731249 3.496948 m +2.324999 4.912574 2.278124 6.496948 3.609374 7.837573 c +12.646874 16.875074 l +13.481250 17.709450 14.662499 17.690699 15.515625 16.837574 c +20.700001 11.662574 l +21.553125 10.809448 21.562500 9.618824 20.728125 8.793823 c +11.690624 -0.253052 l +10.359375 -1.584301 8.775000 -1.528051 7.359375 -0.121801 c +3.731249 3.496948 l +h +13.650000 15.750074 m +6.581250 8.681324 l +12.534375 2.728199 l +19.612499 9.806324 l +19.846874 10.040699 19.856251 10.368824 19.621876 10.603199 c +14.465625 15.759449 l +14.231250 15.993824 13.893750 15.993824 13.650000 15.750074 c +h +4.799999 4.565699 m +8.428124 0.946949 l +9.159374 0.215698 9.993750 0.178198 10.678124 0.871948 c +11.596874 1.790699 l +5.643749 7.743823 l +4.724999 6.825073 l +4.040624 6.131324 4.078125 5.296948 4.799999 4.565699 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 0.399994 3.906128 cm +BT +19.200001 0.000000 0.000000 19.200001 0.487499 1.293823 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q + +endstream +endobj + +9 0 obj + 1098 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Pages 11 0 R + /Type /Catalog + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000002195 00000 n +0000002218 00000 n +0000002393 00000 n +0000002469 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +2530 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/Contents.json new file mode 100644 index 0000000000..61c8447118 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "stroke_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/stroke_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/stroke_30.pdf new file mode 100644 index 0000000000..e9141c5ca5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/stroke_30.pdf @@ -0,0 +1,269 @@ +%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.625000 0.964844 cm +0.000000 0.000000 0.000000 scn +3.106964 2.943411 m +2.698699 3.151850 2.198762 2.989859 1.990323 2.581594 c +1.781884 2.173328 1.943876 1.673391 2.352141 1.464952 c +3.106964 2.943411 l +h +-0.195204 4.012299 m +0.013234 3.604033 0.513171 3.442041 0.921437 3.650480 c +1.329702 3.858919 1.491694 4.358856 1.283255 4.767121 c +-0.195204 4.012299 l +h +0.830000 9.285156 m +0.830000 9.743553 0.458396 10.115156 0.000000 10.115156 c +-0.458396 10.115156 -0.830000 9.743553 -0.830000 9.285156 c +0.830000 9.285156 l +h +-0.830000 12.785156 m +-0.830000 12.326760 -0.458396 11.955156 0.000000 11.955156 c +0.458396 11.955156 0.830000 12.326760 0.830000 12.785156 c +-0.830000 12.785156 l +h +1.283255 17.303192 m +1.491694 17.711456 1.329702 18.211395 0.921437 18.419832 c +0.513172 18.628271 0.013235 18.466280 -0.195204 18.058016 c +1.283255 17.303192 l +h +2.352142 20.605360 m +1.943877 20.396921 1.781885 19.896984 1.990324 19.488720 c +2.198762 19.080454 2.698700 18.918463 3.106965 19.126902 c +2.352142 20.605360 l +h +7.625000 19.580156 m +8.083397 19.580156 8.455000 19.951759 8.455000 20.410156 c +8.455000 20.868553 8.083397 21.240156 7.625000 21.240156 c +7.625000 19.580156 l +h +11.125000 21.240156 m +10.666604 21.240156 10.295000 20.868553 10.295000 20.410156 c +10.295000 19.951759 10.666604 19.580156 11.125000 19.580156 c +11.125000 21.240156 l +h +15.643036 19.126902 m +16.051300 18.918463 16.551239 19.080454 16.759676 19.488720 c +16.968115 19.896984 16.806124 20.396921 16.397860 20.605360 c +15.643036 19.126902 l +h +18.945204 18.058014 m +18.736765 18.466280 18.236828 18.628271 17.828564 18.419832 c +17.420298 18.211393 17.258307 17.711456 17.466745 17.303192 c +18.945204 18.058014 l +h +17.920000 12.785156 m +17.920000 12.326759 18.291603 11.955156 18.750000 11.955156 c +19.208397 11.955156 19.580000 12.326759 19.580000 12.785156 c +17.920000 12.785156 l +h +19.580000 9.285156 m +19.580000 9.743552 19.208397 10.115156 18.750000 10.115156 c +18.291603 10.115156 17.920000 9.743552 17.920000 9.285156 c +19.580000 9.285156 l +h +17.466745 4.767120 m +17.258307 4.358856 17.420298 3.858917 17.828562 3.650480 c +18.236828 3.442041 18.736765 3.604033 18.945204 4.012297 c +17.466745 4.767120 l +h +16.397858 1.464952 m +16.806124 1.673391 16.968115 2.173328 16.759676 2.581593 c +16.551237 2.989859 16.051300 3.151850 15.643035 2.943411 c +16.397858 1.464952 l +h +11.125000 2.490156 m +10.666603 2.490156 10.295000 2.118553 10.295000 1.660156 c +10.295000 1.201759 10.666603 0.830156 11.125000 0.830156 c +11.125000 2.490156 l +h +7.625000 0.830156 m +8.083396 0.830156 8.455000 1.201759 8.455000 1.660156 c +8.455000 2.118553 8.083396 2.490156 7.625000 2.490156 c +7.625000 0.830156 l +h +5.000000 2.490156 m +4.316745 2.490156 3.674134 2.653845 3.106964 2.943411 c +2.352141 1.464952 l +3.147615 1.058826 4.048309 0.830156 5.000000 0.830156 c +5.000000 2.490156 l +h +1.283255 4.767121 m +0.993689 5.334291 0.830000 5.976902 0.830000 6.660156 c +-0.830000 6.660156 l +-0.830000 5.708466 -0.601331 4.807772 -0.195204 4.012299 c +1.283255 4.767121 l +h +0.830000 6.660156 m +0.830000 9.285156 l +-0.830000 9.285156 l +-0.830000 6.660156 l +0.830000 6.660156 l +h +0.830000 12.785156 m +0.830000 15.410156 l +-0.830000 15.410156 l +-0.830000 12.785156 l +0.830000 12.785156 l +h +0.830000 15.410156 m +0.830000 16.093410 0.993689 16.736023 1.283255 17.303192 c +-0.195204 18.058016 l +-0.601331 17.262541 -0.830000 16.361847 -0.830000 15.410156 c +0.830000 15.410156 l +h +3.106965 19.126902 m +3.674134 19.416468 4.316746 19.580156 5.000000 19.580156 c +5.000000 21.240156 l +4.048310 21.240156 3.147616 21.011488 2.352142 20.605360 c +3.106965 19.126902 l +h +5.000000 19.580156 m +7.625000 19.580156 l +7.625000 21.240156 l +5.000000 21.240156 l +5.000000 19.580156 l +h +11.125000 19.580156 m +13.750000 19.580156 l +13.750000 21.240156 l +11.125000 21.240156 l +11.125000 19.580156 l +h +13.750000 19.580156 m +14.433255 19.580156 15.075867 19.416468 15.643036 19.126902 c +16.397860 20.605360 l +15.602386 21.011486 14.701691 21.240156 13.750000 21.240156 c +13.750000 19.580156 l +h +17.466745 17.303192 m +17.756311 16.736021 17.920000 16.093410 17.920000 15.410156 c +19.580000 15.410156 l +19.580000 16.361847 19.351332 17.262541 18.945204 18.058014 c +17.466745 17.303192 l +h +17.920000 15.410156 m +17.920000 12.785156 l +19.580000 12.785156 l +19.580000 15.410156 l +17.920000 15.410156 l +h +17.920000 9.285156 m +17.920000 6.660156 l +19.580000 6.660156 l +19.580000 9.285156 l +17.920000 9.285156 l +h +17.920000 6.660156 m +17.920000 5.976901 17.756311 5.334290 17.466745 4.767120 c +18.945204 4.012297 l +19.351330 4.807771 19.580000 5.708466 19.580000 6.660156 c +17.920000 6.660156 l +h +15.643035 2.943411 m +15.075866 2.653845 14.433254 2.490156 13.750000 2.490156 c +13.750000 0.830156 l +14.701691 0.830156 15.602385 1.058825 16.397858 1.464952 c +15.643035 2.943411 l +h +13.750000 2.490156 m +11.125000 2.490156 l +11.125000 0.830156 l +13.750000 0.830156 l +13.750000 2.490156 l +h +7.625000 2.490156 m +5.000000 2.490156 l +5.000000 0.830156 l +7.625000 0.830156 l +7.625000 2.490156 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 6.267578 6.169922 cm +0.000000 0.000000 0.000000 scn +3.732337 8.830078 m +3.732337 8.001651 3.172693 7.330078 2.482337 7.330078 c +1.791981 7.330078 1.232337 8.001651 1.232337 8.830078 c +1.232337 9.658505 1.791981 10.330078 2.482337 10.330078 c +3.172693 10.330078 3.732337 9.658505 3.732337 8.830078 c +h +0.452934 4.573056 m +0.861247 4.781402 1.361147 4.619296 1.569492 4.210982 c +2.342952 2.695164 3.917479 1.660122 5.732319 1.660122 c +7.544468 1.660122 9.117010 2.692089 9.891705 4.204253 c +10.100713 4.612227 10.600876 4.773520 11.008849 4.564512 c +11.416823 4.355504 11.578116 3.855341 11.369108 3.447367 c +10.321525 1.402535 8.191411 0.000122 5.732319 0.000122 c +3.269572 0.000122 1.136788 1.406699 0.090860 3.456498 c +-0.117485 3.864811 0.044621 4.364711 0.452934 4.573056 c +h +8.982337 7.330078 m +9.672693 7.330078 10.232337 8.001651 10.232337 8.830078 c +10.232337 9.658505 9.672693 10.330078 8.982337 10.330078 c +8.291981 10.330078 7.732337 9.658505 7.732337 8.830078 c +7.732337 8.001651 8.291981 7.330078 8.982337 7.330078 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 6142 +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 +0000006232 00000 n +0000006255 00000 n +0000006428 00000 n +0000006502 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +6561 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/Contents.json new file mode 100644 index 0000000000..711ea8559d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "restore_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/restore_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/restore_30.pdf new file mode 100644 index 0000000000..63ffff1f79 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/restore_30.pdf @@ -0,0 +1,238 @@ +%PDF-1.7 + +1 0 obj + << /Length 2 0 R >> +stream +1.299316 0 0.104004 -0.190430 1.230469 1.183594 d1 + +endstream +endobj + +2 0 obj + 51 +endobj + +3 0 obj + [ 1.299316 ] +endobj + +4 0 obj + << /Length 5 0 R >> +stream +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (FigmaPDF) + /Ordering (FigmaPDF) + /Supplement 0 +>> def +/CMapName /A-B-C def +/CMapType 2 def +1 begincodespacerange +<00> +endcodespacerange +1 beginbfchar +<00> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +endstream +endobj + +5 0 obj + 336 +endobj + +6 0 obj + << /Subtype /Type3 + /CharProcs << /C0 1 0 R >> + /Encoding << /Type /Encoding + /Differences [ 0 /C0 ] + >> + /Widths 3 0 R + /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] + /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] + /Type /Font + /ToUnicode 4 0 R + /FirstChar 0 + /LastChar 0 + /Resources << >> + >> +endobj + +7 0 obj + << /Font << /F1 6 0 R >> >> +endobj + +8 0 obj + << /Length 9 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 -0.600006 2.471802 cm +0.000000 0.000000 0.000000 scn +0.021874 2.728149 m +h +2.468750 2.062525 m +2.018749 2.662525 2.140625 3.206274 2.515625 3.506275 c +3.031250 3.900024 3.696875 3.600025 4.409375 4.134399 c +5.224999 4.743774 4.709374 6.318774 6.171875 7.415649 c +6.781250 7.875025 7.465625 8.090650 8.140625 8.062525 c +8.309375 8.962524 8.815625 9.759399 9.687500 10.640650 c +12.687500 13.668776 19.418751 18.253151 19.971876 18.646900 c +21.621876 19.800026 23.646875 17.793776 22.465626 16.153151 c +22.062500 15.590651 17.468750 8.868774 14.459375 5.868774 c +13.587500 4.987524 12.790625 4.490650 11.890625 4.331274 c +12.031250 3.009399 11.412499 1.706274 10.137500 0.759399 c +7.868750 -0.928101 4.212500 -0.290600 2.468750 2.062525 c +h +20.693750 17.315651 m +20.328125 17.053150 14.946875 13.350025 11.712500 10.481275 c +12.846875 10.050025 13.859375 9.028150 14.290625 7.884399 c +17.168751 11.090650 20.909376 16.546900 21.125000 16.865650 c +21.368750 17.212524 21.012501 17.550026 20.693750 17.315651 c +h +11.056250 6.346900 m +11.196875 6.159400 11.318749 5.971900 11.421875 5.784400 c +12.031250 5.859400 12.584375 6.150024 13.287500 6.825025 c +13.146875 7.950025 11.759375 9.337524 10.643750 9.468775 c +10.006249 8.821899 9.678124 8.240649 9.603125 7.650024 c +10.128125 7.368774 10.625000 6.928149 11.056250 6.346900 c +h +4.381249 2.212524 m +5.693749 0.825024 7.878125 0.815649 9.293750 1.931274 c +10.559375 2.906275 10.878125 4.359400 9.959374 5.550025 c +9.143750 6.600024 7.934375 6.853149 7.062500 6.159400 c +6.012500 5.343775 6.321875 4.021900 5.412499 3.121899 c +5.009375 2.718775 4.484375 2.681274 4.353125 2.559399 c +4.268750 2.475025 4.259375 2.343775 4.381249 2.212524 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -0.600006 2.471802 cm +BT +19.200001 0.000000 0.000000 19.200001 0.021874 2.728149 Tm +/F1 1.000000 Tf +[ (\000) ] TJ +ET +Q +q +1.000000 -0.000000 0.000000 1.000000 -0.000000 11.000000 cm +0.000000 0.000000 0.000000 scn +2.505619 0.000000 m +2.629215 0.000000 2.730338 0.067720 2.764045 0.214447 c +3.067416 1.907449 3.067417 1.918736 4.764046 2.234763 c +4.910113 2.268623 5.000000 2.358917 5.000000 2.494357 c +5.000000 2.641083 4.910113 2.731377 4.764046 2.765237 c +3.067417 3.103837 3.112360 3.115124 2.764045 4.774266 c +2.730338 4.920993 2.640450 5.000000 2.505619 5.000000 c +2.370788 5.000000 2.269665 4.909707 2.247193 4.774266 c +1.887642 3.115124 1.943821 3.103837 0.235956 2.765237 c +0.089889 2.731377 0.000000 2.629797 0.000000 2.494357 c +0.000000 2.370203 0.089889 2.268623 0.235956 2.234763 c +1.943821 1.896163 1.921350 1.907449 2.247193 0.214447 c +2.269665 0.079007 2.370788 0.000000 2.505619 0.000000 c +h +f +n +Q +q +1.000000 -0.000000 0.000000 1.000000 8.000000 18.000000 cm +0.000000 0.000000 0.000000 scn +2.994783 0.000000 m +3.151304 0.000000 3.234783 0.093750 3.255652 0.239583 c +3.631304 2.281250 3.620870 2.322917 5.760000 2.729167 c +5.916522 2.770833 6.000000 2.843750 6.000000 3.000000 c +6.000000 3.156250 5.916522 3.229167 5.760000 3.260417 c +3.631305 3.687500 3.683478 3.729167 3.255652 5.760417 c +3.234783 5.906250 3.151304 6.000000 2.994783 6.000000 c +2.848696 6.000000 2.775652 5.906250 2.733913 5.760417 c +2.306087 3.729167 2.389565 3.687500 0.240000 3.260417 c +0.093913 3.229167 0.000000 3.156250 0.000000 3.000000 c +0.000000 2.843750 0.093913 2.770833 0.240000 2.729167 c +2.389565 2.312500 2.347826 2.281250 2.733913 0.239583 c +2.775652 0.093750 2.848696 0.000000 2.994783 0.000000 c +h +f +n +Q +q +1.000000 -0.000000 0.000000 1.000000 15.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +2.994783 0.000000 m +3.151304 0.000000 3.234783 0.093750 3.255652 0.239583 c +3.631304 2.281250 3.620870 2.322917 5.760000 2.729167 c +5.916522 2.770833 6.000000 2.843750 6.000000 3.000000 c +6.000000 3.156250 5.916522 3.229167 5.760000 3.260417 c +3.631305 3.687500 3.683478 3.729167 3.255652 5.760417 c +3.234783 5.906250 3.151304 6.000000 2.994783 6.000000 c +2.848696 6.000000 2.775652 5.906250 2.733913 5.760417 c +2.306087 3.729167 2.389565 3.687500 0.240000 3.260417 c +0.093913 3.229167 0.000000 3.156250 0.000000 3.000000 c +0.000000 2.843750 0.093913 2.770833 0.240000 2.729167 c +2.389565 2.312500 2.347826 2.281250 2.733913 0.239583 c +2.775652 0.093750 2.848696 0.000000 2.994783 0.000000 c +h +f +n +Q + +endstream +endobj + +9 0 obj + 4292 +endobj + +10 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 7 0 R + /Contents 8 0 R + /Parent 11 0 R + >> +endobj + +11 0 obj + << /Kids [ 10 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +12 0 obj + << /Pages 11 0 R + /Type /Catalog + >> +endobj + +xref +0 13 +0000000000 65535 f +0000000010 00000 n +0000000117 00000 n +0000000138 00000 n +0000000169 00000 n +0000000561 00000 n +0000000583 00000 n +0000000995 00000 n +0000001041 00000 n +0000005389 00000 n +0000005412 00000 n +0000005587 00000 n +0000005663 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 12 0 R + /Size 13 +>> +startxref +5724 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c026ce77c7..1a986a68eb 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -15906,13 +15906,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let images = imageItems as! [UIImage] strongSelf.chatDisplayNode.updateDropInteraction(isActive: false) - if images.count == 1, let image = images.first, let cgImage = image.cgImage { + if images.count == 1, let image = images.first { let maxSide = max(image.size.width, image.size.height) if maxSide.isZero { return } let aspectRatio = min(image.size.width, image.size.height) / maxSide - if (imageHasTransparency(cgImage) && aspectRatio > 0.2) { + if (imageHasTransparency(image) && aspectRatio > 0.2) { strongSelf.enqueueStickerImage(image, isMemoji: false) return } diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index aaab82ea21..06d3d75028 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -283,6 +283,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe }, openPasswordSetup: { }, openPremiumIntro: { }, openPremiumGift: { _ in + }, openPremiumManagement: { }, openActiveSessions: { }, openBirthdaySetup: { }, performActiveSessionAction: { _, _ in diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 3c06926728..3bba233da3 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -4400,13 +4400,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } } - if isPNG && images.count == 1, let image = images.first, let cgImage = image.cgImage { + if isPNG && images.count == 1, let image = images.first { let maxSide = max(image.size.width, image.size.height) if maxSide.isZero { return false } let aspectRatio = min(image.size.width, image.size.height) / maxSide - if isMemoji || (imageHasTransparency(cgImage) && aspectRatio > 0.2) { + if isMemoji || (imageHasTransparency(image) && aspectRatio > 0.2) { self.paste(.sticker(image, isMemoji)) return true } diff --git a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift index a54d468096..6c309a0169 100644 --- a/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/CommandChatInputContextPanelNode.swift @@ -158,6 +158,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable { }, openPremiumGift: { _ in }, + openPremiumManagement: { + }, openActiveSessions: { }, openBirthdaySetup: { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index 606b23013b..cad9a68ecc 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -180,9 +180,19 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) } } + var justUpdated = false return cachedChatTranslationState(engine: context.engine, peerId: peerId) |> mapToSignal { cached in - if let cached, cached.baseLang == baseLang { + let skipCached: Bool + #if DEBUG + skipCached = true + if justUpdated { + return .complete() + } + #else + skipCached = false + #endif + if let cached, cached.baseLang == baseLang, !skipCached { if !dontTranslateLanguages.contains(cached.fromLang) { return .single(cached) } else { @@ -280,6 +290,7 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) } let state = ChatTranslationState(baseLang: baseLang, fromLang: fromLang, toLang: nil, isEnabled: false) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() + justUpdated = true if !dontTranslateLanguages.contains(fromLang) { return state } else {