From 0598c49a8938d9949339d7b5d4831ca6204bc85f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 6 Apr 2024 15:01:03 +0400 Subject: [PATCH 01/14] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../AccountContext/Sources/Premium.swift | 6 +- .../Sources/ChatListController.swift | 12 + .../Sources/ChatListControllerNode.swift | 4 + .../Sources/ChatListSearchListPaneNode.swift | 3 +- .../Sources/ChatListShimmerNode.swift | 2 +- .../Sources/Node/ChatListNode.swift | 22 +- .../Sources/Node/ChatListNodeEntries.swift | 1 + .../Sources/Node/ChatListNoticeItem.swift | 5 + .../Sources/PeekControllerNode.swift | 8 +- .../DrawingUI/Sources/DrawingGesture.swift | 22 +- .../Sources/DrawingMediaEntityView.swift | 31 +- .../DrawingUI/Sources/DrawingScreen.swift | 36 +- .../Sources/DrawingTextEntityView.swift | 4 +- .../DrawingUI/Sources/DrawingView.swift | 104 ++- .../Sources/HashtagSearchController.swift | 3 +- .../Sources/ImageTransparency.swift | 4 +- .../Sources/StickerPreviewPeekContent.swift | 2 +- .../Sources/ItemListController.swift | 74 +- .../Sources/ItemListControllerNode.swift | 138 +++- .../ItemListControllerTabsContentNode.swift | 11 +- .../Sources/Items/ItemListInfoItem.swift | 15 +- .../TGPhotoPaintStickersContext.h | 2 +- .../Sources/TGPhotoDrawingController.m | 2 +- .../PeerAllowedReactionListController.swift | 2 +- .../PremiumUI/Sources/PremiumGiftScreen.swift | 2 +- .../Sources/PremiumIntroScreen.swift | 2 +- .../Sources/ReactionContextNode.swift | 8 +- .../TextSizeSelectionController.swift | 2 +- .../Themes/ThemePreviewControllerNode.swift | 2 +- .../Sources/StickerPackPreviewGridItem.swift | 6 +- .../Sources/StickerPackScreen.swift | 29 +- .../Sources/StickerPreviewPeekContent.swift | 58 +- .../Sources/State/MessageReactions.swift | 2 +- .../TelegramCore/Sources/Suggestions.swift | 1 + ...ChatInlineSearchResultsListComponent.swift | 2 + .../Sources/ChatEntityKeyboardInputNode.swift | 2 +- .../Sources/PaneSearchContainerNode.swift | 2 +- .../MetalResources/EditorDual.metal | 6 +- .../MetalResources/EditorOutline.metal | 49 ++ .../MediaEditor/MetalResources/EditorUtils.h | 2 + .../MetalResources/EditorUtils.metal | 5 + .../Sources/Drawing/DrawingMediaEntity.swift | 80 -- .../Sources/ImageObjectSeparation.swift | 1 + .../MediaEditor/Sources/MediaEditor.swift | 56 +- .../Sources/MediaEditorComposer.swift | 15 +- .../Sources/MediaEditorRenderChain.swift | 10 +- .../Sources/MediaEditorRenderer.swift | 28 +- .../Sources/MediaEditorValues.swift | 1 + .../Sources/MediaEditorVideoExport.swift | 3 +- .../Sources/StickerOutlineRenderPass.swift | 62 ++ .../Sources/UniversalTextureSource.swift | 5 +- .../MediaEditor/Sources/VideoFinishPass.swift | 8 +- .../Components/MediaEditorScreen/BUILD | 1 + .../Sources/MediaCutoutScreen.swift | 272 ++++++- .../Sources/MediaEditorScreen.swift | 758 +++++++++++++++--- .../Sources/StickerCutoutOutlineView.swift | 11 +- .../Sources/PeerAllowedReactionsScreen.swift | 2 +- .../ListItems/PeerInfoScreenInfoItem.swift | 5 +- .../PeerInfoScreenPersonalChannelItem.swift | 4 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 4 + .../Sources/PeerInfoScreen.swift | 22 +- ...aticBusinessMessageListItemComponent.swift | 2 + .../Sources/QuickReplySetupScreen.swift | 2 + .../ThemeAccentColorControllerNode.swift | 2 +- .../StickerPackEditTitleController.swift | 2 +- .../Sources/TabSelectorComponent.swift | 148 +++- .../Sources/TextFieldComponent.swift | 4 +- .../Media Editor/Erase.imageset/Contents.json | 12 + .../Media Editor/Erase.imageset/erase_30.pdf | 170 ++++ .../Outline.imageset/Contents.json | 12 + .../Outline.imageset/stroke_30.pdf | 269 +++++++ .../Restore.imageset/Contents.json | 12 + .../Restore.imageset/restore_30.pdf | 238 ++++++ .../TelegramUI/Sources/ChatController.swift | 4 +- .../ChatSearchResultsContollerNode.swift | 1 + .../Sources/ChatTextInputPanelNode.swift | 4 +- .../CommandChatInputContextPanelNode.swift | 2 + .../TranslateUI/Sources/ChatTranslation.swift | 13 +- 79 files changed, 2478 insertions(+), 463 deletions(-) create mode 100644 submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorOutline.metal create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Erase.imageset/erase_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Outline.imageset/stroke_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/Restore.imageset/restore_30.pdf 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 { From 698a74b6b622ff5762066d15e37c083c2efb4920 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 6 Apr 2024 16:38:31 +0400 Subject: [PATCH 02/14] Various improvements --- .../MediaEditor/Sources/MediaEditor.swift | 2 +- .../Sources/MediaCutoutScreen.swift | 79 +++++++++++++------ .../Sources/MediaEditorScreen.swift | 55 +++++++------ 3 files changed, 88 insertions(+), 48 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 6b3ab5a0aa..13f4cc8701 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -13,7 +13,7 @@ import FastBlur import AccountContext import ImageTransparency -public struct MediaEditorPlayerState { +public struct MediaEditorPlayerState: Equatable { public struct Track: Equatable { public enum Content: Equatable { case video(frames: [UIImage], framesUpdateTimestamp: Double) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 6475f37d9f..9f79ec454c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -159,14 +159,30 @@ private final class MediaCutoutScreenComponent: Component { 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) + + self.updateBackgroundViews() + } + + func updateBackgroundViews() { + guard let controller = self.environment?.controller() as? MediaCutoutScreen else { + return } + let overlayView = controller.overlayView + let backgroundView = controller.backgroundView + + let overlayAlpha: CGFloat + let backgroundAlpha: CGFloat + switch controller.mode { + case .restore: + overlayAlpha = 1.0 + backgroundAlpha = 0.0 + default: + overlayAlpha = 0.0 + backgroundAlpha = 1.0 + } + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setAlpha(view: overlayView, alpha: overlayAlpha) + transition.setAlpha(view: backgroundView, alpha: backgroundAlpha) } private var animatingOut = false @@ -211,14 +227,12 @@ private final class MediaCutoutScreenComponent: Component { 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) - } + + let overlayView = controller.overlayView + let backgroundView = controller.backgroundView + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setAlpha(view: overlayView, alpha: 0.0) + transition.setAlpha(view: backgroundView, alpha: 1.0) } public func playDissolveAnimation() { @@ -489,6 +503,16 @@ final class MediaCutoutScreen: ViewController { return result } + func requestLayout(transition: Transition) { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition) + + if let view = self.componentHost.view as? MediaCutoutScreenComponent.View { + view.updateBackgroundViews() + } + } + } + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { guard let controller = self.controller else { return @@ -565,7 +589,12 @@ final class MediaCutoutScreen: ViewController { } fileprivate let context: AccountContext - fileprivate let mode: Mode + public var mode: Mode { + didSet { + self.updateDrawingState() + self.node.requestLayout(transition: .easeInOut(duration: 0.2)) + } + } fileprivate let mediaEditor: MediaEditor fileprivate let maskWrapperView: UIView fileprivate let previewView: MediaEditorPreviewView @@ -610,13 +639,7 @@ final class MediaCutoutScreen: ViewController { 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))) - } - } + self.updateDrawingState() } required init(coder aDecoder: NSCoder) { @@ -628,6 +651,16 @@ final class MediaCutoutScreen: ViewController { super.displayNodeDidLoad() } + + private func updateDrawingState() { + if let toolState = self.drawingView.appliedToolState { + if case .erase = mode { + self.drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .black))) + } else if case .restore = mode { + self.drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .white))) + } + } + } func requestDismiss(animated: Bool) { self.dismissed() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 28fdc19a0d..275e4b3ea1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -245,8 +245,10 @@ final class MediaEditorScreenComponent: Component { } |> deliverOnMainQueue).start(next: { [weak self] playerState in if let self { - self.playerState = playerState - self.updated() + if self.playerState != playerState { + self.playerState = playerState + self.updated() + } } }) @@ -4421,6 +4423,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate fileprivate var drawingScreen: DrawingScreen? fileprivate var stickerScreen: StickerPickerScreen? + fileprivate weak var cutoutScreen: MediaCutoutScreen? private var defaultToEmoji = false private var previousDrawingData: Data? @@ -4685,28 +4688,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(controller, in: .current) 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: @@ -4718,6 +4699,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate default: cutoutMode = .cutout } + + if self.isDisplayingTool != nil { + guard self.isDisplayingTool != mode else { + return + } + self.isDisplayingTool = mode + self.cutoutScreen?.mode = cutoutMode + self.requestUpdate(transition: .easeInOut(duration: 0.2)) + return + } + guard let mediaEditor = self.mediaEditor, let stickerMaskDrawingView = self.stickerMaskDrawingView, let stickerBackgroundView = self.stickerBackgroundView else { + return + } let cutoutController = MediaCutoutScreen( context: self.context, mode: cutoutMode, @@ -4734,6 +4728,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } controller.present(cutoutController, in: .window(.root)) + self.cutoutScreen = cutoutController self.animateOutToTool(tool: mode, inPlace: true) controller.hapticFeedback.impact(.medium) @@ -4776,6 +4771,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let drawingImage = stickerMaskDrawingView.drawingImage { mediaEditor.setSegmentationMask(drawingImage) } + + if self.isDisplayingTool == .cutoutRestore && !stickerMaskDrawingView.internalState.canUndo && !controller.node.isCutout { + self.cutoutScreen?.mode = .erase + self.isDisplayingTool = .cutoutErase + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) + } } else if controller.node.isCutout { let action = { let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) @@ -4797,6 +4798,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { action() } + + if self.isDisplayingTool == .cutoutRestore { + self.cutoutScreen?.mode = .erase + self.isDisplayingTool = .cutoutErase + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) + } } } } From 7c6651db3484bd8b2e68e39168da03bc1d2ca3e5 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 6 Apr 2024 18:08:20 +0400 Subject: [PATCH 03/14] [WIP] Stickers editor --- .../Sources/UniversalVideoNode.swift | 4 ++ .../Sources/AnimatedStickerNode.swift | 3 ++ .../Sources/DrawingStickerEntityView.swift | 10 +++++ .../Sources/MediaEditorValues.swift | 3 +- .../Sources/MediaEditorVideoExport.swift | 7 +++- .../MediaEditorVideoFFMpegWriter.swift | 41 +++++++++++-------- .../Sources/MediaEditorScreen.swift | 14 +++++++ 7 files changed, 63 insertions(+), 19 deletions(-) diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index 1828a29695..c9333755ff 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -103,6 +103,10 @@ public final class UniversalVideoNode: ASDisplayNode { public private(set) var ownsContentNode: Bool = false public var ownsContentNodeUpdated: ((Bool) -> Void)? + public var duration: Double { + return self.content.duration + } + private let _status = Promise() public var status: Signal { return self._status.get() diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 7de93b69d0..b6b3a76dd3 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -201,6 +201,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke public var frameUpdated: (Int, Int) -> Void = { _, _ in } public private(set) var currentFrameIndex: Int = 0 public private(set) var currentFrameCount: Int = 0 + public private(set) var currentFrameRate: Int = 0 private var playFromIndex: Int? public var frameColorUpdated: ((UIColor) -> Void)? @@ -537,6 +538,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke strongSelf.frameUpdated(frame.index, frame.totalFrames) strongSelf.currentFrameIndex = frame.index strongSelf.currentFrameCount = frame.totalFrames + strongSelf.currentFrameRate = frameRate if frame.isLastFrame { var stopped = false @@ -652,6 +654,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke strongSelf.frameUpdated(frame.index, frame.totalFrames) strongSelf.currentFrameIndex = frame.index strongSelf.currentFrameCount = frame.totalFrames; + strongSelf.currentFrameRate = frameRate if frame.isLastFrame { var stopped = false diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index cd4bb1e43b..a6b7dd50b1 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -86,6 +86,16 @@ public class DrawingStickerEntityView: DrawingEntityView { var currentSize: CGSize? public var updated: () -> Void = {} + public var duration: Double? { + if let animationNode = self.animationNode, animationNode.currentFrameCount > 1 { + return Double(animationNode.currentFrameCount) / Double(animationNode.currentFrameRate) + } else if let videoNode = self.videoNode { + return videoNode.duration + } else { + return nil + } + } + init(context: AccountContext, entity: DrawingStickerEntity) { self.imageNode = TransformImageNode() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 2b466434e1..5c738d60f8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -1637,6 +1637,7 @@ public func recommendedVideoExportConfiguration(values: MediaEditorValues, durat videoSettings: videoSettings, audioSettings: audioSettings, values: values, - frameRate: frameRate + frameRate: frameRate, + preferredDuration: isSticker ? duration: nil ) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 638b12d10d..d6491640f5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -56,17 +56,20 @@ public final class MediaEditorVideoExport { public var audioSettings: [String: Any] public var values: MediaEditorValues public var frameRate: Float + public var preferredDuration: Double? public init( videoSettings: [String: Any], audioSettings: [String: Any], values: MediaEditorValues, - frameRate: Float + frameRate: Float, + preferredDuration: Double? = nil ) { self.videoSettings = videoSettings self.audioSettings = audioSettings self.values = values self.frameRate = frameRate + self.preferredDuration = preferredDuration } var isSticker: Bool { @@ -284,7 +287,7 @@ public final class MediaEditorVideoExport { let duration: CMTime if self.configuration.isSticker { - duration = CMTime(seconds: 3.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + duration = CMTime(seconds: self.configuration.preferredDuration ?? 3.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) } else if let mainAsset { if let trimmedDuration = self.configuration.timeRange?.duration { duration = trimmedDuration diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift index c9b703df92..b42e99a435 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoFFMpegWriter.swift @@ -3,6 +3,7 @@ import UIKit import CoreMedia import FFMpegBinding import ImageDCT +import Accelerate final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter { public static let registerFFMpegGlobals: Void = { @@ -12,6 +13,15 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter { let ffmpegWriter = FFMpegVideoWriter() var pool: CVPixelBufferPool? + + let conversionInfo: vImage_ARGBToYpCbCr + + init() { + var pixelRange = vImage_YpCbCrPixelRange( Yp_bias: 16, CbCr_bias: 128, YpRangeMax: 235, CbCrRangeMax: 240, YpMax: 235, YpMin: 16, CbCrMax: 240, CbCrMin: 16) + var conversionInfo = vImage_ARGBToYpCbCr() + let _ = vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &conversionInfo, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, vImage_Flags(kvImageNoFlags)) + self.conversionInfo = conversionInfo + } func setup(configuration: MediaEditorVideoExport.Configuration, outputPath: String) { let _ = MediaEditorVideoFFMpegWriter.registerFFMpegGlobals @@ -91,28 +101,27 @@ final class MediaEditorVideoFFMpegWriter: MediaEditorVideoExportWriter { } func appendPixelBuffer(_ buffer: CVPixelBuffer, at time: CMTime) -> Bool { - let width = Int32(CVPixelBufferGetWidth(buffer)) - let height = Int32(CVPixelBufferGetHeight(buffer)) - let bytesPerRow = Int32(CVPixelBufferGetBytesPerRow(buffer)) + let width = CVPixelBufferGetWidth(buffer) + let height = CVPixelBufferGetHeight(buffer) + let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer) - let frame = FFMpegAVFrame(pixelFormat: .YUVA, width: width, height: height) + let frame = FFMpegAVFrame(pixelFormat: .YUVA, width: Int32(width), height: Int32(height)) CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags.readOnly) let src = CVPixelBufferGetBaseAddress(buffer) - splitRGBAIntoYUVAPlanes( - src, - frame.data[0], - frame.data[1], - frame.data[2], - frame.data[3], - width, - height, - bytesPerRow, - true, - true - ) + + var srcBuffer = vImage_Buffer(data: src, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: bytesPerRow) + var yBuffer = vImage_Buffer(data: frame.data[0], height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: width) + var uBuffer = vImage_Buffer(data: frame.data[1], height: vImagePixelCount(height / 2), width: vImagePixelCount(width / 2), rowBytes: width / 2) + var vBuffer = vImage_Buffer(data: frame.data[2], height: vImagePixelCount(height / 2), width: vImagePixelCount(width / 2), rowBytes: width / 2) + var aBuffer = vImage_Buffer(data: frame.data[3], height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: width) + + var outInfo = self.conversionInfo + let _ = vImageConvert_ARGB8888To420Yp8_Cb8_Cr8(&srcBuffer, &yBuffer, &uBuffer, &vBuffer, &outInfo, [ 3, 2, 1, 0 ], vImage_Flags(kvImageDoNotTile)) + vImageExtractChannel_ARGB8888(&srcBuffer, &aBuffer, 3, vImage_Flags(kvImageDoNotTile)) + CVPixelBufferUnlockBaseAddress(buffer, CVPixelBufferLockFlags.readOnly) return self.ffmpegWriter.encode(frame) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 275e4b3ea1..4fc58e4315 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6847,6 +6847,20 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if case let .video(video, _) = exportSubject { duration = video.duration.seconds } + if isSticker { + duration = 3.0 + var stickerDurations: [Double] = [] + self.node.entitiesView.eachView { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView { + if let duration = stickerEntityView.duration, duration > 0.0 { + stickerDurations.append(duration) + } + } + } + if !stickerDurations.isEmpty { + duration = stickerDurations.max() ?? 3.0 + } + } let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0, isSticker: isSticker) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).\(fileExtension)" let videoExport = MediaEditorVideoExport(postbox: self.context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: outputPath, textScale: 2.0) From ee2b7be5e27f469f90d51d200eddd28b3576ebc1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 6 Apr 2024 19:53:19 +0400 Subject: [PATCH 04/14] [WIP] Stickers editor --- .../Sources/AccountContext.swift | 2 +- .../Sources/DrawingReactionView.swift | 2 +- .../DrawingUI/Sources/DrawingScreen.swift | 4 +- .../Sources/DrawingStickerEntityView.swift | 6 +- .../Sources/DrawingTextEntityView.swift | 2 +- .../ImportStickerPackControllerNode.swift | 6 +- .../Sources/LegacyPaintStickersContext.swift | 3 +- .../Sources/StickerPackScreen.swift | 24 +++--- .../Network/FetchedMediaResource.swift | 86 +++++++++++++++++++ .../Sources/State/ManagedRecentStickers.swift | 8 +- .../SyncCore/SyncCore_MediaReference.swift | 74 ++++++++++++++++ .../Stickers/ImportStickers.swift | 41 ++++++--- .../Sources/AvatarEditorScreen.swift | 5 +- .../Sources/ChatEntityKeyboardInputNode.swift | 11 ++- .../Drawing/DrawingStickerEntity.swift | 12 +-- .../Sources/MediaEditorComposerEntity.swift | 2 +- .../Sources/MediaEditorScreen.swift | 22 ++--- .../Sources/StickerPickerScreen.swift | 16 +++- .../Sources/SharedAccountContext.swift | 2 +- 19 files changed, 263 insertions(+), 65 deletions(-) diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index ec2cb4a88a..ed954b3c6e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1024,7 +1024,7 @@ public protocol SharedAccountContext: AnyObject { func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any?, UIView?, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeStoryMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController - func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController + func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/DrawingUI/Sources/DrawingReactionView.swift b/submodules/DrawingUI/Sources/DrawingReactionView.swift index a86449a87e..8ecbabc19d 100644 --- a/submodules/DrawingUI/Sources/DrawingReactionView.swift +++ b/submodules/DrawingUI/Sources/DrawingReactionView.swift @@ -189,7 +189,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView { } if case let .file(_, type) = self.stickerEntity.content, case let .reaction(_, style) = type { - self.stickerEntity.content = .file(animation, .reaction(updateReaction.reaction, style)) + self.stickerEntity.content = .file(.standalone(media: animation), .reaction(updateReaction.reaction, style)) } var nodeToTransitionOut: ASDisplayNode? diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 15232b7a07..e5d661d92e 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2893,13 +2893,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U for entity in self.entitiesView.entities { if let sticker = entity as? DrawingStickerEntity, case let .file(file, _) = sticker.content { let coder = PostboxEncoder() - coder.encodeRootObject(file) + coder.encodeRootObject(file.media) stickers.append(coder.makeData()) } else if let text = entity as? DrawingTextEntity, let subEntities = text.renderSubEntities { for sticker in subEntities { if let sticker = sticker as? DrawingStickerEntity, case let .file(file, _) = sticker.content { let coder = PostboxEncoder() - coder.encodeRootObject(file) + coder.encodeRootObject(file.media) stickers.append(coder.makeData()) } } diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index a6b7dd50b1..e2c9d4e880 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -118,7 +118,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private var file: TelegramMediaFile? { if case let .file(file, _) = self.stickerEntity.content { - return file + return file.media } else { return nil } @@ -158,7 +158,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private var dimensions: CGSize { switch self.stickerEntity.content { case let .file(file, _): - return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + return file.media.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) case let .image(image, _): return image.size case let .animatedImage(_, thumbnailImage): @@ -174,7 +174,7 @@ public class DrawingStickerEntityView: DrawingEntityView { private func updateAnimationColor() { let color: UIColor? - if case let .file(file, type) = self.stickerEntity.content, file.isCustomTemplateEmoji { + if case let .file(file, type) = self.stickerEntity.content, file.media.isCustomTemplateEmoji { if case let .reaction(_, style) = type { if case .white = style { color = UIColor(rgb: 0x000000) diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 5321abea5a..c8ee6138ca 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -725,7 +725,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate let itemSize: CGFloat = floor(24.0 * fontSize * 0.78 / 17.0) let emojiTextPosition = emojiRect.center.offsetBy(dx: -textSize.width / 2.0, dy: -textSize.height / 2.0) - let entity = DrawingStickerEntity(content: .file(file, .sticker)) + let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) entity.referenceDrawingSize = CGSize(width: itemSize * 4.0, height: itemSize * 4.0) entity.scale = scale entity.position = textPosition.offsetBy( diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index 0d5d6c1957..f988ba1df5 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -625,9 +625,9 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll if let localResource = item.stickerItem.resource { self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id) } - stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) + stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) } else if let resource = item.stickerItem.resource { - stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) + stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) } } var thumbnailSticker: ImportSticker? @@ -638,7 +638,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll } let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) - thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords) + thumbnailSticker = ImportSticker(resource: .standalone(resource: resource), emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords) } let firstStickerItem = thumbnailSticker ?? stickers.first diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index e749b0f6ea..145a408982 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -102,7 +102,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { self.animated = entity.isAnimated switch entity.content { - case let .file(file, _): + case let .file(fileReference, _): + let file = fileReference.media self.file = file if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { self.source = AnimatedStickerResourceSource(postbox: postbox, resource: file.resource, isVideo: file.isVideoSticker || file.mimeType == "video/webm") diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 7644877303..983ddcda93 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -25,6 +25,8 @@ import Pasteboard import StickerPackEditTitleController import EntityKeyboard +private let maxStickersCount = 120 + private enum StickerPackPreviewGridEntry: Comparable, Identifiable { case sticker(index: Int, stableId: Int, stickerItem: StickerPackItem?, isEmpty: Bool, isPremium: Bool, isLocked: Bool, isEditing: Bool, isAdd: Bool) case add @@ -1251,7 +1253,7 @@ private final class StickerPackContainer: ASDisplayNode { completion: { file, emoji, commit in dismissImpl?() let sticker = ImportSticker( - resource: file.resource, + resource: .standalone(resource: file.resource), emojis: emoji, dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), mimeType: file.mimeType, @@ -1292,18 +1294,18 @@ private final class StickerPackContainer: ASDisplayNode { let context = self.context let controller = self.context.sharedContext.makeStickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData, completion: { file in var emoji = "🫥" - for attribute in file.attributes { - if case let .Sticker(displayText, _, _) = attribute { + for attribute in file.media.attributes { + if case let .Sticker(displayText, _, _) = attribute, !displayText.isEmpty { emoji = displayText break } } let sticker = ImportSticker( - resource: file.resource, + resource: file.resourceReference(file.media.resource), emojis: [emoji], - dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), - mimeType: file.mimeType, + dimensions: file.media.dimensions ?? PixelDimensions(width: 512, height: 512), + mimeType: file.media.mimeType, keywords: "" ) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) @@ -1313,7 +1315,7 @@ private final class StickerPackContainer: ASDisplayNode { (navigationController?.viewControllers.last as? ViewController)?.present(packController, in: .window(.root)) Queue.mainQueue().after(0.1) { - packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + packController.present(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file.media, loop: true, title: nil, text: "Sticker added to **\(info.title)** sticker set.", undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) } }) }) @@ -1342,7 +1344,7 @@ private final class StickerPackContainer: ASDisplayNode { transitionArguments: nil, completion: { file, emoji, commit in let sticker = ImportSticker( - resource: file.resource, + resource: .standalone(resource: file.resource), emojis: emoji, dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), mimeType: file.mimeType, @@ -1787,7 +1789,7 @@ private final class StickerPackContainer: ASDisplayNode { self.updateButton(count: count) } - if GlobalExperimentalSettings.enableWIPStickers && info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) { + if GlobalExperimentalSettings.enableWIPStickers && info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) && entries.count < maxStickersCount { entries.append(.add) } } @@ -1883,7 +1885,9 @@ private final class StickerPackContainer: ASDisplayNode { } } - entries.append(.add) + if entries.count < maxStickersCount { + entries.append(.add) + } self.currentEntries = entries diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index a54d8a540b..d31307d64d 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -299,6 +299,8 @@ private enum MediaReferenceRevalidationKey: Hashable { case webPage(webPage: WebpageReference) case stickerPack(stickerPack: StickerPackReference) case savedGifs + case savedStickers + case recentStickers case peer(peer: PeerReference) case wallpaper(wallpaper: WallpaperReference) case wallpapers @@ -544,6 +546,66 @@ final class MediaReferenceRevalidationContext { } } + func savedStickers(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> { + return self.genericItem(key: .savedStickers, background: background, request: { next, error in + let loadSavedStickers: Signal<[TelegramMediaFile], NoError> = postbox.transaction { transaction -> [TelegramMediaFile] in + return transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers).compactMap({ item -> TelegramMediaFile? in + if let contents = item.contents.get(RecentMediaItem.self) { + let file = contents.media + return file + } + return nil + }) + } + return (managedSavedStickers(postbox: postbox, network: network, forceFetch: true) + |> mapToSignal { _ -> Signal<[TelegramMediaFile], NoError> in + return .complete() + } + |> then(loadSavedStickers) + |> castError(RevalidateMediaReferenceError.self)).start(next: { value in + next(value) + }, error: { _ in + error(.generic) + }) + }) |> mapToSignal { next -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> in + if let next = next as? [TelegramMediaFile] { + return .single(next) + } else { + return .fail(.generic) + } + } + } + + func recentStickers(postbox: Postbox, network: Network, background: Bool) -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> { + return self.genericItem(key: .recentStickers, background: background, request: { next, error in + let loadRecentStickers: Signal<[TelegramMediaFile], NoError> = postbox.transaction { transaction -> [TelegramMediaFile] in + return transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers).compactMap({ item -> TelegramMediaFile? in + if let contents = item.contents.get(RecentMediaItem.self) { + let file = contents.media + return file + } + return nil + }) + } + return (managedRecentStickers(postbox: postbox, network: network, forceFetch: true) + |> mapToSignal { _ -> Signal<[TelegramMediaFile], NoError> in + return .complete() + } + |> then(loadRecentStickers) + |> castError(RevalidateMediaReferenceError.self)).start(next: { value in + next(value) + }, error: { _ in + error(.generic) + }) + }) |> mapToSignal { next -> Signal<[TelegramMediaFile], RevalidateMediaReferenceError> in + if let next = next as? [TelegramMediaFile] { + return .single(next) + } else { + return .fail(.generic) + } + } + } + func peer(accountPeerId: PeerId, postbox: Postbox, network: Network, background: Bool, peer: PeerReference) -> Signal { return self.genericItem(key: .peer(peer: peer), background: background, request: { next, error in return (_internal_updatedRemotePeer(accountPeerId: accountPeerId, postbox: postbox, network: network, peer: peer) @@ -811,6 +873,30 @@ func revalidateMediaResourceReference(accountPeerId: PeerId, postbox: Postbox, n } return .fail(.generic) } + case let .savedSticker(media): + return revalidationContext.savedStickers(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation) + |> mapToSignal { result -> Signal in + for file in result { + if media.id != nil && file.id == media.id { + if let updatedResource = findUpdatedMediaResource(media: file, previousMedia: media, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + } + } + return .fail(.generic) + } + case let .recentSticker(media): + return revalidationContext.recentStickers(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation) + |> mapToSignal { result -> Signal in + for file in result { + if media.id != nil && file.id == media.id { + if let updatedResource = findUpdatedMediaResource(media: file, previousMedia: media, resource: resource) { + return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)) + } + } + } + return .fail(.generic) + } case let .avatarList(peer, media): return revalidationContext.peerAvatars(accountPeerId: accountPeerId, postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) |> mapToSignal { result -> Signal in diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 4b73712944..bac8b35cff 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -42,8 +42,8 @@ private func managedRecentMedia(postbox: Postbox, network: Network, collectionId } |> switchToLatest } -func managedRecentStickers(postbox: Postbox, network: Network) -> Signal { - return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: false, fetch: { hash in +func managedRecentStickers(postbox: Postbox, network: Network, forceFetch: Bool = false) -> Signal { + return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudRecentStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: false, forceFetch: forceFetch, fetch: { hash in return network.request(Api.functions.messages.getRecentStickers(flags: 0, hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in @@ -88,8 +88,8 @@ func managedRecentGifs(postbox: Postbox, network: Network, forceFetch: Bool = fa }) } -func managedSavedStickers(postbox: Postbox, network: Network) -> Signal { - return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudSavedStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: true, forceFetch: false, fetch: { hash in +func managedSavedStickers(postbox: Postbox, network: Network, forceFetch: Bool = false) -> Signal { + return managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudSavedStickers, extractItemId: { RecentMediaItemId($0).mediaId.id }, reverseHashOrder: true, forceFetch: forceFetch, fetch: { hash in return network.request(Api.functions.messages.getFavedStickers(hash: hash)) |> retryRequest |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 57a8473844..9d6213c71c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -262,6 +262,8 @@ public enum AnyMediaReference: Equatable { case webPage(webPage: WebpageReference, media: Media) case stickerPack(stickerPack: StickerPackReference, media: Media) case savedGif(media: Media) + case savedSticker(media: Media) + case recentSticker(media: Media) case avatarList(peer: PeerReference, media: Media) case attachBot(peer: PeerReference, media: Media) case customEmoji(media: Media) @@ -299,6 +301,18 @@ public enum AnyMediaReference: Equatable { } else { return false } + case let .savedSticker(lhsMedia): + if case let .savedSticker(rhsMedia) = rhs, lhsMedia.isEqual(to: rhsMedia) { + return true + } else { + return false + } + case let .recentSticker(lhsMedia): + if case let .recentSticker(rhsMedia) = rhs, lhsMedia.isEqual(to: rhsMedia) { + return true + } else { + return false + } case let .avatarList(lhsPeer, lhsMedia): if case let .avatarList(rhsPeer, rhsMedia) = rhs, lhsPeer == rhsPeer, lhsMedia.isEqual(to: rhsMedia) { return true @@ -338,6 +352,10 @@ public enum AnyMediaReference: Equatable { return .stickerPack(stickerPack: stickerPack) case .savedGif: return .savedGif + case .savedSticker: + return .savedSticker + case .recentSticker: + return .recentSticker case .avatarList: return nil case .attachBot: @@ -371,6 +389,14 @@ public enum AnyMediaReference: Equatable { if let media = media as? T { return .savedGif(media: media) } + case let .savedSticker(media): + if let media = media as? T { + return .savedSticker(media: media) + } + case let .recentSticker(media): + if let media = media as? T { + return .recentSticker(media: media) + } case let .avatarList(peer, media): if let media = media as? T { return .avatarList(peer: peer, media: media) @@ -403,6 +429,10 @@ public enum AnyMediaReference: Equatable { return media case let .savedGif(media): return media + case let .savedSticker(media): + return media + case let .recentSticker(media): + return media case let .avatarList(_, media): return media case let .attachBot(_, media): @@ -425,12 +455,16 @@ public enum PartialMediaReference: Equatable { case webPage case stickerPack case savedGif + case savedSticker + case recentSticker } case message(message: MessageReference) case webPage(webPage: WebpageReference) case stickerPack(stickerPack: StickerPackReference) case savedGif + case savedSticker + case recentSticker public init?(decoder: PostboxDecoder) { guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else { @@ -448,6 +482,10 @@ public enum PartialMediaReference: Equatable { self = .stickerPack(stickerPack: stickerPack) case .savedGif: self = .savedGif + case .savedSticker: + self = .savedSticker + case .recentSticker: + self = .recentSticker } } @@ -464,6 +502,10 @@ public enum PartialMediaReference: Equatable { encoder.encodeObject(stickerPack, forKey: "spk") case .savedGif: encoder.encodeInt32(CodingCase.savedGif.rawValue, forKey: "_r") + case .savedSticker: + encoder.encodeInt32(CodingCase.savedSticker.rawValue, forKey: "_r") + case .recentSticker: + encoder.encodeInt32(CodingCase.recentSticker.rawValue, forKey: "_r") } } @@ -477,6 +519,10 @@ public enum PartialMediaReference: Equatable { return .stickerPack(stickerPack: stickerPack, media: media) case .savedGif: return .savedGif(media: media) + case .savedSticker: + return .savedSticker(media: media) + case .recentSticker: + return .recentSticker(media: media) } } } @@ -488,6 +534,8 @@ public enum MediaReference { case webPage case stickerPack case savedGif + case savedSticker + case recentSticker case avatarList case attachBot case customEmoji @@ -499,6 +547,8 @@ public enum MediaReference { case webPage(webPage: WebpageReference, media: T) case stickerPack(stickerPack: StickerPackReference, media: T) case savedGif(media: T) + case savedSticker(media: T) + case recentSticker(media: T) case avatarList(peer: PeerReference, media: T) case attachBot(peer: PeerReference, media: T) case customEmoji(media: T) @@ -537,6 +587,16 @@ public enum MediaReference { return nil } self = .savedGif(media: media) + case .savedSticker: + guard let media = decoder.decodeObjectForKey("m") as? T else { + return nil + } + self = .savedSticker(media: media) + case .recentSticker: + guard let media = decoder.decodeObjectForKey("m") as? T else { + return nil + } + self = .recentSticker(media: media) case .avatarList: let peer = decoder.decodeObjectForKey("pr", decoder: { PeerReference(decoder: $0) }) as! PeerReference guard let media = decoder.decodeObjectForKey("m") as? T else { @@ -584,6 +644,12 @@ public enum MediaReference { case let .savedGif(media): encoder.encodeInt32(CodingCase.savedGif.rawValue, forKey: "_r") encoder.encodeObject(media, forKey: "m") + case let .savedSticker(media): + encoder.encodeInt32(CodingCase.savedSticker.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") + case let .recentSticker(media): + encoder.encodeInt32(CodingCase.recentSticker.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") case let .avatarList(peer, media): encoder.encodeInt32(CodingCase.avatarList.rawValue, forKey: "_r") encoder.encodeObject(peer, forKey: "pr") @@ -615,6 +681,10 @@ public enum MediaReference { return .stickerPack(stickerPack: stickerPack, media: media) case let .savedGif(media): return .savedGif(media: media) + case let .savedSticker(media): + return .savedSticker(media: media) + case let .recentSticker(media): + return .recentSticker(media: media) case let .avatarList(peer, media): return .avatarList(peer: peer, media: media) case let .attachBot(peer, media): @@ -642,6 +712,10 @@ public enum MediaReference { return media case let .savedGif(media): return media + case let .savedSticker(media): + return media + case let .recentSticker(media): + return media case let .avatarList(_, media): return media case let .attachBot(_, media): diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 183e1be0f9..bd0e213582 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -2,7 +2,7 @@ import Foundation import Postbox import SwiftSignalKit import TelegramApi - +import MtProtoKit public enum UploadStickerStatus { case progress(Float) @@ -77,13 +77,13 @@ public enum CreateStickerSetError { } public struct ImportSticker { - public let resource: MediaResource + public let resource: MediaResourceReference let emojis: [String] public let dimensions: PixelDimensions public let mimeType: String public let keywords: String - public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions, mimeType: String, keywords: String) { + public init(resource: MediaResourceReference, emojis: [String], dimensions: PixelDimensions, mimeType: String, keywords: String) { self.resource = resource self.emojis = emojis self.dimensions = dimensions @@ -94,7 +94,7 @@ public struct ImportSticker { public extension ImportSticker { var stickerPackItem: StickerPackItem? { - guard let resource = self.resource as? TelegramMediaResource else { + guard let resource = self.resource.resource as? TelegramMediaResource else { return nil } var fileAttributes: [TelegramMediaFileAttribute] = [] @@ -150,10 +150,10 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri stickers.append(thumbnail) } for sticker in stickers { - if let resource = sticker.resource as? CloudDocumentMediaResource { + if let resource = sticker.resource.resource as? CloudDocumentMediaResource { uploadStickers.append(.single(.complete(resource, sticker.mimeType))) } else { - uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) |> mapError { _ -> CreateStickerSetError in return .generic }) @@ -294,13 +294,13 @@ public enum AddStickerToSetError { func _internal_addStickerToStickerSet(account: Account, packReference: StickerPackReference, sticker: ImportSticker) -> Signal { let uploadSticker: Signal - if let resource = sticker.resource as? CloudDocumentMediaResource { + if let resource = sticker.resource.resource as? CloudDocumentMediaResource { uploadSticker = .single(.complete(resource, sticker.mimeType)) } else { uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(AddStickerToSetError.self) |> mapToSignal { peer in - return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) |> mapError { _ -> AddStickerToSetError in return .generic } @@ -317,8 +317,27 @@ func _internal_addStickerToStickerSet(account: Account, packReference: StickerPa flags |= (1 << 1) } let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords) - + return account.network.request(Api.functions.stickers.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker)) + |> `catch` { error -> Signal in + if error.errorDescription == "FILE_REFERENCE_EXPIRED" { + return revalidateMediaResourceReference(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, revalidationContext: account.mediaReferenceRevalidationContext, info: TelegramCloudMediaResourceFetchInfo(reference: sticker.resource, preferBackgroundReferenceRevalidation: false, continueInBackground: false), resource: sticker.resource.resource) + |> mapError { _ -> MTRpcError in + return MTRpcError(errorCode: 500, errorDescription: "Internal") + } + |> mapToSignal { result -> Signal in + guard let resource = result.updatedResource as? CloudDocumentMediaResource else { + return .fail(MTRpcError(errorCode: 500, errorDescription: "Internal")) + } + + let inputSticker: Api.InputStickerSetItem = .inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), emoji: sticker.emojis.joined(), maskCoords: nil, keywords: sticker.keywords) + + return account.network.request(Api.functions.stickers.addStickerToSet(stickerset: packReference.apiInputStickerSet, sticker: inputSticker)) + } + } else { + return .fail(error) + } + } |> mapError { error -> AddStickerToSetError in return .generic } @@ -403,13 +422,13 @@ func _internal_replaceSticker(account: Account, previousSticker: FileMediaRefere } let uploadSticker: Signal - if let resource = sticker.resource as? CloudDocumentMediaResource { + if let resource = sticker.resource.resource as? CloudDocumentMediaResource { uploadSticker = .single(.complete(resource, sticker.mimeType)) } else { uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(ReplaceStickerError.self) |> mapToSignal { peer in - return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) |> mapError { _ -> ReplaceStickerError in return .generic } diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index cfab79f187..4b545754f9 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -1401,7 +1401,7 @@ final class AvatarEditorScreenComponent: Component { try? backgroundImage.jpegData(compressionQuality: 0.8)?.write(to: tempUrl) let drawingSize = CGSize(width: 1920.0, height: 1920.0) - let entity = DrawingStickerEntity(content: .file(file, .sticker)) + let entity = DrawingStickerEntity(content: .file(.standalone(media: file), .sticker)) entity.referenceDrawingSize = drawingSize entity.position = CGPoint(x: drawingSize.width / 2.0, y: drawingSize.height / 2.0) entity.scale = 3.3 @@ -1409,7 +1409,8 @@ final class AvatarEditorScreenComponent: Component { var fileId: Int64 = 0 var stickerPackId: Int64 = 0 var stickerPackAccessHash: Int64 = 0 - if case let .file(file, _) = entity.content { + if case let .file(fileReference, _) = entity.content { + let file = fileReference.media if file.isCustomEmoji { fileId = file.fileId.id } else if file.isAnimatedSticker { diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index d0e9c13b51..5f187d2a3f 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1195,7 +1195,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { if let id = groupId.base as? ItemCollectionId, context.sharedContext.currentStickerSettings.with({ $0 }).dynamicPackOrder { bubbleUpEmojiOrStickersets.append(id) } - let _ = interaction.sendSticker(.standalone(media: file), false, false, nil, false, view, rect, layer, bubbleUpEmojiOrStickersets) + + let reference: FileMediaReference + if groupId == AnyHashable("saved") { + reference = .savedSticker(media: file) + } else if groupId == AnyHashable("recent") { + reference = .recentSticker(media: file) + } else { + reference = .standalone(media: file) + } + let _ = interaction.sendSticker(reference, false, false, nil, false, view, rect, layer, bubbleUpEmojiOrStickersets) } }) }, diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 92015ddc47..2779b41157 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -32,7 +32,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case sticker case reaction(MessageReaction.Reaction, ReactionStyle) } - case file(TelegramMediaFile, FileType) + case file(FileMediaReference, FileType) case image(UIImage, ImageType) case animatedImage(Data, UIImage) case video(TelegramMediaFile) @@ -43,7 +43,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { switch lhs { case let .file(lhsFile, lhsFileType): if case let .file(rhsFile, rhsFileType) = rhs { - return lhsFile.fileId == rhsFile.fileId && lhsFileType == rhsFileType + return lhsFile.media.fileId == rhsFile.media.fileId && lhsFileType == rhsFileType } else { return false } @@ -152,7 +152,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { if case .reaction = type { dimensions = CGSize(width: 512.0, height: 512.0) } else { - dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + dimensions = file.media.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) } case let .video(file): dimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) @@ -176,7 +176,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case .reaction: return false default: - return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" + return file.media.isAnimatedSticker || file.media.isVideoSticker || file.media.mimeType == "video/webm" } } case .image: @@ -248,7 +248,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { fileType = .sticker } - self.content = .file(file, fileType) + self.content = .file(.standalone(media: file), fileType) } else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { let isRectangle = try container.decodeIfPresent(Bool.self, forKey: .isRectangle) ?? false let isDualPhoto = try container.decodeIfPresent(Bool.self, forKey: .isDualPhoto) ?? false @@ -290,7 +290,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encode(self.uuid, forKey: .uuid) switch self.content { case let .file(file, fileType): - try container.encode(file, forKey: .file) + try container.encode(file.media, forKey: .file) switch fileType { case let .reaction(reaction, reactionStyle): try container.encode(reaction, forKey: .reaction) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index ab1c4569b5..1fc6ac6656 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -69,7 +69,7 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti let content: MediaEditorComposerStickerEntity.Content switch entity.content { case let .file(file, _): - content = .file(file) + content = .file(file.media) case let .image(image, _): content = .image(image) case let .animatedImage(data, _): diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4fc58e4315..494f8c2c4c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3051,7 +3051,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) } else if case let .sticker(sticker, emoji) = effectiveSubject { controller.stickerSelectedEmoji = emoji - let stickerEntity = DrawingStickerEntity(content: .file(sticker, .sticker)) + let stickerEntity = DrawingStickerEntity(content: .file(.standalone(media: sticker), .sticker)) stickerEntity.referenceDrawingSize = storyDimensions stickerEntity.scale = 4.0 stickerEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) @@ -4382,7 +4382,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let reaction = self.availableReactions.first(where: { reaction in return reaction.reaction.rawValue == .builtin(heart) }) { - let stickerEntity = DrawingStickerEntity(content: .file(reaction.stillAnimation, .reaction(.builtin(heart), .white))) + let stickerEntity = DrawingStickerEntity(content: .file(.standalone(media: reaction.stillAnimation), .reaction(.builtin(heart), .white))) self.interaction?.insertEntity(stickerEntity, scale: 1.175) } @@ -4530,11 +4530,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let content { if case let .file(file, _) = content { - if file.isCustomEmoji { - self.defaultToEmoji = true - } else { - self.defaultToEmoji = false - } + self.defaultToEmoji = file.media.isCustomEmoji } let stickerEntity = DrawingStickerEntity(content: content) @@ -5823,7 +5819,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } for entity in entities { if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, type) = stickerEntity.content, case let .reaction(reaction, _) = type, case .custom = reaction { - self.presentUnavailableReactionPremiumSuggestion(file: file) + self.presentUnavailableReactionPremiumSuggestion(file: file.media) return false } } @@ -5879,13 +5875,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate switch entity { case let .sticker(stickerEntity): if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType { - stickers.append(file) + stickers.append(file.media) } case let .text(textEntity): if let subEntities = textEntity.renderSubEntities { for entity in subEntities { if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType { - stickers.append(file) + stickers.append(file.media) } } } @@ -6467,7 +6463,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate shortName: "", stickers: [ ImportSticker( - resource: file.resource, + resource: .standalone(resource: file.resource), emojis: self.effectiveStickerEmoji(), dimensions: PixelDimensions(width: 512, height: 512), mimeType: "image/webp", @@ -6602,7 +6598,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case let .createStickerPack(title): let sticker = ImportSticker( - resource: resource, + resource: .standalone(resource: resource), emojis: self.effectiveStickerEmoji(), dimensions: dimensions, mimeType: mimeType, @@ -6621,7 +6617,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case let .addToStickerPack(pack, _): let sticker = ImportSticker( - resource: resource, + resource: .standalone(resource: resource), emojis: self.effectiveStickerEmoji(), dimensions: dimensions, mimeType: mimeType, diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index c5828da772..e99ea5ec77 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -147,7 +147,7 @@ private final class StickerSelectionComponent: Component { c.dismiss(animated: true) } }) - if controller.completion(.file(file.media, .sticker)) { + if controller.completion(.file(file, .sticker)) { controller.dismiss(animated: true) } } @@ -884,7 +884,7 @@ public class StickerPickerScreen: ViewController { }) }) } else if let file = item.itemFile { - if controller.completion(.file(file, .sticker)) { + if controller.completion(.file(.standalone(media: file), .sticker)) { controller.dismiss(animated: true) } } else if case let .staticEmoji(emoji) = item.content { @@ -1299,7 +1299,7 @@ public class StickerPickerScreen: ViewController { guard let self, let controller = self.controller else { return false } - if controller.completion(.file(fileReference.media, .sticker)) { + if controller.completion(.file(fileReference, .sticker)) { controller.dismiss(animated: true) } return true @@ -1311,7 +1311,15 @@ public class StickerPickerScreen: ViewController { } }) } else { - let _ = controller.completion(.file(file, .sticker)) + let reference: FileMediaReference + if groupId == AnyHashable("saved") { + reference = .savedSticker(media: file) + } else if groupId == AnyHashable("recent") { + reference = .recentSticker(media: file) + } else { + reference = .standalone(media: file) + } + let _ = controller.completion(.file(reference, .sticker)) controller.dismiss(animated: true) } }, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b828eba274..dc2d9621cd 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2421,7 +2421,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return stickerMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) } - public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (TelegramMediaFile) -> Void) -> ViewController { + public func makeStickerPickerScreen(context: AccountContext, inputData: Promise, completion: @escaping (FileMediaReference) -> Void) -> ViewController { let controller = StickerPickerScreen(context: context, inputData: inputData.get(), expanded: true, hasGifs: false, hasInteractiveStickers: false) controller.completion = { content in if let content, case let .file(file, _) = content { From a6b5f0f96eb8b194b5068dc6d447ca1d4d357f5b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 6 Apr 2024 23:08:44 +0400 Subject: [PATCH 05/14] [WIP] Stickers editor --- .../Sources/ImportStickerPackController.swift | 2 +- .../ImportStickerPackControllerNode.swift | 6 +- .../Sources/StickerPackScreen.swift | 22 +++++- .../Stickers/ImportStickers.swift | 16 +++-- .../Stickers/TelegramEngineStickers.swift | 4 +- .../Sources/MediaEditorScreen.swift | 69 +++++++++++-------- .../Sources/UndoOverlayControllerNode.swift | 2 +- 7 files changed, 80 insertions(+), 41 deletions(-) diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift index 01c79d7df3..dff3fadfcc 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackController.swift @@ -89,7 +89,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = [] for sticker in strongSelf.stickerPack.stickers { if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] { - signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType) + signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), duration: nil, mimeType: sticker.mimeType) |> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in switch result { case .progress: diff --git a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift index f988ba1df5..84c0bff8bf 100644 --- a/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift +++ b/submodules/ImportStickerPackUI/Sources/ImportStickerPackControllerNode.swift @@ -625,9 +625,9 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll if let localResource = item.stickerItem.resource { self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id) } - stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) + stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, duration: nil, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) } else if let resource = item.stickerItem.resource { - stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) + stickers.append(ImportSticker(resource: .standalone(resource: resource._asResource()), emojis: item.stickerItem.emojis, dimensions: dimensions, duration: nil, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords)) } } var thumbnailSticker: ImportSticker? @@ -638,7 +638,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll } let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) - thumbnailSticker = ImportSticker(resource: .standalone(resource: resource), emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords) + thumbnailSticker = ImportSticker(resource: .standalone(resource: resource), emojis: [], dimensions: dimensions, duration: nil, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords) } let firstStickerItem = thumbnailSticker ?? stickers.first diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 983ddcda93..da8c3454b4 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -717,6 +717,16 @@ private final class StickerPackContainer: ASDisplayNode { if let reorderPosition = self.reorderPosition, let file = itemNode.stickerPackItem?.file { let _ = self.context.engine.stickers.reorderSticker(sticker: .standalone(media: file), position: reorderPosition).startStandalone() + + if let (info, items, isInstalled) = self.currentStickerPack { + var updatedItems = items + if let index = items.firstIndex(where: { $0.file.fileId == file.fileId }) { + let item = items[index] + updatedItems.remove(at: index) + updatedItems.insert(item, at: reorderPosition) + } + self.currentStickerPack = (info, updatedItems, isInstalled) + } } } else { reorderNode.removeFromSupernode() @@ -1256,6 +1266,7 @@ private final class StickerPackContainer: ASDisplayNode { resource: .standalone(resource: file.resource), emojis: emoji, dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + duration: file.duration, mimeType: file.mimeType, keywords: "" ) @@ -1305,6 +1316,7 @@ private final class StickerPackContainer: ASDisplayNode { resource: file.resourceReference(file.media.resource), emojis: [emoji], dimensions: file.media.dimensions ?? PixelDimensions(width: 512, height: 512), + duration: file.media.duration, mimeType: file.media.mimeType, keywords: "" ) @@ -1347,6 +1359,7 @@ private final class StickerPackContainer: ASDisplayNode { resource: .standalone(resource: file.resource), emojis: emoji, dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + duration: file.duration, mimeType: file.mimeType, keywords: "" ) @@ -1856,12 +1869,14 @@ private final class StickerPackContainer: ASDisplayNode { entries.append(.sticker(index: entries.count, stableId: resolvedStableId, stickerItem: item, isEmpty: false, isPremium: isPremium, isLocked: isLocked, isEditing: false, isAdd: false)) } + var addedReorderItem = false var currentIndex: Int = 0 for item in generalItems { - if self.isReordering, let reorderNode = self.reorderNode, let reorderItem = reorderNode.itemNode?.stickerPackItem, let reorderPosition = self.reorderPosition { + if self.isReordering, let reorderItem = self.reorderNode?.itemNode?.stickerPackItem, let reorderPosition = self.reorderPosition { if currentIndex == reorderPosition { addItem(reorderItem, false, false) currentIndex += 1 + addedReorderItem = true } if item.file.fileId == reorderItem.file.fileId { @@ -1875,6 +1890,11 @@ private final class StickerPackContainer: ASDisplayNode { currentIndex += 1 } } + if !addedReorderItem, let reorderItem = self.reorderNode?.itemNode?.stickerPackItem, let reorderPosition = self.reorderPosition, currentIndex == reorderPosition { + addItem(reorderItem, false, false) + currentIndex += 1 + addedReorderItem = true + } if !premiumConfiguration.isPremiumDisabled { if !premiumItems.isEmpty { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index bd0e213582..c107e8635e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -33,7 +33,7 @@ private func uploadedSticker(postbox: Postbox, network: Network, resource: Media } } -func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, mimeType: String) -> Signal { +func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { guard let inputPeer = apiInputPeer(peer) else { return .fail(.generic) } @@ -51,6 +51,9 @@ func _internal_uploadSticker(account: Account, peer: Peer, resource: MediaResour let flags: Int32 = 0 var attributes: [Api.DocumentAttribute] = [] attributes.append(.documentAttributeSticker(flags: 0, alt: alt, stickerset: .inputStickerSetEmpty, maskCoords: nil)) + if let duration { + attributes.append(.documentAttributeVideo(flags: 0, duration: duration, w: dimensions.width, h: dimensions.height, preloadPrefixSize: nil)) + } attributes.append(.documentAttributeImageSize(w: dimensions.width, h: dimensions.height)) return account.network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(flags: flags, file: file, thumb: nil, mimeType: mimeType, attributes: attributes, stickers: nil, ttlSeconds: nil))) |> mapError { _ -> UploadStickerError in return .generic } @@ -80,13 +83,15 @@ public struct ImportSticker { public let resource: MediaResourceReference let emojis: [String] public let dimensions: PixelDimensions + public let duration: Double? public let mimeType: String public let keywords: String - public init(resource: MediaResourceReference, emojis: [String], dimensions: PixelDimensions, mimeType: String, keywords: String) { + public init(resource: MediaResourceReference, emojis: [String], dimensions: PixelDimensions, duration: Double?, mimeType: String, keywords: String) { self.resource = resource self.emojis = emojis self.dimensions = dimensions + self.duration = duration self.mimeType = mimeType self.keywords = keywords } @@ -102,6 +107,7 @@ public extension ImportSticker { fileAttributes.append(.FileName(fileName: "sticker.webm")) fileAttributes.append(.Animated) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) + fileAttributes.append(.Video(duration: self.duration ?? 3.0, size: self.dimensions, flags: [], preloadSize: nil)) } else if self.mimeType == "application/x-tgsticker" { fileAttributes.append(.FileName(fileName: "sticker.tgs")) fileAttributes.append(.Animated) @@ -153,7 +159,7 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri if let resource = sticker.resource.resource as? CloudDocumentMediaResource { uploadStickers.append(.single(.complete(resource, sticker.mimeType))) } else { - uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + uploadStickers.append(_internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType) |> mapError { _ -> CreateStickerSetError in return .generic }) @@ -300,7 +306,7 @@ func _internal_addStickerToStickerSet(account: Account, packReference: StickerPa uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(AddStickerToSetError.self) |> mapToSignal { peer in - return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType) |> mapError { _ -> AddStickerToSetError in return .generic } @@ -428,7 +434,7 @@ func _internal_replaceSticker(account: Account, previousSticker: FileMediaRefere uploadSticker = account.postbox.loadedPeerWithId(account.peerId) |> castError(ReplaceStickerError.self) |> mapToSignal { peer in - return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, mimeType: sticker.mimeType) + return _internal_uploadSticker(account: account, peer: peer, resource: sticker.resource.resource, alt: sticker.emojis.first ?? "", dimensions: sticker.dimensions, duration: sticker.duration, mimeType: sticker.mimeType) |> mapError { _ -> ReplaceStickerError in return .generic } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 01db7419d8..4300d76c83 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -78,8 +78,8 @@ public extension TelegramEngine { return _internal_stickerPacksAttachedToMedia(account: self.account, media: media) } - public func uploadSticker(peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, mimeType: String) -> Signal { - return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, alt: alt, dimensions: dimensions, mimeType: mimeType) + public func uploadSticker(peer: Peer, resource: MediaResource, alt: String, dimensions: PixelDimensions, duration: Double?, mimeType: String) -> Signal { + return _internal_uploadSticker(account: self.account, peer: peer, resource: resource, alt: alt, dimensions: dimensions, duration: duration, mimeType: mimeType) } public func createStickerSet(title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 494f8c2c4c..15f25e9f43 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6231,6 +6231,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var stickerRecommendedEmoji: [String] = [] private var stickerSelectedEmoji: [String] = [] + private func effectiveStickerEmoji() -> [String] { let filtered = self.stickerSelectedEmoji.filter { !$0.isEmpty } guard !filtered.isEmpty else { @@ -6238,6 +6239,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } return filtered } + + private func preferredStickerDuration() -> Double { + var duration: Double = 3.0 + var stickerDurations: [Double] = [] + self.node.entitiesView.eachView { entityView in + if let stickerEntityView = entityView as? DrawingStickerEntityView { + if let duration = stickerEntityView.duration, duration > 0.0 { + stickerDurations.append(duration) + } + } + } + if !stickerDurations.isEmpty { + duration = stickerDurations.max() ?? 3.0 + } + return duration + } + private weak var stickerResultController: PeekController? func presentStickerPreview(image: UIImage) { guard let mediaEditor = self.node.mediaEditor else { @@ -6257,8 +6275,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.context.account.postbox.mediaBox.storeResourceData(isVideo ? thumbnailResource.id : resource.id, data: data) } } - var file = stickerFile(resource: resource, thumbnailResource: thumbnailResource, size: Int64(0), dimensions: PixelDimensions(image.size), isVideo: isVideo) - let emoji = self.stickerSelectedEmoji + var file = stickerFile(resource: resource, thumbnailResource: thumbnailResource, size: Int64(0), dimensions: PixelDimensions(image.size), duration: self.preferredStickerDuration(), isVideo: isVideo) var menuItems: [ContextMenuItem] = [] if case let .stickerEditor(mode) = self.mode { @@ -6274,7 +6291,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { self.stickerResultController?.disappeared = nil self.completion(MediaEditorScreen.Result( - media: .sticker(file: file, emoji: emoji), + media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), mediaAreas: [], caption: NSAttributedString(), options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false), @@ -6465,8 +6482,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate ImportSticker( resource: .standalone(resource: file.resource), emojis: self.effectiveStickerEmoji(), - dimensions: PixelDimensions(width: 512, height: 512), - mimeType: "image/webp", + dimensions: file.dimensions ?? PixelDimensions(width: 512, height: 512), + duration: file.duration, + mimeType: file.mimeType, keywords: "" ) ], @@ -6512,9 +6530,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private func uploadSticker(_ file: TelegramMediaFile, action: StickerAction) { let context = self.context let dimensions = PixelDimensions(width: 512, height: 512) + let duration = file.duration let mimeType = file.mimeType let isVideo = file.mimeType == "video/webm" - let emoji = self.stickerSelectedEmoji + let emojis = self.effectiveStickerEmoji() var isUpdate = false if case .update = action { @@ -6578,13 +6597,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let resource = resource as? CloudDocumentMediaResource { return .single(.progress(1.0)) |> then(.single(.complete(resource, mimeType))) } else { - return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, alt: "", dimensions: dimensions, mimeType: mimeType) + return context.engine.stickers.uploadSticker(peer: peer._asPeer(), resource: resource, alt: "", dimensions: dimensions, duration: duration, mimeType: mimeType) |> mapToSignal { status -> Signal in switch status { case let .progress(progress): return .single(.progress(isVideo ? 0.5 + progress * 0.5 : progress)) case let .complete(resource, _): - let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, isVideo: isVideo) + let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: file.size ?? 0, dimensions: dimensions, duration: file.duration, isVideo: isVideo) switch action { case .send: return .single(status) @@ -6599,8 +6618,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case let .createStickerPack(title): let sticker = ImportSticker( resource: .standalone(resource: resource), - emojis: self.effectiveStickerEmoji(), + emojis: emojis, dimensions: dimensions, + duration: duration, mimeType: mimeType, keywords: "" ) @@ -6618,8 +6638,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate case let .addToStickerPack(pack, _): let sticker = ImportSticker( resource: .standalone(resource: resource), - emojis: self.effectiveStickerEmoji(), + emojis: emojis, dimensions: dimensions, + duration: duration, mimeType: mimeType, keywords: "" ) @@ -6659,10 +6680,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let result: MediaEditorScreen.Result switch action { case .update: - result = MediaEditorScreen.Result(media: .sticker(file: file, emoji: emoji)) + result = MediaEditorScreen.Result(media: .sticker(file: file, emoji: emojis)) case .upload, .send: - let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: resource.size ?? 0, dimensions: dimensions, isVideo: isVideo) - result = MediaEditorScreen.Result(media: .sticker(file: file, emoji: emoji)) + let file = stickerFile(resource: resource, thumbnailResource: file.previewRepresentations.first?.resource, size: resource.size ?? 0, dimensions: dimensions, duration: self.preferredStickerDuration(), isVideo: isVideo) + result = MediaEditorScreen.Result(media: .sticker(file: file, emoji: emojis)) default: result = MediaEditorScreen.Result() } @@ -6844,18 +6865,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate duration = video.duration.seconds } if isSticker { - duration = 3.0 - var stickerDurations: [Double] = [] - self.node.entitiesView.eachView { entityView in - if let stickerEntityView = entityView as? DrawingStickerEntityView { - if let duration = stickerEntityView.duration, duration > 0.0 { - stickerDurations.append(duration) - } - } - } - if !stickerDurations.isEmpty { - duration = stickerDurations.max() ?? 3.0 - } + duration = self.preferredStickerDuration() } let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0, isSticker: isSticker) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).\(fileExtension)" @@ -7610,12 +7620,15 @@ extension MediaScrubberComponent.Track { } } -private func stickerFile(resource: TelegramMediaResource, thumbnailResource: TelegramMediaResource?, size: Int64, dimensions: PixelDimensions, isVideo: Bool) -> TelegramMediaFile { +private func stickerFile(resource: TelegramMediaResource, thumbnailResource: TelegramMediaResource?, size: Int64, dimensions: PixelDimensions, duration: Double?, isVideo: Bool) -> TelegramMediaFile { var fileAttributes: [TelegramMediaFileAttribute] = [] fileAttributes.append(.FileName(fileName: isVideo ? "sticker.webm" : "sticker.webp")) fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) - fileAttributes.append(.ImageSize(size: dimensions)) - + if isVideo { + fileAttributes.append(.Video(duration: duration ?? 3.0, size: dimensions, flags: [], preloadSize: nil)) + } else { + fileAttributes.append(.ImageSize(size: dimensions)) + } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if let thumbnailResource { previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index 78386b6264..80944b6120 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -761,7 +761,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { if file.isAnimatedSticker { thumbnailItem = .animated(EngineMediaResource(file.resource)) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: file.resource) - } else if let dimensions = file.dimensions, let resource = chatMessageStickerResource(file: file, small: true) as? TelegramMediaResource { + } else if let dimensions = file.dimensions, let resource = chatMessageStickerResource(file: file, small: false) as? TelegramMediaResource { thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resourceReference = MediaResourceReference.media(media: .standalone(media: file), resource: resource) } From 64c7c6bb40863af7f0b75204e5ad4a686332d850 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 7 Apr 2024 14:26:11 +0400 Subject: [PATCH 06/14] Fix today birthdays dismissal --- submodules/TelegramCore/Sources/Suggestions.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramCore/Sources/Suggestions.swift b/submodules/TelegramCore/Sources/Suggestions.swift index 5823c214b9..7c19315255 100644 --- a/submodules/TelegramCore/Sources/Suggestions.swift +++ b/submodules/TelegramCore/Sources/Suggestions.swift @@ -58,10 +58,10 @@ func _internal_getServerDismissedSuggestions(account: Account) -> Signal<[Server guard let appConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) else { return [] } - guard let data = appConfiguration.data, let listItems = data["hidden_suggestions"] as? [String] else { - return [] + var listItems: [String] = [] + if let data = appConfiguration.data, let listItemsValues = data["hidden_suggestions"] as? [String] { + listItems.append(contentsOf: listItemsValues) } - var items = listItems.compactMap { item -> ServerProvidedSuggestion? in return ServerProvidedSuggestion(rawValue: item) } From cf9699a1ba40573e3924b63e00e85b1e10964770 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 7 Apr 2024 16:41:41 +0400 Subject: [PATCH 07/14] [WIP] Stickers editor --- .../Sources/StickerOutlineRenderPass.swift | 116 ++++++++++++++---- .../Sources/MediaEditorScreen.swift | 18 +++ 2 files changed, 113 insertions(+), 21 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift index 90b2319b04..68dbff32ac 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift @@ -1,13 +1,25 @@ import Foundation +import UIKit +import Display import Metal import simd import CoreImage +private let maxBorderWidth: Float = 40.0 + final class StickerOutlineRenderPass: RenderPass { var value: simd_float1 = 0.0 - var context: CIContext? - var maskFilter: CIFilter? + private var context: CIContext? + + private var edgeMaskFilter: CIFilter? + private var edgeMaskImage: (CIImage, simd_float1)? + + private var maskFilter: CIFilter? + private var alphaFilter: CIFilter? + private var blendFilter: CIFilter? + private var sourceAtopFilter: CIFilter? + private var whiteImage: CIImage? private var outputTexture: MTLTexture? @@ -19,10 +31,72 @@ final class StickerOutlineRenderPass: RenderPass { guard self.value > 0.005, let context = self.context else { return input } - + + let width = self.value * maxBorderWidth + if self.maskFilter == nil { + self.edgeMaskFilter = CIFilter(name: "CIBlendWithMask") + self.edgeMaskFilter?.setValue(CIImage(color: .clear), forKey: kCIInputBackgroundImageKey) + self.maskFilter = CIFilter(name: "CIMorphologyMaximum") + + self.alphaFilter = CIFilter(name: "CIColorMatrix") + self.alphaFilter?.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputRVector") + self.alphaFilter?.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector") + self.alphaFilter?.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector") + self.alphaFilter?.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector") + self.blendFilter = CIFilter(name: "CIBlendWithMask") + self.sourceAtopFilter = CIFilter(name: "CISourceAtopCompositing") + self.whiteImage = CIImage(color: .white) } + + if self.edgeMaskImage == nil || self.edgeMaskImage?.1 != width { + self.edgeMaskImage = (roundedCornersMaskImage(outlineWidth: CGFloat(width)), width) + } + + self.edgeMaskFilter?.setValue(CIImage(mtlTexture: input), forKey: kCIInputImageKey) + self.edgeMaskFilter?.setValue(self.edgeMaskImage?.0, forKey: kCIInputMaskImageKey) + + guard let image = self.edgeMaskFilter?.outputImage else { + return input + } + + guard let maskFilter = self.maskFilter else { + return input + } + + maskFilter.setValue(width, forKey: kCIInputRadiusKey) + maskFilter.setValue(image, forKey: kCIInputImageKey) + + guard let eroded = maskFilter.outputImage else { + return input + } + + self.sourceAtopFilter?.setValue(self.whiteImage, forKey: kCIInputImageKey) + self.sourceAtopFilter?.setValue(eroded, forKey: kCIInputBackgroundImageKey) + + guard let colorizedImage = self.sourceAtopFilter?.outputImage?.cropped(to: eroded.extent) else { + return input + } + + self.alphaFilter?.setValue(image, forKey: kCIInputImageKey) + + guard let alphaOnlyImage = self.alphaFilter?.outputImage, let whiteImage = self.whiteImage else { + return input + } + + let blendMask = alphaOnlyImage.composited(over: whiteImage).cropped(to: alphaOnlyImage.extent) + + self.blendFilter?.setValue(colorizedImage, forKey: kCIInputImageKey) + self.blendFilter?.setValue(blendMask, forKey: kCIInputMaskImageKey) + self.blendFilter?.setValue(CIImage(color: .clear), forKey: kCIInputBackgroundImageKey) + + guard let outline = self.blendFilter?.outputImage else { + return input + } + + let resultImage = outline.composited(over: image) + if self.outputTexture == nil { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.textureType = .type2D @@ -37,26 +111,26 @@ final class StickerOutlineRenderPass: RenderPass { self.outputTexture = texture texture.label = "outlineOutputTexture" } - guard let maskFilter = self.maskFilter, let image = CIImage(mtlTexture: input) else { + + guard let outputTexture = self.outputTexture 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 - } + let renderDestination = CIRenderDestination(mtlTexture: outputTexture, commandBuffer: commandBuffer) + _ = try? context.startTask(toRender: resultImage, to: renderDestination) + return outputTexture } } + +private func roundedCornersMaskImage(outlineWidth: CGFloat) -> CIImage { + let rectSize = CGSize(width: floor(1080.0 * 0.97) - outlineWidth * 2.0, height: floor(1080.0 * 0.97) - outlineWidth * 2.0) + let cornerRadius = floor(1080.0 * 0.97) / 8.0 - outlineWidth + let image = generateImage(CGSize(width: 1080.0, height: 1920.0), opaque: true, scale: 1.0) { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: floor((1080.0 - rectSize.width) / 2.0), y: floor((1920.0 - rectSize.width) / 2.0)), size: rectSize), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)) + context.setFillColor(UIColor.white.cgColor) + context.fillPath() + }?.cgImage + return CIImage(cgImage: image!) +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 15f25e9f43..8de0b1a540 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3428,10 +3428,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } + private var previousPanTimestamp: Double? + private var previousPinchTimestamp: Double? + private var previousRotateTimestamp: Double? + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { return } + let currentTimestamp = CACurrentMediaTime() + if let previousPanTimestamp = self.previousPanTimestamp, currentTimestamp - previousPanTimestamp < 0.016 { + return + } + self.previousPanTimestamp = currentTimestamp self.entitiesView.handlePan(gestureRecognizer) } @@ -3439,6 +3448,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { return } + let currentTimestamp = CACurrentMediaTime() + if let previousPinchTimestamp = self.previousPinchTimestamp, currentTimestamp - previousPinchTimestamp < 0.016 { + return + } + self.previousPinchTimestamp = currentTimestamp self.entitiesView.handlePinch(gestureRecognizer) } @@ -3446,6 +3460,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, case .message = subject, !self.entitiesView.hasSelection { return } + let currentTimestamp = CACurrentMediaTime() + if let previousRotateTimestamp = self.previousRotateTimestamp, currentTimestamp - previousRotateTimestamp < 0.016 { + return + } self.entitiesView.handleRotate(gestureRecognizer) } From a51c65e26877f525b7a44a6cbd79cea75cde3ee0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 7 Apr 2024 18:39:05 +0400 Subject: [PATCH 08/14] [WIP] Stickers editor --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + ...nchronizeRecentlyUsedMediaOperations.swift | 7 ++ .../Stickers/TelegramEngineStickers.swift | 10 +- .../Sources/ChatEntityKeyboardInputNode.swift | 21 +++- .../Sources/StickerOutlineRenderPass.swift | 3 +- .../Sources/MediaCutoutScreen.swift | 6 +- .../Sources/MediaEditorScreen.swift | 4 +- .../Sources/StickerCutoutOutlineView.swift | 116 ++++++++++++++++-- .../TelegramUI/Sources/ChatController.swift | 2 +- 9 files changed, 151 insertions(+), 21 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d649e756bf..d7f77c8498 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11898,3 +11898,6 @@ Sorry for the inconvenience."; "ChatList.PremiumGraceTitle" = "⚠️ Don't lose access to Telegram Premium!"; "ChatList.PremiumGraceText" = "Your exclusive benefits are about to expire."; + +"Stickers.RemoveFromRecent" = "Remove from Recents"; +"Conversation.StickerRemovedFromRecent" = "Sticker was removed from Recents."; diff --git a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift index fd0ef9b6aa..fe1b5734c2 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift @@ -46,6 +46,13 @@ func addRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaRe } } +func _internal_removeRecentlyUsedSticker(transaction: Transaction, fileReference: FileMediaReference) { + if let resource = fileReference.media.resource as? CloudDocumentMediaResource { + transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, itemId: RecentMediaItemId(fileReference.media.fileId).rawValue) + addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .remove(id: resource.fileId, accessHash: resource.accessHash)) + } +} + func _internal_clearRecentlyUsedStickers(transaction: Transaction) { transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudRecentStickers, items: []) addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .clear) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 4300d76c83..2f2005e228 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -286,9 +286,15 @@ public extension TelegramEngine { } } - public func addRecentlyUsedSticker(file: TelegramMediaFile) { + public func addRecentlyUsedSticker(fileReference: FileMediaReference) { let _ = self.account.postbox.transaction({ transaction -> Void in - TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) + }).start() + } + + public func removeRecentlyUsedSticker(fileReference: FileMediaReference) { + let _ = self.account.postbox.transaction({ transaction -> Void in + _internal_removeRecentlyUsedSticker(transaction: transaction, fileReference: fileReference) }).start() } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 5f187d2a3f..477b16b599 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1346,7 +1346,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { stickerPacks: [packReference], loadedStickerPacks: [], isEditing: true, - expandIfNeeded: false, + expandIfNeeded: true, parentNavigationController: interaction.getNavigationController(), sendSticker: { [weak interaction] fileReference, sourceView, sourceRect in return interaction?.sendSticker(fileReference, false, false, nil, false, sourceView, sourceRect, nil, []) ?? false @@ -2845,6 +2845,25 @@ public final class EmojiContentPeekBehaviorImpl: EmojiContentPeekBehavior { break } } + + if groupId == AnyHashable("recent") { + menuItems.append( + .action(ContextMenuActionItem(text: presentationData.strings.Stickers_RemoveFromRecent, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.default) + + guard let strongSelf = self else { + return + } + + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + interaction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Conversation_StickerRemovedFromRecent, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) + + strongSelf.context.engine.stickers.removeRecentlyUsedSticker(fileReference: .recentSticker(media: file)) + })) + ) + } } guard let view = view else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift index 68dbff32ac..610e25f56e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/StickerOutlineRenderPass.swift @@ -95,7 +95,8 @@ final class StickerOutlineRenderPass: RenderPass { return input } - let resultImage = outline.composited(over: image) + var resultImage = outline.composited(over: image) + resultImage = outline.composited(over: resultImage) if self.outputTexture == nil { let textureDescriptor = MTLTextureDescriptor() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 9f79ec454c..f30ac1668d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -141,11 +141,7 @@ private final class MediaCutoutScreenComponent: Component { 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 - } - } + 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) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 8de0b1a540..9c99b90398 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2866,8 +2866,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate 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 +// let filledSize = maskDrawingSize.aspectFitted(previewSize) + let maskScale = previewSize.width / min(maskDrawingSize.width, maskDrawingSize.height) initialMaskScale = maskScale initialMaskPosition = CGPoint(x: previewSize.width / 2.0, y: previewSize.height / 2.0) stickerMaskDrawingView.bounds = CGRect(origin: .zero, size: maskDrawingSize) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 413b61c9d3..02396e6e58 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -18,6 +18,7 @@ final class StickerCutoutOutlineView: UIView { let strokeLayer = SimpleShapeLayer() let imageLayer = SimpleLayer() var outlineLayer = CAEmitterLayer() + var outline2Layer = CAEmitterLayer() var glowLayer = CAEmitterLayer() override init(frame: CGRect) { @@ -56,23 +57,32 @@ final class StickerCutoutOutlineView: UIView { } private func setupAnimation(path: BezierPath) { + self.outlineLayer.removeFromSuperlayer() + self.outline2Layer.removeFromSuperlayer() + self.glowLayer.removeFromSuperlayer() + self.outlineLayer = CAEmitterLayer() - self.outlineLayer.opacity = 0.7 + self.outlineLayer.opacity = 0.77 + self.outline2Layer = CAEmitterLayer() + self.outline2Layer.opacity = 0.7 + self.glowLayer = CAEmitterLayer() self.layer.addSublayer(self.outlineLayer) +// self.layer.addSublayer(self.outline2Layer) self.layer.addSublayer(self.glowLayer) let randomBeginTime = (previousBeginTime + 4) % 6 previousBeginTime = randomBeginTime - let duration = min(5.0, max(2.0, path.length / 2200.0)) + let duration = min(6.0, max(2.5, path.length / 2200.0)) let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") outlineAnimation.path = path.path.cgPath outlineAnimation.duration = duration outlineAnimation.repeatCount = .infinity outlineAnimation.calculationMode = .paced + outlineAnimation.fillMode = .forwards outlineAnimation.beginTime = Double(randomBeginTime) self.outlineLayer.add(outlineAnimation, forKey: "emitterPosition") @@ -85,14 +95,49 @@ final class StickerCutoutOutlineView: UIView { lineEmitterCell.color = UIColor.white.cgColor lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage lineEmitterCell.lifetime = 2.2 - lineEmitterCell.birthRate = 120 - lineEmitterCell.scale = 0.14 + lineEmitterCell.birthRate = 700 + lineEmitterCell.scale = 0.17 lineEmitterCell.alphaSpeed = -0.4 self.outlineLayer.emitterCells = [lineEmitterCell] self.outlineLayer.emitterMode = .points - self.outlineLayer.emitterSize = CGSize(width: 1.0, height: 1.0) - self.outlineLayer.emitterShape = .point + self.outlineLayer.emitterSize = CGSize(width: 1.33, height: 1.33) + self.outlineLayer.emitterShape = .rectangle + + + + + + let outline2Animation = CAKeyframeAnimation(keyPath: "emitterPosition") + outline2Animation.path = path.path.cgPath + outline2Animation.duration = duration + outline2Animation.repeatCount = .infinity + outline2Animation.calculationMode = .paced + outline2Animation.fillMode = .forwards + outline2Animation.beginTime = Double(randomBeginTime) + 0.02 + self.outline2Layer.add(outline2Animation, forKey: "emitterPosition") + + let line2EmitterCell = CAEmitterCell() + line2EmitterCell.beginTime = CACurrentMediaTime() + let line2AlphaBehavior = createEmitterBehavior(type: "valueOverLife") + line2AlphaBehavior.setValue("color.alpha", forKey: "keyPath") + line2AlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") + line2EmitterCell.setValue([line2AlphaBehavior], forKey: "emitterBehaviors") + line2EmitterCell.color = UIColor.white.cgColor + line2EmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage + line2EmitterCell.lifetime = 2.2 + line2EmitterCell.birthRate = 500 + line2EmitterCell.scale = 0.14 + line2EmitterCell.alphaSpeed = -0.4 + + self.outline2Layer.emitterCells = [line2EmitterCell] + self.outline2Layer.emitterMode = .points + self.outline2Layer.emitterSize = CGSize(width: 1.5, height: 1.5) + self.outline2Layer.emitterShape = .circle + + + + let glowAnimation = CAKeyframeAnimation(keyPath: "emitterPosition") glowAnimation.path = path.path.cgPath @@ -123,6 +168,7 @@ final class StickerCutoutOutlineView: UIView { self.strokeLayer.animateAlpha(from: 0.0, to: CGFloat(self.strokeLayer.opacity), duration: 0.4) self.outlineLayer.animateAlpha(from: 0.0, to: CGFloat(self.outlineLayer.opacity), duration: 0.4, delay: 0.0) + self.outline2Layer.animateAlpha(from: 0.0, to: CGFloat(self.outline2Layer.opacity), duration: 0.4, delay: 0.0) self.glowLayer.animateAlpha(from: 0.0, to: CGFloat(self.glowLayer.opacity), duration: 0.4, delay: 0.0) let values = [1.0, 1.07, 1.0] @@ -133,6 +179,7 @@ final class StickerCutoutOutlineView: UIView { override func layoutSubviews() { self.strokeLayer.frame = self.bounds.offsetBy(dx: 0.0, dy: 1.0) self.outlineLayer.frame = self.bounds + self.outline2Layer.frame = self.bounds self.imageLayer.frame = self.bounds self.glowLayer.frame = self.bounds } @@ -152,8 +199,8 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE return nil } - contour = simplify(contour, tolerance: 1.4) - let path = BezierPath(points: contour, smooth: false) + contour = simplify(contour, tolerance: 1.0) + let path = BezierPath(points: contour, smooth: true) let firstScale = min(size.width, size.height) / 256.0 let secondScale = size.width / 1080.0 @@ -494,8 +541,59 @@ private class BezierPath { init(points: [CGPoint], smooth: Bool) { self.path = UIBezierPath() - + if smooth { + if points.count < 3 { + self.path.move(to: points.first ?? CGPoint.zero) + self.path.addLine(to: points[1]) + self.length = points[1].distanceFrom(points[0]) + return + } else { + self.path.move(to: points.first!) + + let n = points.count - 1 + let tension = 0.5 + + for i in 0 ..< n { + let currentPoint = points[i] + var nextIndex = (i + 1) % points.count + var prevIndex = i == 0 ? points.count - 1 : i - 1 + var nextNextIndex = (nextIndex + 1) % points.count + let prevPoint = points[prevIndex] + let nextPoint = points[nextIndex] + let nextNextPoint = points[nextNextIndex] + + let d1 = sqrt(pow(currentPoint.x - prevPoint.x, 2) + pow(currentPoint.y - prevPoint.y, 2)) + let d2 = sqrt(pow(nextPoint.x - currentPoint.x, 2) + pow(nextPoint.y - currentPoint.y, 2)) + let d3 = sqrt(pow(nextNextPoint.x - nextPoint.x, 2) + pow(nextNextPoint.y - nextPoint.y, 2)) + + var controlPoint1: CGPoint + if d1 < 0.0001 { + controlPoint1 = currentPoint + } else { + controlPoint1 = CGPoint(x: currentPoint.x + (tension * d2 / (d2 + d3)) * (nextPoint.x - prevPoint.x), + y: currentPoint.y + (tension * d2 / (d2 + d3)) * (nextPoint.y - prevPoint.y)) + } + + prevIndex = i + nextIndex = (i + 1) % points.count + nextNextIndex = (nextIndex + 1) % points.count + + let controlPoint2: CGPoint + if d3 < 0.0001 { + controlPoint2 = nextPoint + } else { + controlPoint2 = CGPoint(x: nextPoint.x - (tension * d2 / (d1 + d2)) * (nextNextPoint.x - currentPoint.x), + y: nextPoint.y - (tension * d2 / (d1 + d2)) * (nextNextPoint.y - currentPoint.y)) + } + + self.path.addCurve(to: nextPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) + self.length += nextPoint.distanceFrom(currentPoint) + } + + self.path.close() + } + } else if smooth { let K: CGFloat = 0.2 var c1 = [Int: CGPoint]() var c2 = [Int: CGPoint]() diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index bec990963e..b2304b60c9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -14362,7 +14362,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G Queue.mainQueue().after(3.0) { if let message = self.chatDisplayNode.historyNode.lastVisbleMesssage(), let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile, file.isSticker { - self.context.engine.stickers.addRecentlyUsedSticker(file: file) + self.context.engine.stickers.addRecentlyUsedSticker(fileReference: .message(message: MessageReference(message), media: file)) } } } From 1e155f324315afc171b376fd13e8dbe42582076d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 7 Apr 2024 20:24:02 +0400 Subject: [PATCH 09/14] [WIP] Stickers editor --- .../Sources/StickerPackScreen.swift | 2 +- .../Sources/ImageObjectClassification.swift | 3 --- .../Sources/MediaEditorScreen.swift | 6 ++--- .../Sources/StickerCutoutOutlineView.swift | 24 +++++++++++-------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index da8c3454b4..5d2a9aada7 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -544,7 +544,7 @@ private final class StickerPackContainer: ASDisplayNode { if let strongSelf = self { 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 { + if let self, let contorller = self.controller { 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)) } }) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectClassification.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectClassification.swift index af64865a5b..7ec65a5033 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectClassification.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectClassification.swift @@ -34,9 +34,6 @@ public func classifyImage(_ image: UIImage) -> Signal<[(String, Float)], NoError subscriber.putNext(filteredResult.map { ($0.identifier, $0.confidence) }) subscriber.putCompletion() } -#if targetEnvironment(simulator) - request.usesCPUOnly = true -#endif try? handler.perform([request]) return ActionDisposable { request.cancel() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 9c99b90398..b6bc653571 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3437,7 +3437,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } let currentTimestamp = CACurrentMediaTime() - if let previousPanTimestamp = self.previousPanTimestamp, currentTimestamp - previousPanTimestamp < 0.016 { + if let previousPanTimestamp = self.previousPanTimestamp, currentTimestamp - previousPanTimestamp < 0.016, case .changed = gestureRecognizer.state { return } self.previousPanTimestamp = currentTimestamp @@ -3449,7 +3449,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } let currentTimestamp = CACurrentMediaTime() - if let previousPinchTimestamp = self.previousPinchTimestamp, currentTimestamp - previousPinchTimestamp < 0.016 { + if let previousPinchTimestamp = self.previousPinchTimestamp, currentTimestamp - previousPinchTimestamp < 0.016, case .changed = gestureRecognizer.state { return } self.previousPinchTimestamp = currentTimestamp @@ -3461,7 +3461,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } let currentTimestamp = CACurrentMediaTime() - if let previousRotateTimestamp = self.previousRotateTimestamp, currentTimestamp - previousRotateTimestamp < 0.016 { + if let previousRotateTimestamp = self.previousRotateTimestamp, currentTimestamp - previousRotateTimestamp < 0.016, case .changed = gestureRecognizer.state { return } self.entitiesView.handleRotate(gestureRecognizer) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 02396e6e58..4a16dc3db1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -31,9 +31,7 @@ final class StickerCutoutOutlineView: UIView { self.strokeLayer.shadowRadius = 4.0 self.layer.allowsGroupOpacity = true - -// self.imageLayer.contentsGravity = .resizeAspect - + self.layer.addSublayer(self.strokeLayer) self.layer.addSublayer(self.imageLayer) } @@ -95,8 +93,8 @@ final class StickerCutoutOutlineView: UIView { lineEmitterCell.color = UIColor.white.cgColor lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage lineEmitterCell.lifetime = 2.2 - lineEmitterCell.birthRate = 700 - lineEmitterCell.scale = 0.17 + lineEmitterCell.birthRate = 800 + lineEmitterCell.scale = 0.18 lineEmitterCell.alphaSpeed = -0.4 self.outlineLayer.emitterCells = [lineEmitterCell] @@ -193,6 +191,7 @@ 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)) + let contourImageSize = image.extent.size.aspectFilled(CGSize(width: 256.0, height: 256.0)) var contour = findContours(pixelBuffer: pixelBuffer) guard !contour.isEmpty else { @@ -202,10 +201,9 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE contour = simplify(contour, tolerance: 1.0) let path = BezierPath(points: contour, smooth: true) - let firstScale = min(size.width, size.height) / 256.0 - let secondScale = size.width / 1080.0 + let contoursScale = min(size.width, size.height) / 256.0 + let valuesScale = size.width / 1080.0 - var transform = CGAffineTransform.identity let position = values.cropOffset let rotation = values.cropRotation let scale = values.cropScale @@ -215,9 +213,15 @@ private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaE y: (size.height - scaledImageSize.height * scale) / 2.0 ) - transform = transform.translatedBy(x: positionOffset.x + position.x * secondScale, y: positionOffset.y + position.y * secondScale) + var transform = CGAffineTransform.identity + transform = transform.translatedBy(x: contourImageSize.width / 2.0, y: contourImageSize.height / 2.0) transform = transform.rotated(by: rotation) - transform = transform.scaledBy(x: scale * firstScale, y: scale * firstScale) + transform = transform.translatedBy(x: -contourImageSize.width / 2.0, y: -contourImageSize.height / 2.0) + path.apply(transform, scale: 1.0) + + transform = CGAffineTransform.identity + transform = transform.translatedBy(x: positionOffset.x + position.x * valuesScale, y: positionOffset.y + position.y * valuesScale) + transform = transform.scaledBy(x: scale * contoursScale, y: scale * contoursScale) if !path.path.isEmpty { path.apply(transform, scale: scale) From f79bbc3ebabb2152a2be12e14607b42321f9773e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 8 Apr 2024 03:35:16 +0400 Subject: [PATCH 10/14] [WIP] Stickers editor --- .../PendingMessages/EnqueueMessage.swift | 6 +--- .../SyncCore/SyncCore_MediaReference.swift | 27 ++++++++++++++++ .../MediaEditor/Sources/MediaEditor.swift | 11 ++----- .../Sources/MediaCutoutScreen.swift | 32 ++++++++++++++----- .../Sources/MediaEditorScreen.swift | 31 ++++++++++++------ .../Sources/StickerCutoutOutlineView.swift | 15 +++++++++ .../TransformOutgoingMessageMedia.swift | 16 +++++----- 7 files changed, 98 insertions(+), 40 deletions(-) diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 2b70d10d63..7f812a1476 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -330,11 +330,7 @@ private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: if let mediaReference = mediaReference { signals.append(opportunisticallyTransformMessageWithMedia(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, mediaReference: mediaReference, userInteractive: userInteractive) |> map { result -> (Bool, EnqueueMessage) in - if let result = result { - return (true, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: .standalone(media: result.media), threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) - } else { - return (false, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) - } + return (result != nil, .message(text: text, attributes: attributes, inlineStickers: inlineStickers, mediaReference: result ?? mediaReference, threadId: threadId, replyToMessageId: replyToMessageId, replyToStoryId: replyToStoryId, localGroupingKey: localGroupingKey, correlationId: correlationId, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)) }) } else { signals.append(.single((false, message))) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 9d6213c71c..afe0961520 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -444,6 +444,33 @@ public enum AnyMediaReference: Equatable { } } + public func withUpdatedMedia(_ media: Media) -> AnyMediaReference { + switch self { + case .standalone: + return .standalone(media: media) + case let .message(message, _): + return .message(message: message, media: media) + case let .webPage(webPage, _): + return .webPage(webPage: webPage, media: media) + case let .stickerPack(stickerPack, _): + return .stickerPack(stickerPack: stickerPack, media: media) + case .savedGif: + return .savedGif(media: media) + case .savedSticker: + return .savedSticker(media: media) + case .recentSticker: + return .recentSticker(media: media) + case let .avatarList(peer, _): + return .avatarList(peer: peer, media: media) + case let .attachBot(peer, _): + return .attachBot(peer: peer, media: media) + case .customEmoji: + return .customEmoji(media: media) + case let .story(peer, id, _): + return .story(peer: peer, id: id, media: media) + } + } + public func resourceReference(_ resource: MediaResource) -> MediaResourceReference { return .media(media: self, resource: resource) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 13f4cc8701..2a94a06a80 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -194,7 +194,6 @@ public final class MediaEditor { public private(set) var canCutout: Bool = false 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 } @@ -1721,9 +1720,7 @@ public final class MediaEditor { } private var mainInputMask: MTLTexture? - public func removeSegmentationMask() { - self.isCutoutUpdated(false) - + public func removeSegmentationMask() { self.mainInputMask = nil self.renderer.currentMainInputMask = nil if !self.skipRendering { @@ -1731,15 +1728,11 @@ public final class MediaEditor { } } - public func setSegmentationMask(_ image: UIImage, andEnable enable: Bool = false, updateCutout: Bool = false) { + public func setSegmentationMask(_ image: UIImage, andEnable enable: Bool = false) { guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { return } - if updateCutout { - self.isCutoutUpdated(true) - } - //TODO:replace with pixelbuffer? self.mainInputMask = loadTexture(image: image, device: device) if enable { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index f30ac1668d..838538984c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -102,14 +102,15 @@ private final class MediaCutoutScreenComponent: Component { } @objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) { - guard let component = self.component else { + guard let component = self.component, let controller = self.environment?.controller() as? MediaCutoutScreen else { return } - let location = gestureRecognizer.location(in: gestureRecognizer.view) + + let location = gestureRecognizer.location(in: controller.drawingView) let point = CGPoint( - x: location.x / self.previewContainerView.frame.width, - y: location.y / self.previewContainerView.frame.height + x: location.x / controller.drawingView.bounds.width, + y: location.y / controller.drawingView.bounds.height ) component.mediaEditor.processImage { [weak self] originalImage, _ in @@ -118,7 +119,7 @@ private final class MediaCutoutScreenComponent: Component { 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, updateCutout: true) + component.mediaEditor.setSegmentationMask(mask) if let maskData = mask.pngData() { controller.drawingView.setup(withDrawing: maskData) } @@ -189,7 +190,7 @@ private final class MediaCutoutScreenComponent: Component { let mediaEditor = controller.mediaEditor if let drawingImage = controller.drawingView.drawingImage { - mediaEditor.setSegmentationMask(drawingImage, andEnable: true, updateCutout: false) + mediaEditor.setSegmentationMask(drawingImage, andEnable: true) } let initialOutlineValue = self.initialOutlineValue mediaEditor.setOnNextDisplay { [weak controller, weak mediaEditor] in @@ -197,6 +198,7 @@ private final class MediaCutoutScreenComponent: Component { if let initialOutlineValue { mediaEditor?.setToolValue(.stickerOutline, value: initialOutlineValue) } + controller?.completed() } self.animatingOut = true @@ -249,6 +251,7 @@ private final class MediaCutoutScreenComponent: Component { dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage) + controller.completedWithCutout() controller.requestDismiss(animated: true) } @@ -386,7 +389,19 @@ private final class MediaCutoutScreenComponent: Component { if labelView.superview == nil { self.buttonsContainerView.addSubview(labelView) } - transition.setFrame(view: labelView, frame: labelFrame) + if labelView.bounds.width > 0.0 && labelFrame.width != labelView.bounds.width { + if let snapshotView = labelView.snapshotView(afterScreenUpdates: false) { + snapshotView.center = labelView.center + self.buttonsContainerView.addSubview(snapshotView) + + labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + } + } + labelView.bounds = CGRect(origin: .zero, size: labelFrame.size) + transition.setPosition(view: labelView, position: labelFrame.center) } transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) @@ -403,7 +418,6 @@ private final class MediaCutoutScreenComponent: Component { 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 @@ -598,6 +612,8 @@ final class MediaCutoutScreen: ViewController { fileprivate let overlayView: UIView fileprivate let backgroundView: UIView + var completed: () -> Void = {} + var completedWithCutout: () -> Void = {} var dismissed: () -> Void = {} private var initialValues: MediaEditorValues diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index b6bc653571..cf097e1d81 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1132,10 +1132,14 @@ final class MediaEditorScreenComponent: Component { } 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 + if transition.animation.isImmediate { cutoutButtonView.removeFromSuperview() - }) - cutoutButtonView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, delay: 0.0) + } else { + 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) + } } } @@ -2927,13 +2931,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.hasTransparency = hasTransparency self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) } - mediaEditor.isCutoutUpdated = { [weak self] isCutout in - guard let self else { - return - } - self.isCutout = isCutout - self.requestLayout(forceUpdate: true, transition: .immediate) - } mediaEditor.maskUpdated = { [weak self] mask in guard let self else { return @@ -4736,6 +4733,17 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate overlayView: self.stickerMaskPreviewView, backgroundView: stickerBackgroundView ) + cutoutController.completedWithCutout = { [weak self] in + if let self { + self.isCutout = true + self.requestLayout(forceUpdate: true, transition: .immediate) + } + } + cutoutController.completed = { [weak self] in + if let self { + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) + } + } cutoutController.dismissed = { [weak self] in if let self { self.animateInFromTool(inPlace: true) @@ -4802,6 +4810,9 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate }) mediaEditor.removeSegmentationMask() self.stickerMaskDrawingView?.clearWithEmptyColor() + + self.isCutout = false + self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25)) } if let value = mediaEditor.getToolValue(.stickerOutline) as? Float, value > 0.0 { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index 4a16dc3db1..5e001b4105 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -169,6 +169,21 @@ final class StickerCutoutOutlineView: UIView { self.outline2Layer.animateAlpha(from: 0.0, to: CGFloat(self.outline2Layer.opacity), duration: 0.4, delay: 0.0) self.glowLayer.animateAlpha(from: 0.0, to: CGFloat(self.glowLayer.opacity), duration: 0.4, delay: 0.0) + + self.animateBump(path: path) + } + + private func animateBump(path: BezierPath) { + let boundingBox = path.path.cgPath.boundingBox + let pathCenter = CGPoint(x: boundingBox.midX, y: boundingBox.midY) + +// let originalPosition = self.imageLayer.position +// let originalAnchorPoint = self.imageLayer.anchorPoint + + let layerPathCenter = self.imageLayer.convert(pathCenter, from: self.imageLayer.superlayer) + self.imageLayer.anchorPoint = CGPoint(x: layerPathCenter.x / layer.bounds.width, y: layerPathCenter.y / layer.bounds.height) + self.imageLayer.position = layerPathCenter + let values = [1.0, 1.07, 1.0] let keyTimes = [0.0, 0.67, 1.0] self.imageLayer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.4, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) diff --git a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift index 2d0679a638..9bf6587903 100644 --- a/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift +++ b/submodules/TelegramUI/Sources/TransformOutgoingMessageMedia.swift @@ -68,21 +68,21 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } attributes.append(.ImageSize(size: PixelDimensions(imageDimensions))) let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)]).withUpdatedAttributes(attributes) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } else { let updatedFile = file.withUpdatedSize(data.size) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } } else { let updatedFile = file.withUpdatedSize(data.size) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } } else { let updatedFile = file.withUpdatedSize(data.size) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } @@ -97,11 +97,11 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me let scaledImageSize = CGSize(width: scaledImage.size.width * scaledImage.scale, height: scaledImage.size.height * scaledImage.scale) let updatedFile = file.withUpdatedSize(data.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledImageSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)]) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } else { let updatedFile = file.withUpdatedSize(data.size) - subscriber.putNext(.standalone(media: updatedFile)) + subscriber.putNext(media.withUpdatedMedia(updatedFile)) subscriber.putCompletion() } @@ -109,7 +109,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } |> runOn(opportunistic ? Queue.mainQueue() : Queue.concurrentDefaultQueue()) } else { let updatedFile = file.withUpdatedSize(data.size) - return .single(.standalone(media: updatedFile)) + return .single(media.withUpdatedMedia(updatedFile)) } } else if opportunistic { return .single(nil) @@ -158,7 +158,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me postbox.mediaBox.storeResourceData(thumbnailResource.id, data: smallestData) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(smallestSize), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let updatedImage = TelegramMediaImage(imageId: image.imageId, representations: representations, immediateThumbnailData: image.immediateThumbnailData, reference: image.reference, partialReference: image.partialReference, flags: []) - return .single(.standalone(media: updatedImage)) + return .single(media.withUpdatedMedia(updatedImage)) } } From 2e38e21c9a1b5a60631f204b9b48828b37a88eb1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 8 Apr 2024 03:36:33 +0400 Subject: [PATCH 11/14] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 91e354dc69..591e528c6f 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.10.1", + "app": "10.11", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0" From 30d31c42c689f1bae5ede561666c7d81188c1595 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 8 Apr 2024 15:18:16 +0400 Subject: [PATCH 12/14] Fix --- .../Sources/ItemListController.swift | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/submodules/ItemListUI/Sources/ItemListController.swift b/submodules/ItemListUI/Sources/ItemListController.swift index 2fa6bbdc1d..2ea7923f7b 100644 --- a/submodules/ItemListUI/Sources/ItemListController.swift +++ b/submodules/ItemListUI/Sources/ItemListController.swift @@ -325,13 +325,17 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable strongSelf.navigationItem.titleView = nil strongSelf.segmentedTitleView = nil strongSelf.navigationBar?.setContentNode(nil, animated: false) - strongSelf.controllerNode.panRecognizer?.isEnabled = false + if strongSelf.isNodeLoaded { + 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) + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.panRecognizer?.isEnabled = false + } case let .sectionControl(sections, index): strongSelf.title = "" if let segmentedTitleView = strongSelf.segmentedTitleView, segmentedTitleView.segments == sections { @@ -347,7 +351,9 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable } } strongSelf.navigationBar?.setContentNode(nil, animated: false) - strongSelf.controllerNode.panRecognizer?.isEnabled = false + if strongSelf.isNodeLoaded { + strongSelf.controllerNode.panRecognizer?.isEnabled = false + } case let .textWithTabs(title, sections, index): strongSelf.title = title if let tabsNavigationContentNode = strongSelf.tabsNavigationContentNode, tabsNavigationContentNode.segments == sections { @@ -366,21 +372,23 @@ 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 + if strongSelf.isNodeLoaded { + 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.panGestureAllowedDirections = { + if index == 0 { + return [.leftCenter] + } else if index == sections.count - 1 { + return [.rightCenter] + } else { + return [.leftCenter, .rightCenter] + } } + strongSelf.controllerNode.panRecognizer?.isEnabled = true } - strongSelf.controllerNode.panRecognizer?.isEnabled = true } } strongSelf.navigationButtonActions = (left: controllerState.leftNavigationButton?.action, right: controllerState.rightNavigationButton?.action, secondaryRight: controllerState.secondaryRightNavigationButton?.action) From a50a53777475289d90fcb415101e897733b6f576 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 8 Apr 2024 15:37:36 +0400 Subject: [PATCH 13/14] Fix sticker emoji selection --- .../ReactionSelectionNode/Sources/ReactionContextNode.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 7b5e40c25d..33b3547781 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -2780,6 +2780,10 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.view.endEditing(true) self.longPressRecognizer?.isEnabled = false + guard self.isExpanded else { + return + } + self.animateFromExtensionDistance = 0.0 self.extensionDistance = 0.0 self.visibleExtensionDistance = 0.0 From 9ec01a7f24efe698cefef01673822e7888fe6eb3 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 8 Apr 2024 16:42:46 +0400 Subject: [PATCH 14/14] Update API --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + submodules/TelegramApi/Sources/Api0.swift | 5 +- submodules/TelegramApi/Sources/Api22.swift | 34 +++++---- submodules/TelegramApi/Sources/Api26.swift | 70 ++++++++----------- submodules/TelegramApi/Sources/Api27.swift | 40 +++++++++++ .../ApiUtils/TelegramMediaWebpage.swift | 11 +++ .../PendingMessageUploadedContent.swift | 6 +- .../SyncCore_TelegramMediaWebpage.swift | 58 +++++++++++++++ .../TelegramEngine/Messages/AdMessages.swift | 32 +++++++-- .../ChatMessageWebpageBubbleContentNode.swift | 2 + 10 files changed, 199 insertions(+), 61 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d7f77c8498..47151fb452 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11901,3 +11901,5 @@ Sorry for the inconvenience."; "Stickers.RemoveFromRecent" = "Remove from Recents"; "Conversation.StickerRemovedFromRecent" = "Sticker was removed from Recents."; + +"Conversation.ViewStickers" = "VIEW STICKERS"; diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 65c96ee419..16bf8a63a6 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -857,7 +857,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1239335713] = { return Api.ShippingOption.parse_shippingOption($0) } dict[-2010155333] = { return Api.SimpleWebViewResult.parse_simpleWebViewResultUrl($0) } dict[-425595208] = { return Api.SmsJob.parse_smsJob($0) } - dict[-1611532106] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } + dict[-1108478618] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) } dict[-1237848657] = { return Api.StatsDateRangeDays.parse_statsDateRangeDays($0) } @@ -1071,6 +1071,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[555358088] = { return Api.WebPage.parse_webPageEmpty($0) } dict[1930545681] = { return Api.WebPage.parse_webPageNotModified($0) } dict[-1328464313] = { return Api.WebPage.parse_webPagePending($0) } + dict[1355547603] = { return Api.WebPageAttribute.parse_webPageAttributeStickerSet($0) } dict[781501415] = { return Api.WebPageAttribute.parse_webPageAttributeStory($0) } dict[1421174295] = { return Api.WebPageAttribute.parse_webPageAttributeTheme($0) } dict[211046684] = { return Api.WebViewMessageSent.parse_webViewMessageSent($0) } @@ -1351,7 +1352,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramApi/Sources/Api22.swift b/submodules/TelegramApi/Sources/Api22.swift index c028e604e9..d92743523e 100644 --- a/submodules/TelegramApi/Sources/Api22.swift +++ b/submodules/TelegramApi/Sources/Api22.swift @@ -478,13 +478,13 @@ public extension Api { } public extension Api { enum SponsoredMessage: TypeConstructorDescription { - case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) + case sponsoredMessage(flags: Int32, randomId: Buffer, url: String, title: String, message: String, entities: [Api.MessageEntity]?, photo: Api.Photo?, color: Api.PeerColor?, buttonText: String, sponsorInfo: String?, additionalInfo: String?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let buttonText, let sponsorInfo, let additionalInfo): + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let color, let buttonText, let sponsorInfo, let additionalInfo): if boxed { - buffer.appendInt32(-1611532106) + buffer.appendInt32(-1108478618) } serializeInt32(flags, buffer: buffer, boxed: false) serializeBytes(randomId, buffer: buffer, boxed: false) @@ -497,6 +497,7 @@ public extension Api { item.serialize(buffer, true) }} if Int(flags) & Int(1 << 6) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 13) != 0 {color!.serialize(buffer, true)} serializeString(buttonText, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 7) != 0 {serializeString(sponsorInfo!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(additionalInfo!, buffer: buffer, boxed: false)} @@ -506,8 +507,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let buttonText, let sponsorInfo, let additionalInfo): - return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) + case .sponsoredMessage(let flags, let randomId, let url, let title, let message, let entities, let photo, let color, let buttonText, let sponsorInfo, let additionalInfo): + return ("sponsoredMessage", [("flags", flags as Any), ("randomId", randomId as Any), ("url", url as Any), ("title", title as Any), ("message", message as Any), ("entities", entities as Any), ("photo", photo as Any), ("color", color as Any), ("buttonText", buttonText as Any), ("sponsorInfo", sponsorInfo as Any), ("additionalInfo", additionalInfo as Any)]) } } @@ -530,12 +531,16 @@ public extension Api { if Int(_1!) & Int(1 << 6) != 0 {if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.Photo } } - var _8: String? - _8 = parseString(reader) + var _8: Api.PeerColor? + if Int(_1!) & Int(1 << 13) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.PeerColor + } } var _9: String? - if Int(_1!) & Int(1 << 7) != 0 {_9 = parseString(reader) } + _9 = parseString(reader) var _10: String? - if Int(_1!) & Int(1 << 8) != 0 {_10 = parseString(reader) } + if Int(_1!) & Int(1 << 7) != 0 {_10 = parseString(reader) } + var _11: String? + if Int(_1!) & Int(1 << 8) != 0 {_11 = parseString(reader) } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -543,11 +548,12 @@ public extension Api { let _c5 = _5 != nil let _c6 = (Int(_1!) & Int(1 << 1) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 6) == 0) || _7 != nil - let _c8 = _8 != nil - let _c9 = (Int(_1!) & Int(1 << 7) == 0) || _9 != nil - let _c10 = (Int(_1!) & Int(1 << 8) == 0) || _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, buttonText: _8!, sponsorInfo: _9, additionalInfo: _10) + let _c8 = (Int(_1!) & Int(1 << 13) == 0) || _8 != nil + let _c9 = _9 != nil + let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { + return Api.SponsoredMessage.sponsoredMessage(flags: _1!, randomId: _2!, url: _3!, title: _4!, message: _5!, entities: _6, photo: _7, color: _8, buttonText: _9!, sponsorInfo: _10, additionalInfo: _11) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 22cbd99c52..189331107c 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,10 +1,22 @@ public extension Api { indirect enum WebPageAttribute: TypeConstructorDescription { + case webPageAttributeStickerSet(flags: Int32, stickers: [Api.Document]) case webPageAttributeStory(flags: Int32, peer: Api.Peer, id: Int32, story: Api.StoryItem?) case webPageAttributeTheme(flags: Int32, documents: [Api.Document]?, settings: Api.ThemeSettings?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { + case .webPageAttributeStickerSet(let flags, let stickers): + if boxed { + buffer.appendInt32(1355547603) + } + serializeInt32(flags, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(stickers.count)) + for item in stickers { + item.serialize(buffer, true) + } + break case .webPageAttributeStory(let flags, let peer, let id, let story): if boxed { buffer.appendInt32(781501415) @@ -31,6 +43,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { + case .webPageAttributeStickerSet(let flags, let stickers): + return ("webPageAttributeStickerSet", [("flags", flags as Any), ("stickers", stickers as Any)]) case .webPageAttributeStory(let flags, let peer, let id, let story): return ("webPageAttributeStory", [("flags", flags as Any), ("peer", peer as Any), ("id", id as Any), ("story", story as Any)]) case .webPageAttributeTheme(let flags, let documents, let settings): @@ -38,6 +52,22 @@ public extension Api { } } + public static func parse_webPageAttributeStickerSet(_ reader: BufferReader) -> WebPageAttribute? { + var _1: Int32? + _1 = reader.readInt32() + var _2: [Api.Document]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.WebPageAttribute.webPageAttributeStickerSet(flags: _1!, stickers: _2!) + } + else { + return nil + } + } public static func parse_webPageAttributeStory(_ reader: BufferReader) -> WebPageAttribute? { var _1: Int32? _1 = reader.readInt32() @@ -1330,43 +1360,3 @@ public extension Api.account { } } -public extension Api.account { - enum TmpPassword: TypeConstructorDescription { - case tmpPassword(tmpPassword: Buffer, validUntil: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .tmpPassword(let tmpPassword, let validUntil): - if boxed { - buffer.appendInt32(-614138572) - } - serializeBytes(tmpPassword, buffer: buffer, boxed: false) - serializeInt32(validUntil, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .tmpPassword(let tmpPassword, let validUntil): - return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)]) - } - } - - public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? { - var _1: Buffer? - _1 = parseBytes(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index b154cef686..babd3e977a 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,43 @@ +public extension Api.account { + enum TmpPassword: TypeConstructorDescription { + case tmpPassword(tmpPassword: Buffer, validUntil: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .tmpPassword(let tmpPassword, let validUntil): + if boxed { + buffer.appendInt32(-614138572) + } + serializeBytes(tmpPassword, buffer: buffer, boxed: false) + serializeInt32(validUntil, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .tmpPassword(let tmpPassword, let validUntil): + return ("tmpPassword", [("tmpPassword", tmpPassword as Any), ("validUntil", validUntil as Any)]) + } + } + + public static func parse_tmpPassword(_ reader: BufferReader) -> TmpPassword? { + var _1: Buffer? + _1 = parseBytes(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.account.TmpPassword.tmpPassword(tmpPassword: _1!, validUntil: _2!) + } + else { + return nil + } + } + + } +} public extension Api.account { enum WallPapers: TypeConstructorDescription { case wallPapers(hash: Int64, wallpapers: [Api.WallPaper]) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift index 461472ee2a..b5c63bcf8b 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaWebpage.swift @@ -12,6 +12,17 @@ func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPa files = documents.compactMap { telegramMediaFileFromApiDocument($0) } } return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) })) + case let .webPageAttributeStickerSet(apiFlags, stickers): + var flags = TelegramMediaWebpageStickerPackAttribute.Flags() + if (apiFlags & (1 << 0)) != 0 { + flags.insert(.isEmoji) + } + if (apiFlags & (1 << 1)) != 0 { + flags.insert(.isTemplate) + } + var files: [TelegramMediaFile] = [] + files = stickers.compactMap { telegramMediaFileFromApiDocument($0) } + return .stickerPack(TelegramMediaWebpageStickerPackAttribute(flags: flags, files: files)) case .webPageAttributeStory: return nil } diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 389fc5dc23..6068f07450 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -145,7 +145,11 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post if let mediaReference = mediaReference { finalMediaReference = mediaReference } else if file.isSticker { - finalMediaReference = .standalone(media: file) + if let partialReference = file.partialReference { + finalMediaReference = partialReference.mediaReference(file) + } else { + finalMediaReference = .standalone(media: file) + } } else { finalMediaReference = .savedGif(media: file) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift index 2fcf26ac99..de0cecfa5f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaWebpage.swift @@ -3,16 +3,20 @@ import Postbox private enum TelegramMediaWebpageAttributeTypes: Int32 { case unsupported case theme + case stickerPack } public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case unsupported case theme(TelegraMediaWebpageThemeAttribute) + case stickerPack(TelegramMediaWebpageStickerPackAttribute) public init(decoder: PostboxDecoder) { switch decoder.decodeInt32ForKey("r", orElse: 0) { case TelegramMediaWebpageAttributeTypes.theme.rawValue: self = .theme(decoder.decodeObjectForKey("a", decoder: { TelegraMediaWebpageThemeAttribute(decoder: $0) }) as! TelegraMediaWebpageThemeAttribute) + case TelegramMediaWebpageAttributeTypes.stickerPack.rawValue: + self = .stickerPack(decoder.decodeObjectForKey("a", decoder: { TelegramMediaWebpageStickerPackAttribute(decoder: $0) }) as! TelegramMediaWebpageStickerPackAttribute) default: self = .unsupported } @@ -25,6 +29,9 @@ public enum TelegramMediaWebpageAttribute: PostboxCoding, Equatable { case let .theme(attribute): encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.theme.rawValue, forKey: "r") encoder.encodeObject(attribute, forKey: "a") + case let .stickerPack(attribute): + encoder.encodeInt32(TelegramMediaWebpageAttributeTypes.stickerPack.rawValue, forKey: "r") + encoder.encodeObject(attribute, forKey: "a") } } } @@ -69,6 +76,57 @@ public final class TelegraMediaWebpageThemeAttribute: PostboxCoding, Equatable { } } +public final class TelegramMediaWebpageStickerPackAttribute: PostboxCoding, Equatable { + public struct Flags: OptionSet { + public var rawValue: Int32 + + public init() { + self.rawValue = 0 + } + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let isEmoji = Flags(rawValue: 1 << 0) + public static let isTemplate = Flags(rawValue: 1 << 1) + } + + public static func == (lhs: TelegramMediaWebpageStickerPackAttribute, rhs: TelegramMediaWebpageStickerPackAttribute) -> Bool { + if lhs.flags != rhs.flags { + return false + } + if lhs.files.count != rhs.files.count { + return false + } else { + for i in 0 ..< lhs.files.count { + if !lhs.files[i].isEqual(to: rhs.files[i]) { + return false + } + } + } + return true + } + + public let flags: Flags + public let files: [TelegramMediaFile] + + public init(flags: Flags, files: [TelegramMediaFile]) { + self.flags = flags + self.files = files + } + + public init(decoder: PostboxDecoder) { + self.flags = Flags(rawValue: decoder.decodeInt32ForKey("flags", orElse: 0)) + self.files = decoder.decodeObjectArrayForKey("files") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.flags.rawValue, forKey: "flags") + encoder.encodeObjectArray(self.files, forKey: "files") + } +} + public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable { public let url: String public let displayUrl: String diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index faaed76cf0..4c5b712b09 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -12,6 +12,8 @@ private class AdMessagesHistoryContextImpl { case text case textEntities case media + case color + case backgroundEmojiId case url case buttonText case sponsorInfo @@ -30,6 +32,8 @@ private class AdMessagesHistoryContextImpl { public let text: String public let textEntities: [MessageTextEntity] public let media: [Media] + public let color: PeerNameColor? + public let backgroundEmojiId: Int64? public let url: String public let buttonText: String public let sponsorInfo: String? @@ -43,6 +47,8 @@ private class AdMessagesHistoryContextImpl { text: String, textEntities: [MessageTextEntity], media: [Media], + color: PeerNameColor?, + backgroundEmojiId: Int64?, url: String, buttonText: String, sponsorInfo: String?, @@ -55,6 +61,8 @@ private class AdMessagesHistoryContextImpl { self.text = text self.textEntities = textEntities self.media = media + self.color = color + self.backgroundEmojiId = backgroundEmojiId self.url = url self.buttonText = buttonText self.sponsorInfo = sponsorInfo @@ -81,6 +89,8 @@ private class AdMessagesHistoryContextImpl { self.media = mediaData.compactMap { data -> Media? in return PostboxDecoder(buffer: MemoryBuffer(data: data)).decodeRootObject() as? Media } + self.color = try container.decodeIfPresent(Int32.self, forKey: .color).flatMap { PeerNameColor(rawValue: $0) } + self.backgroundEmojiId = try container.decodeIfPresent(Int64.self, forKey: .backgroundEmojiId) self.url = try container.decode(String.self, forKey: .url) self.buttonText = try container.decode(String.self, forKey: .buttonText) @@ -107,6 +117,9 @@ private class AdMessagesHistoryContextImpl { } try container.encode(mediaData, forKey: .media) + try container.encodeIfPresent(self.color?.rawValue, forKey: .color) + try container.encodeIfPresent(self.backgroundEmojiId, forKey: .backgroundEmojiId) + try container.encode(self.url, forKey: .url) try container.encode(self.buttonText, forKey: .buttonText) @@ -197,8 +210,8 @@ private class AdMessagesHistoryContextImpl { defaultBannedRights: nil, usernames: [], storiesHidden: nil, - nameColor: .blue, - backgroundEmojiId: nil, + nameColor: self.color ?? .blue, + backgroundEmojiId: self.backgroundEmojiId, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, @@ -408,7 +421,7 @@ private class AdMessagesHistoryContextImpl { for message in messages { switch message { - case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, buttonText, sponsorInfo, additionalInfo): + case let .sponsoredMessage(flags, randomId, url, title, message, entities, photo, color, buttonText, sponsorInfo, additionalInfo): var parsedEntities: [MessageTextEntity] = [] if let entities = entities { parsedEntities = messageTextEntitiesFromApiEntities(entities) @@ -417,8 +430,17 @@ private class AdMessagesHistoryContextImpl { let isRecommended = (flags & (1 << 5)) != 0 let canReport = (flags & (1 << 12)) != 0 + var nameColorIndex: Int32? + var backgroundEmojiId: Int64? + if let color = color { + switch color { + case let .peerColor(_, color, backgroundEmojiIdValue): + nameColorIndex = color + backgroundEmojiId = backgroundEmojiIdValue + } + } + let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) } - parsedMessages.append(CachedMessage( opaqueId: randomId.makeData(), messageType: isRecommended ? .recommended : .sponsored, @@ -426,6 +448,8 @@ private class AdMessagesHistoryContextImpl { text: message, textEntities: parsedEntities, media: photo.flatMap { [$0] } ?? [], + color: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, + backgroundEmojiId: backgroundEmojiId, url: url, buttonText: buttonText, sponsorInfo: sponsorInfo, diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index fc6b107e92..bbc8da69bb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -466,6 +466,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Conversation_BoostChannel case "telegram_group_boost": actionTitle = item.presentationData.strings.Conversation_BoostChannel + case "telegram_stickerset": + actionTitle = item.presentationData.strings.Conversation_ViewStickers default: break }