From ce3a6c32023f140cf2c17a5ad19ba4cdd0a18912 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 28 Feb 2024 23:17:35 +0400 Subject: [PATCH 1/6] Various fixes --- .../Sources/PremiumIntroScreen.swift | 99 ++++++++++++++++--- .../Sources/PremiumLimitsListScreen.swift | 66 ++++++++----- .../TelegramNotices/Sources/Notices.swift | 26 +++++ .../AvatarStoryIndicatorComponent.swift | 9 +- 4 files changed, 161 insertions(+), 39 deletions(-) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index d5d9c5fbd6..21d6f91c16 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1531,8 +1531,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ApplicationSpecificNotice.dismissedPremiumColorsBadge(accountManager: context.sharedContext.accountManager), ApplicationSpecificNotice.dismissedMessageTagsBadge(accountManager: context.sharedContext.accountManager), ApplicationSpecificNotice.dismissedLastSeenBadge(accountManager: context.sharedContext.accountManager), - ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager) - ).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge in + ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager), + ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager) + ).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge, dismissedBusinessBadge in guard let self else { return } @@ -1552,8 +1553,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if !dismissedMessagePrivacyBadge { newPerks.append(PremiumPerk.messagePrivacy.identifier) } - //TODO: - newPerks.append(PremiumPerk.business.identifier) + if !dismissedBusinessBadge { + newPerks.append(PremiumPerk.business.identifier) + } self.newPerks = newPerks self.updated() }) @@ -1937,17 +1939,31 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if case .business = context.component.mode, case .business = perk { continue } + + let isNew = state.newPerks.contains(perk.identifier) + let titleComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: perk.title(strings: strings), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 0 + )) + + let titleCombinedComponent: AnyComponent + if isNew { + titleCombinedComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BadgeComponent(color: gradientColors[i], text: strings.Premium_New))) + ], spacing: 5.0)) + } else { + titleCombinedComponent = AnyComponent(HStack([AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent)], spacing: 0.0)) + } + perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: perk.title(strings: strings), - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemPrimaryTextColor - )), - maximumNumberOfLines: 0 - ))), + AnyComponentWithIdentity(id: AnyHashable(0), component: titleCombinedComponent), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: perk.subtitle(strings: strings), @@ -2013,6 +2029,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() case .business: demoSubject = .business + let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone() default: demoSubject = .doubleLimits } @@ -3721,3 +3738,61 @@ private final class EmojiActionIconComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private final class BadgeComponent: CombinedComponent { + let color: UIColor + let text: String + + init( + color: UIColor, + text: String + ) { + self.color = color + self.text = text + } + + static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.text != rhs.text { + return false + } + return true + } + + static var body: Body { + let badgeBackground = Child(RoundedRectangle.self) + let badgeText = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + let badgeText = badgeText.update( + component: MultilineTextComponent(text: .plain(NSAttributedString(string: component.text, font: Font.semibold(11.0), textColor: .white))), + availableSize: context.availableSize, + transition: context.transition + ) + + let badgeSize = CGSize(width: badgeText.size.width + 7.0, height: 16.0) + let badgeBackground = badgeBackground.update( + component: RoundedRectangle( + color: component.color, + cornerRadius: 5.0 + ), + availableSize: badgeSize, + transition: context.transition + ) + + context.add(badgeBackground + .position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0)) + ) + + context.add(badgeText + .position(CGPoint(x: badgeSize.width / 2.0, y: badgeSize.height / 2.0)) + ) + + return badgeSize + } + } +} diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 3ae7dc6294..4fd0a10755 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -384,7 +384,27 @@ public class PremiumLimitsListScreen: ViewController { let theme = self.presentationData.theme let strings = self.presentationData.strings - if let stickers = self.stickers, let appIcons = self.appIcons, let configuration = self.promoConfiguration { + let videos: [String: TelegramMediaFile] = self.promoConfiguration?.videos ?? [:] + let stickers = self.stickers ?? [] + let appIcons = self.appIcons ?? [] + + let isReady: Bool + switch controller.subject { + case .premiumStickers: + isReady = !stickers.isEmpty + case .appIcons: + isReady = !appIcons.isEmpty + case .stories: + isReady = true + case .doubleLimits: + isReady = true + case .business: + isReady = true + default: + isReady = !videos.isEmpty + } + + if isReady { let context = controller.context let textColor = theme.actionSheet.primaryTextColor @@ -482,7 +502,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["more_upload"], + videoFile: videos["more_upload"], decoration: .dataRain )), title: strings.Premium_UploadSize, @@ -500,7 +520,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["faster_download"], + videoFile: videos["faster_download"], decoration: .fasterStars )), title: strings.Premium_FasterSpeed, @@ -518,7 +538,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["voice_to_text"], + videoFile: videos["voice_to_text"], decoration: .badgeStars )), title: strings.Premium_VoiceToText, @@ -536,7 +556,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["no_ads"], + videoFile: videos["no_ads"], decoration: .swirlStars )), title: strings.Premium_NoAds, @@ -554,7 +574,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["infinite_reactions"], + videoFile: videos["infinite_reactions"], decoration: .swirlStars )), title: strings.Premium_InfiniteReactions, @@ -593,7 +613,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["emoji_status"], + videoFile: videos["emoji_status"], decoration: .badgeStars )), title: strings.Premium_EmojiStatus, @@ -611,7 +631,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["advanced_chat_management"], + videoFile: videos["advanced_chat_management"], decoration: .swirlStars )), title: strings.Premium_ChatManagement, @@ -629,7 +649,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["profile_badge"], + videoFile: videos["profile_badge"], decoration: .badgeStars )), title: strings.Premium_Badge, @@ -647,7 +667,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["animated_userpics"], + videoFile: videos["animated_userpics"], decoration: .swirlStars )), title: strings.Premium_Avatar, @@ -681,7 +701,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .bottom, - videoFile: configuration.videos["animated_emoji"], + videoFile: videos["animated_emoji"], decoration: .emoji )), title: strings.Premium_AnimatedEmoji, @@ -700,7 +720,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["translations"], + videoFile: videos["translations"], decoration: .hello )), title: strings.Premium_Translation, @@ -718,7 +738,7 @@ public class PremiumLimitsListScreen: ViewController { content: AnyComponent(PhoneDemoComponent( context: context, position: .top, - videoFile: configuration.videos["peer_colors"], + videoFile: videos["peer_colors"], decoration: .badgeStars )), title: strings.Premium_Colors, @@ -737,7 +757,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["wallpapers"], + videoFile: videos["wallpapers"], decoration: .swirlStars )), title: strings.Premium_Wallpapers, @@ -756,7 +776,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["saved_tags"], + videoFile: videos["saved_tags"], decoration: .tag )), title: strings.Premium_MessageTags, @@ -775,7 +795,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["last_seen"], + videoFile: videos["last_seen"], decoration: .badgeStars )), title: strings.Premium_LastSeen, @@ -794,7 +814,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["message_privacy"], + videoFile: videos["message_privacy"], decoration: .swirlStars )), title: strings.Premium_MessagePrivacy, @@ -846,7 +866,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["business_location"], + videoFile: videos["business_location"], decoration: .business )), title: strings.Business_Location, @@ -866,7 +886,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["business_hours"], + videoFile: videos["business_hours"], decoration: .business )), title: strings.Business_OpeningHours, @@ -886,7 +906,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["quick_replies"], + videoFile: videos["quick_replies"], decoration: .business )), title: strings.Business_QuickReplies, @@ -906,7 +926,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["greeting_message"], + videoFile: videos["greeting_message"], decoration: .business )), title: strings.Business_GreetingMessages, @@ -926,7 +946,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["away_message"], + videoFile: videos["away_message"], decoration: .business )), title: strings.Business_AwayMessages, @@ -946,7 +966,7 @@ public class PremiumLimitsListScreen: ViewController { context: context, position: .top, model: .island, - videoFile: configuration.videos["business_bots"], + videoFile: videos["business_bots"], decoration: .business )), title: strings.Business_Chatbots, diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 627928f5ac..7eee7c482b 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -199,6 +199,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case savedMessageTagLabelSuggestion = 65 case dismissedLastSeenBadge = 66 case dismissedMessagePrivacyBadge = 67 + case dismissedBusinessBadge = 68 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -529,6 +530,10 @@ private struct ApplicationSpecificNoticeKeys { static func dismissedMessagePrivacyBadge() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedMessagePrivacyBadge.key) } + + static func dismissedBusinessBadge() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedBusinessBadge.key) + } } public struct ApplicationSpecificNotice { @@ -2223,4 +2228,25 @@ public struct ApplicationSpecificNotice { } |> take(1) } + + public static func setDismissedBusinessBadge(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Void in + if let entry = CodableEntry(ApplicationSpecificBoolNotice()) { + transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessBadge(), entry) + } + } + |> ignoreValues + } + + public static func dismissedBusinessBadge(accountManager: AccountManager) -> Signal { + return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessBadge()) + |> map { view -> Bool in + if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) { + return true + } else { + return false + } + } + |> take(1) + } } diff --git a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift index ebe68f2d33..5cd33daa87 100644 --- a/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent/Sources/AvatarStoryIndicatorComponent.swift @@ -345,16 +345,17 @@ public final class AvatarStoryIndicatorComponent: Component { } let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! - - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + if let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations) { + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } } } } else { let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth context.setLineWidth(lineWidth) if component.isRoundedRect { - context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.25)).cgPath) + let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: floor(diameter * 0.27)) + context.addPath(path.cgPath) } else { context.addEllipse(in: CGRect(origin: CGPoint(x: size.width * 0.5 - diameter * 0.5, y: size.height * 0.5 - diameter * 0.5), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)) } From 5ce85e7b7599e213ae532f47cddf744025210047 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Feb 2024 01:35:52 +0400 Subject: [PATCH 2/6] Premium and business screens improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 + .../AccountContext/Sources/Premium.swift | 1 + .../ChatListFilterPresetListController.swift | 17 +- .../PremiumUI/Sources/PremiumDemoScreen.swift | 28 ++- .../PremiumUI/Sources/PremiumGiftScreen.swift | 1 + .../Sources/PremiumIntroScreen.swift | 235 ++++++++++-------- .../Sources/SharedAccountContext.swift | 4 + 7 files changed, 182 insertions(+), 109 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 42e90545a5..2af89cfaec 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11316,6 +11316,11 @@ Sorry for the inconvenience."; "ChannelBoost.Header.Giveaway" = "giveaway"; "ChannelBoost.Header.Features" = "features"; +"Premium.FolderTags" = "Folder Tags"; +"Premium.FolderTagsInfo" = "Add colorful labels to chats for faster access with Telegram Premium."; +"Premium.FolderTagsStandaloneInfo" = "Add colorful labels to chats for faster access with Telegram Premium."; +"Premium.FolderTags.Proceed" = "About Telegram Premium"; + "Premium.Business" = "Telegram Business"; "Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies."; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index a23c4a9523..c327bc5597 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -38,6 +38,7 @@ public enum PremiumIntroSource { case presence case readTime case messageTags + case folderTags } public enum PremiumGiftSource: Equatable { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 3b496c6b11..8ed12be0da 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -558,14 +558,15 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch }, updateDisplayTags: { value in context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value) }, updateDisplayTagsLocked: { - //TODO:localize - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_reorder", scale: 0.05, colors: [:], title: nil, text: "Subscribe to **Telegram Premium** to show folder tags.", customUndoText: presentationData.strings.ChatListFolderSettings_SubscribeToMoveAllAction, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in - if case .undo = action { - pushControllerImpl?(PremiumIntroScreen(context: context, source: .folders)) - } - return false }) - ) + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + pushControllerImpl?(controller) }) let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 5504cdc4d8..5957a9920f 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1004,7 +1004,7 @@ private final class DemoSheetContent: CombinedComponent { position: .top, model: .island, videoFile: configuration.videos["last_seen"], - decoration: .tag + decoration: .badgeStars )), title: strings.Premium_LastSeen, text: strings.Premium_LastSeenInfo, @@ -1023,7 +1023,7 @@ private final class DemoSheetContent: CombinedComponent { position: .top, model: .island, videoFile: configuration.videos["message_privacy"], - decoration: .tag + decoration: .swirlStars )), title: strings.Premium_MessagePrivacy, text: strings.Premium_MessagePrivacyInfo, @@ -1033,6 +1033,26 @@ private final class DemoSheetContent: CombinedComponent { ) ) + availableItems[.folderTags] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.folderTags, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + model: .island, + videoFile: configuration.videos["folder_tags"], + decoration: .tag + )), + title: strings.Premium_FolderTags, + text: strings.Premium_FolderTagsStandaloneInfo, + textColor: textColor + ) + ) + ) + ) + let index: Int = 0 var items: [DemoPagerComponent.Item] = [] if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) { @@ -1136,6 +1156,8 @@ private final class DemoSheetContent: CombinedComponent { buttonText = strings.Premium_LastSeen_Proceed case .messagePrivacy: buttonText = strings.Premium_MessagePrivacy_Proceed + case .folderTags: + buttonText = strings.Premium_FolderTags_Proceed default: buttonText = strings.Common_OK } @@ -1177,6 +1199,8 @@ private final class DemoSheetContent: CombinedComponent { text = strings.Premium_LastSeenInfo case .messagePrivacy: text = strings.Premium_MessagePrivacyInfo + case .folderTags: + text = strings.Premium_FolderTagsStandaloneInfo default: text = "" } diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index d2afc9fc9c..2e81b49ed3 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -435,6 +435,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), + UIColor(rgb: 0x676bff), //replace UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 21d6f91c16..11e56087a1 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -282,6 +282,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .folderTags: + if case .folderTags = rhs { + return true + } else { + return false + } } } @@ -326,6 +332,7 @@ public enum PremiumSource: Equatable { case presence case readTime case messageTags + case folderTags var identifier: String? { switch self { @@ -413,6 +420,8 @@ public enum PremiumSource: Equatable { return "read_time" case .messageTags: return "saved_tags" + case .folderTags: + return "folder_tags" } } } @@ -448,7 +457,6 @@ public enum PremiumPerk: CaseIterable { case businessAwayMessage case businessChatBots - public static var allCases: [PremiumPerk] { return [ .doubleLimits, @@ -471,12 +479,28 @@ public enum PremiumPerk: CaseIterable { .messageTags, .lastSeen, .messagePrivacy, + .folderTags, .business ] } - init?(identifier: String) { - for perk in PremiumPerk.allCases { + public static var allBusinessCases: [PremiumPerk] { + return [ + .businessLocation, + .businessHours, + .businessGreetingMessage, + .businessQuickReplies, + .businessAwayMessage, + .businessChatBots, +// .emojiStatus, +// .folderTags, +// .stories, + ] + } + + + init?(identifier: String, business: Bool) { + for perk in business ? PremiumPerk.allBusinessCases : PremiumPerk.allCases { if perk.identifier == identifier { self = perk return @@ -527,10 +551,22 @@ public enum PremiumPerk: CaseIterable { return "last_seen" case .messagePrivacy: return "message_privacy" + case .folderTags: + return "folder_tags" case .business: return "business" - default: - return "" + case .businessLocation: + return "location" + case .businessHours: + return "opening_hours" + case .businessQuickReplies: + return "quick_replies" + case .businessGreetingMessage: + return "greeting_messages" + case .businessAwayMessage: + return "away_messages" + case .businessChatBots: + return "chatbots" } } @@ -576,10 +612,23 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_LastSeen case .messagePrivacy: return strings.Premium_MessagePrivacy + case .folderTags: + return strings.Premium_FolderTags case .business: return strings.Premium_Business - default: - return "" + + case .businessLocation: + return strings.Business_Location + case .businessHours: + return strings.Business_OpeningHours + case .businessQuickReplies: + return strings.Business_QuickReplies + case .businessGreetingMessage: + return strings.Business_GreetingMessages + case .businessAwayMessage: + return strings.Business_AwayMessages + case .businessChatBots: + return strings.Business_Chatbots } } @@ -625,10 +674,23 @@ public enum PremiumPerk: CaseIterable { return strings.Premium_LastSeenInfo case .messagePrivacy: return strings.Premium_MessagePrivacyInfo + case .folderTags: + return strings.Premium_FolderTagsInfo case .business: return strings.Premium_BusinessInfo - default: - return "" + + case .businessLocation: + return strings.Business_LocationInfo + case .businessHours: + return strings.Business_OpeningHoursInfo + case .businessQuickReplies: + return strings.Business_QuickRepliesInfo + case .businessGreetingMessage: + return strings.Business_GreetingMessagesInfo + case .businessAwayMessage: + return strings.Business_AwayMessagesInfo + case .businessChatBots: + return strings.Business_ChatbotsInfo } } @@ -674,86 +736,22 @@ public enum PremiumPerk: CaseIterable { return "Premium/Perk/LastSeen" case .messagePrivacy: return "Premium/Perk/MessagePrivacy" + case .folderTags: + return "Premium/Perk/MessageTags" case .business: return "Premium/Perk/Business" - default: - return "" - } - } -} - -private enum BusinessPerk: CaseIterable { - case location - case hours - case quickReplies - case greetings - case awayMessages - case chatbots - - var identifier: String { - switch self { - case .location: - return "location" - case .hours: - return "opening_hours" - case .quickReplies: - return "quick_replies" - case .greetings: - return "greeting_messages" - case .awayMessages: - return "away_messages" - case .chatbots: - return "chatbots" - } - } - - func title(strings: PresentationStrings) -> String { - switch self { - case .location: - return strings.Business_Location - case .hours: - return strings.Business_OpeningHours - case .quickReplies: - return strings.Business_QuickReplies - case .greetings: - return strings.Business_GreetingMessages - case .awayMessages: - return strings.Business_AwayMessages - case .chatbots: - return strings.Business_Chatbots - } - } - - func subtitle(strings: PresentationStrings) -> String { - switch self { - case .location: - return strings.Business_LocationInfo - case .hours: - return strings.Business_OpeningHoursInfo - case .quickReplies: - return strings.Business_QuickRepliesInfo - case .greetings: - return strings.Business_GreetingMessagesInfo - case .awayMessages: - return strings.Business_AwayMessagesInfo - case .chatbots: - return strings.Business_ChatbotsInfo - } - } - - var iconName: String { - switch self { - case .location: + + case .businessLocation: return "Premium/BusinessPerk/Location" - case .hours: + case .businessHours: return "Premium/BusinessPerk/Hours" - case .quickReplies: + case .businessQuickReplies: return "Premium/BusinessPerk/Replies" - case .greetings: + case .businessGreetingMessage: return "Premium/BusinessPerk/Greetings" - case .awayMessages: + case .businessAwayMessage: return "Premium/BusinessPerk/Away" - case .chatbots: + case .businessChatBots: return "Premium/BusinessPerk/Chatbots" } } @@ -783,20 +781,32 @@ struct PremiumIntroConfiguration { .animatedUserpics, .premiumStickers, .business + ], businessPerks: [ + .businessGreetingMessage, + .businessAwayMessage, + .businessQuickReplies, + .businessChatBots, + .businessHours, + .businessLocation +// .emojiStatus, +// .folderTags, +// .stories ]) } let perks: [PremiumPerk] + let businessPerks: [PremiumPerk] - fileprivate init(perks: [PremiumPerk]) { + fileprivate init(perks: [PremiumPerk], businessPerks: [PremiumPerk]) { self.perks = perks + self.businessPerks = businessPerks } public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration { if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] { var perks: [PremiumPerk] = [] for value in values { - if let perk = PremiumPerk(identifier: value) { + if let perk = PremiumPerk(identifier: value, business: false) { if !perks.contains(perk) { perks.append(perk) } else { @@ -825,7 +835,29 @@ struct PremiumIntroConfiguration { perks.append(.business) } #endif - return PremiumIntroConfiguration(perks: perks) + + + var businessPerks: [PremiumPerk] = [] + if let values = data["business_promo_order"] as? [String] { + for value in values { + if let perk = PremiumPerk(identifier: value, business: true) { + if !businessPerks.contains(perk) { + businessPerks.append(perk) + } else { + businessPerks = [] + break + } + } else { + businessPerks = [] + break + } + } + } + if businessPerks.count < 4 { + businessPerks = PremiumIntroConfiguration.defaultValue.businessPerks + } + + return PremiumIntroConfiguration(perks: perks, businessPerks: businessPerks) } else { return .defaultValue } @@ -1798,6 +1830,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0x9b4fed), UIColor(rgb: 0x8958ff), UIColor(rgb: 0x676bff), + UIColor(rgb: 0x676bff), //replace UIColor(rgb: 0x6172ff), UIColor(rgb: 0x5b79ff), UIColor(rgb: 0x4492ff), @@ -2109,7 +2142,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var i = 0 var perksItems: [AnyComponentWithIdentity] = [] - for perk in BusinessPerk.allCases { + for perk in state.configuration.businessPerks { perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ @@ -2140,7 +2173,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let isPremium = state?.isPremium == true if isPremium { switch perk { - case .location: + case .businessLocation: let _ = (accountContext.engine.data.get( TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId) ) @@ -2150,7 +2183,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in })) }) - case .hours: + case .businessHours: let _ = (accountContext.engine.data.get( TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId) ) @@ -2160,7 +2193,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in })) }) - case .quickReplies: + case .businessQuickReplies: let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext) |> take(1) |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in @@ -2169,7 +2202,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData)) }) - case .greetings: + case .businessGreetingMessage: let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) |> take(1) |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in @@ -2178,7 +2211,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false)) }) - case .awayMessages: + case .businessAwayMessage: let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext) |> take(1) |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in @@ -2187,7 +2220,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true)) }) - case .chatbots: + case .businessChatBots: let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext) |> take(1) |> deliverOnMainQueue).start(next: { [weak accountContext] initialData in @@ -2196,25 +2229,29 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData)) }) + default: + fatalError() } } else { var demoSubject: PremiumDemoScreen.Subject switch perk { - case .location: + case .businessLocation: demoSubject = .businessLocation - case .hours: + case .businessHours: demoSubject = .businessHours - case .quickReplies: + case .businessQuickReplies: demoSubject = .businessQuickReplies - case .greetings: + case .businessGreetingMessage: demoSubject = .businessGreetingMessage - case .awayMessages: + case .businessAwayMessage: demoSubject = .businessAwayMessage - case .chatbots: + case .businessChatBots: demoSubject = .businessChatBots + default: + fatalError() } var dismissImpl: (() -> Void)? - let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: [.businessLocation, .businessHours, .businessQuickReplies, .businessGreetingMessage, .businessAwayMessage, .businessChatBots], buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) + let controller = PremiumLimitsListScreen(context: accountContext, subject: demoSubject, source: .intro(state?.price), order: state?.configuration.businessPerks, buttonText: isPremium ? strings.Common_OK : (state?.isAnnual == true ? strings.Premium_SubscribeForAnnual(state?.price ?? "—").string : strings.Premium_SubscribeFor(state?.price ?? "–").string), isPremium: isPremium, forceDark: forceDark) controller.action = { [weak state] in dismissImpl?() if state?.isPremium == false { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 72ec09c895..f454f44991 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2000,6 +2000,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .readTime case .messageTags: mappedSource = .messageTags + case .folderTags: + mappedSource = .folderTags } let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark) controller.wasDismissed = dismissed @@ -2049,6 +2051,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .lastSeen case .messagePrivacy: mappedSubject = .messagePrivacy + case .folderTags: + mappedSubject = .folderTags default: mappedSubject = .doubleLimits } From 64eaaae4fd429931fae21cbb290e238fbc0a39ba Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Feb 2024 01:36:56 +0400 Subject: [PATCH 3/6] Various improvements --- .../Sources/DrawingEntitiesView.swift | 2 +- .../Stickers/TelegramEngineStickers.swift | 6 + .../MetalResources/EditorDual.metal | 22 ++- .../Sources/ImageObjectSeparation.swift | 64 +++++++++ .../MediaEditor/Sources/MediaEditor.swift | 46 +++++++ .../Sources/MediaEditorRenderer.swift | 3 +- .../Sources/UniversalTextureSource.swift | 7 + .../MediaEditor/Sources/VideoFinishPass.swift | 55 ++++++-- .../Components/MediaEditorScreen/BUILD | 1 + .../Sources/MediaCutoutScreen.swift | 95 +++++++++++-- .../Sources/MediaEditorScreen.swift | 97 +++++++++----- .../CutoutUndo.imageset/Contents.json | 12 ++ .../CutoutUndo.imageset/undo2_30.pdf | 126 ++++++++++++++++++ 13 files changed, 474 insertions(+), 62 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 5d6719096e..34253dde77 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -885,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) { self.selectEntity(viewToSelect.entity, animate: false) self.onInteractionUpdated(true) - } else if gestureRecognizer.numberOfTouches == 2 || self.isStickerEditor, let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { + } else if gestureRecognizer.numberOfTouches == 2 || (self.isStickerEditor && self.autoSelectEntities), let mediaEntityView = self.subviews.first(where: { $0 is DrawingEntityMediaView }) as? DrawingEntityMediaView { mediaEntityView.handlePan(gestureRecognizer) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 8e607e3dce..acc8702807 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -257,6 +257,12 @@ public extension TelegramEngine { return (items.map(\.file), isFinalResult) } } + + public func addRecentlyUsedSticker(file: TelegramMediaFile) { + let _ = self.account.postbox.transaction({ transaction -> Void in + TelegramCore.addRecentlyUsedSticker(transaction: transaction, fileReference: .standalone(media: file)) + }).start() + } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal index c45b5ec9ff..a4fa037c62 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorDual.metal @@ -3,6 +3,14 @@ using namespace metal; +typedef struct { + float2 dimensions; + float roundness; + float alpha; + float isOpaque; + float empty; +} VideoEncodeParameters; + typedef struct { float4 pos; float2 texCoord; @@ -17,11 +25,10 @@ float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius) fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], texture2d texture [[texture(0)]], - constant uint2 &resolution[[buffer(0)]], - constant float &roundness[[buffer(1)]], - constant float &alpha[[buffer(2)]] + texture2d mask [[texture(1)]], + constant VideoEncodeParameters& adjustments [[buffer(0)]] ) { - float2 R = float2(resolution.x, resolution.y); + float2 R = float2(adjustments.dimensions.x, adjustments.dimensions.y); float2 uv = (in.localPos - float2(0.5, 0.5)) * 2.0; if (R.x > R.y) { @@ -33,10 +40,11 @@ fragment half4 dualFragmentShader(RasterizerData in [[stage_in]], constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear); half3 color = texture.sample(samplr, in.texCoord).rgb; + float colorAlpha = min(1.0, adjustments.isOpaque + mask.sample(samplr, in.texCoord).r); - float t = 1.0 / resolution.y; + float t = 1.0 / adjustments.dimensions.y; float side = 1.0 * aspectRatio; - float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, roundness)), side * roundness)); + float distance = smoothstep(t, -t, sdfRoundedRectangle(uv, float2(0.0, 0.0), float2(side, mix(1.0, side, adjustments.roundness)), side * adjustments.roundness)); - return mix(half4(color, 0.0), half4(color, 1.0 * alpha), distance); + return mix(half4(color, 0.0), half4(color, colorAlpha * adjustments.alpha), distance); } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift index 9ecd12429b..fe2f1100f0 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageObjectSeparation.swift @@ -55,6 +55,70 @@ public func cutoutStickerImage(from image: UIImage, onlyCheck: Bool = false) -> } } +public enum CutoutResult { + case image(UIImage) + case pixelBuffer(CVPixelBuffer) +} + +public func cutoutImage(from image: UIImage, atPoint point: CGPoint?, asImage: Bool) -> Signal { + if #available(iOS 17.0, *) { + guard let cgImage = image.cgImage else { + return .single(nil) + } + return Signal { subscriber in + let ciContext = CIContext(options: nil) + let inputImage = CIImage(cgImage: cgImage) + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + let request = VNGenerateForegroundInstanceMaskRequest { [weak handler] request, error in + guard let handler, let result = request.results?.first as? VNInstanceMaskObservation else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + + let instances = IndexSet(instances(atPoint: point, inObservation: result).prefix(1)) + if let mask = try? result.generateScaledMaskForImage(forInstances: instances, from: handler) { + if asImage { + let filter = CIFilter.blendWithMask() + filter.inputImage = inputImage + filter.backgroundImage = CIImage(color: .clear) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } + } else { + let filter = CIFilter.blendWithMask() + filter.inputImage = CIImage(color: .white) + filter.backgroundImage = CIImage(color: .black) + filter.maskImage = CIImage(cvPixelBuffer: mask) + if let output = filter.outputImage, let cgImage = ciContext.createCGImage(output, from: inputImage.extent) { + let image = UIImage(cgImage: cgImage) + subscriber.putNext(.image(image)) + subscriber.putCompletion() + return + } +// subscriber.putNext(.pixelBuffer(mask)) +// subscriber.putCompletion() + } + } + subscriber.putNext(nil) + subscriber.putCompletion() + } + + try? handler.perform([request]) + return ActionDisposable { + request.cancel() + } + } + |> runOn(queue) + } else { + return .single(nil) + } +} + @available(iOS 17.0, *) private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet { guard let point = maybePoint else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index fe03b681b0..183b2405e3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -190,6 +190,7 @@ public final class MediaEditor { public private(set) var canCutout: Bool = false public var canCutoutUpdated: (Bool) -> Void = { _ in } + public var isCutoutUpdated: (Bool) -> Void = { _ in } private var textureCache: CVMetalTextureCache! @@ -1682,6 +1683,51 @@ public final class MediaEditor { self.renderer.renderFrame() } + public func getSeparatedImage(point: CGPoint?) -> Signal { + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return .single(nil) + } + return cutoutImage(from: image, atPoint: point, asImage: true) + |> map { result in + if let result, case let .image(image) = result { + return image + } else { + return nil + } + } + } + + public func removeSeparationMask() { + self.isCutoutUpdated(false) + + self.renderer.currentMainInputMask = nil + if !self.skipRendering { + self.updateRenderChain() + } + } + + public func setSeparationMask(point: CGPoint?) { + guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice else { + return + } + guard let textureSource = self.renderer.textureSource as? UniversalTextureSource, let image = textureSource.mainImage else { + return + } + self.isCutoutUpdated(true) + + let _ = (cutoutImage(from: image, atPoint: point, asImage: false) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let result, case let .image(image) = result else { + return + } + //TODO:replace with pixelbuffer + self.renderer.currentMainInputMask = loadTexture(image: image, device: device) + if !self.skipRendering { + self.updateRenderChain() + } + }) + } + private func maybeGeneratePersonSegmentation(_ image: UIImage?) { if #available(iOS 15.0, *), let cgImage = image?.cgImage { let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 0242d8dc45..c4bddd4725 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -98,6 +98,7 @@ final class MediaEditorRenderer { } private var currentMainInput: Input? + var currentMainInputMask: MTLTexture? private var currentAdditionalInput: Input? private(set) var resultTexture: MTLTexture? @@ -202,7 +203,7 @@ final class MediaEditorRenderer { } if let mainTexture { - return self.videoFinishPass.process(input: mainTexture, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) + return self.videoFinishPass.process(input: mainTexture, inputMask: self.currentMainInputMask, secondInput: additionalTexture, timestamp: mainInput.timestamp, device: device, commandBuffer: commandBuffer) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift index 71f84cdfa1..4e7a97f844 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/UniversalTextureSource.swift @@ -42,6 +42,13 @@ final class UniversalTextureSource: TextureSource { ) } + var mainImage: UIImage? { + if let mainInput = self.mainInputContext?.input, case let .image(image) = mainInput { + return image + } + return nil + } + func setMainInput(_ input: Input) { guard let renderTarget = self.renderTarget else { return diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift index bd96115295..d6e3fcbe43 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoFinishPass.swift @@ -144,6 +144,14 @@ private var transitionDuration = 0.5 private var apperanceDuration = 0.2 private var videoRemovalDuration: Double = 0.2 +struct VideoEncodeParameters { + var dimensions: simd_float2 + var roundness: simd_float1 + var alpha: simd_float1 + var isOpaque: simd_float1 + var empty: simd_float1 +} + final class VideoFinishPass: RenderPass { private var cachedTexture: MTLTexture? @@ -195,6 +203,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, texture: MTLTexture, textureRotation: TextureRotation, + maskTexture: MTLTexture?, position: VideoPosition, roundness: Float, alpha: Float, @@ -202,6 +211,11 @@ final class VideoFinishPass: RenderPass { device: MTLDevice ) { encoder.setFragmentTexture(texture, index: 0) + if let maskTexture { + encoder.setFragmentTexture(maskTexture, index: 1) + } else { + encoder.setFragmentTexture(texture, index: 1) + } let center = CGPoint( x: position.position.x - containerSize.width / 2.0, @@ -220,14 +234,25 @@ final class VideoFinishPass: RenderPass { options: []) encoder.setVertexBuffer(buffer, offset: 0, index: 0) - var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) - encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - - var roundness = roundness - encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) - - var alpha = alpha - encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) + var parameters = VideoEncodeParameters( + dimensions: simd_float2(Float(size.width), Float(size.height)), + roundness: roundness, + alpha: alpha, + isOpaque: maskTexture == nil ? 1.0 : 0.0, + empty: 0 + ) + encoder.setFragmentBytes(¶meters, length: MemoryLayout.size, index: 0) +// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height)) +// encoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) +// +// var roundness = roundness +// encoder.setFragmentBytes(&roundness, length: MemoryLayout.size, index: 1) +// +// var alpha = alpha +// encoder.setFragmentBytes(&alpha, length: MemoryLayout.size, index: 2) +// +// var isOpaque = maskTexture == nil ? 1.0 : 0.0 +// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout.size, index: 3) encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) } @@ -478,7 +503,14 @@ final class VideoFinishPass: RenderPass { return (backgroundVideoState, foregroundVideoState, disappearingVideoState) } - func process(input: MTLTexture, secondInput: MTLTexture?, timestamp: CMTime, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process( + input: MTLTexture, + inputMask: MTLTexture?, + secondInput: MTLTexture?, + timestamp: CMTime, + device: MTLDevice, + commandBuffer: MTLCommandBuffer + ) -> MTLTexture? { if !self.isStory { return input } @@ -536,7 +568,6 @@ final class VideoFinishPass: RenderPass { ) if self.gradientColors.topColor.w > 0.0 { - renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!) self.encodeGradient( using: renderCommandEncoder, containerSize: containerSize, @@ -554,6 +585,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: transitionVideoState.texture, textureRotation: transitionVideoState.textureRotation, + maskTexture: nil, position: transitionVideoState.position, roundness: transitionVideoState.roundness, alpha: transitionVideoState.alpha, @@ -567,6 +599,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: mainVideoState.texture, textureRotation: mainVideoState.textureRotation, + maskTexture: inputMask, position: mainVideoState.position, roundness: mainVideoState.roundness, alpha: mainVideoState.alpha, @@ -580,6 +613,7 @@ final class VideoFinishPass: RenderPass { containerSize: containerSize, texture: additionalVideoState.texture, textureRotation: additionalVideoState.textureRotation, + maskTexture: nil, position: additionalVideoState.position, roundness: additionalVideoState.roundness, alpha: additionalVideoState.alpha, @@ -603,6 +637,7 @@ final class VideoFinishPass: RenderPass { containerSize: CGSize, device: MTLDevice ) { + encoder.setRenderPipelineState(self.gradientPipelineState!) let vertices = verticesDataForRotation(.rotate0Degrees) let buffer = device.makeBuffer( diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 728aedc52c..88ab5ba5ef 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", "//submodules/TelegramUI/Components/MediaScrubberComponent", "//submodules/Components/BlurredBackgroundComponent", + "//submodules/TelegramUI/Components/DustEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift index 2e411f9cf7..e1b3c046cc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaCutoutScreen.swift @@ -15,6 +15,7 @@ import MediaEditor import Photos import LottieAnimationComponent import MessageInputPanelComponent +import DustEffect private final class MediaCutoutScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -40,9 +41,13 @@ private final class MediaCutoutScreenComponent: Component { public final class View: UIView { private let buttonsContainerView = UIView() private let buttonsBackgroundView = UIView() + private let previewContainerView = UIView() private let cancelButton = ComponentView() private let label = ComponentView() private let doneButton = ComponentView() + + private let fadeView = UIView() + private let separatedImageView = UIImageView() private var component: MediaCutoutScreenComponent? private weak var state: State? @@ -51,18 +56,44 @@ private final class MediaCutoutScreenComponent: Component { override init(frame: CGRect) { self.buttonsContainerView.clipsToBounds = true + self.fadeView.alpha = 0.0 + self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.6) + + self.separatedImageView.contentMode = .scaleAspectFit + super.init(frame: frame) self.backgroundColor = .clear self.addSubview(self.buttonsContainerView) self.buttonsContainerView.addSubview(self.buttonsBackgroundView) + + self.addSubview(self.fadeView) + self.addSubview(self.separatedImageView) + self.addSubview(self.previewContainerView) + + self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:)))) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + @objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } + let location = gestureRecognizer.location(in: gestureRecognizer.view) + + let point = CGPoint( + x: location.x / self.previewContainerView.frame.width, + y: location.y / self.previewContainerView.frame.height + ) + component.mediaEditor.setSeparationMask(point: point) + + self.playDissolveAnimation() + } + func animateInFromEditor() { self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -74,6 +105,7 @@ private final class MediaCutoutScreenComponent: Component { self.cancelButton.view?.isHidden = true + self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in completion() }) @@ -82,14 +114,35 @@ private final class MediaCutoutScreenComponent: Component { self.state?.updated() } + public func playDissolveAnimation() { + guard let component = self.component, let resultImage = component.mediaEditor.resultImage, let environment = self.environment, let controller = environment.controller() as? MediaCutoutScreen else { + return + } + let previewView = controller.previewView + + let dustEffectLayer = DustEffectLayer() + dustEffectLayer.position = previewView.center + dustEffectLayer.bounds = previewView.bounds + previewView.superview?.layer.insertSublayer(dustEffectLayer, below: previewView.layer) + + dustEffectLayer.animationSpeed = 2.2 + dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in + dustEffectLayer?.removeFromSuperlayer() + } + + dustEffectLayer.addItem(frame: previewView.bounds, image: resultImage) + + controller.requestDismiss(animated: true) + } + func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: Transition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value self.environment = environment + let isFirstTime = self.component == nil self.component = component self.state = state -// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let isTablet: Bool if case .regular = environment.metrics.widthClass { isTablet = true @@ -97,8 +150,6 @@ private final class MediaCutoutScreenComponent: Component { isTablet = false } -// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor - let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 var controlsBottomInset: CGFloat = 0.0 @@ -119,7 +170,7 @@ private final class MediaCutoutScreenComponent: Component { } } -// var previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset)) let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset)) let cancelButtonSize = self.cancelButton.update( @@ -140,7 +191,7 @@ private final class MediaCutoutScreenComponent: Component { guard let controller = environment.controller() as? MediaCutoutScreen else { return } - controller.requestDismiss(reset: true, animated: true) + controller.requestDismiss(animated: true) } )), environment: {}, @@ -177,6 +228,30 @@ private final class MediaCutoutScreenComponent: Component { transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame) transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size)) + transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame) + transition.setFrame(view: self.separatedImageView, frame: previewContainerFrame) + + let frameWidth = floor(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 _ = (component.mediaEditor.getSeparatedImage(point: nil) + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let self else { + return + } + self.separatedImageView.image = image + self.state?.updated(transition: .easeInOut(duration: 0.2)) + }) + } else { + if let _ = self.separatedImageView.image { + transition.setAlpha(view: self.fadeView, alpha: 1.0) + } else { + transition.setAlpha(view: self.fadeView, alpha: 0.0) + } + } return availableSize } } @@ -315,14 +390,16 @@ public final class MediaCutoutScreen: ViewController { fileprivate let context: AccountContext fileprivate let mediaEditor: MediaEditor + fileprivate let previewView: MediaEditorPreviewView public var dismissed: () -> Void = {} private var initialValues: MediaEditorValues - public init(context: AccountContext, mediaEditor: MediaEditor) { + public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) { self.context = context self.mediaEditor = mediaEditor + self.previewView = previewView self.initialValues = mediaEditor.values.makeCopy() super.init(navigationBarPresentationData: nil) @@ -343,11 +420,7 @@ public final class MediaCutoutScreen: ViewController { super.displayNodeDidLoad() } - func requestDismiss(reset: Bool, animated: Bool) { - if reset { - self.mediaEditor.values = self.initialValues - } - + func requestDismiss(animated: Bool) { self.dismissed() self.node.animateOutToEditor(completion: { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 2f078dad8e..abae6ccac6 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -154,6 +154,7 @@ final class MediaEditorScreenComponent: Component { case tools case done case cutout + case undo } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -172,6 +173,8 @@ final class MediaEditorScreenComponent: Component { image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)! case .cutout: image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)! + case .undo: + image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)! case .done: image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -981,13 +984,14 @@ 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(.cutout), - title: "Cut Out an Object" + icon: state.image(isCutout ? .undo : .cutout), + title: isCutout ? "Undo Cut Out" : "Cut Out an Object" )), effectAlignment: .center, action: { @@ -2161,6 +2165,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissBySwipeSuppressed = false fileprivate var canCutout = false + fileprivate var isCutout = false private (set) var hasAnyChanges = false @@ -2513,7 +2518,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.canCutout = canCutout controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } - + mediaEditor.isCutoutUpdated = { [weak self] isCutout in + guard let self else { + return + } + self.isCutout = isCutout + self.requestLayout(forceUpdate: true, transition: .immediate) + } if case .message = effectiveSubject { } else { @@ -4231,14 +4242,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.entitiesView.selectEntity(nil) } - let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor) - controller.dismissed = { [weak self] in - if let self { - self.animateInFromTool(inPlace: true) + if controller.node.isCutout { + let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false) + if let snapshotView { + self.previewView.superview?.addSubview(snapshotView) } + self.previewView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { _ in + snapshotView?.removeFromSuperview() + }) + mediaEditor.removeSeparationMask() + } else { + let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor, previewView: self.previewView) + controller.dismissed = { [weak self] in + if let self { + self.animateInFromTool(inPlace: true) + } + } + self.controller?.present(controller, in: .window(.root)) + self.animateOutToTool(inPlace: true) } - self.controller?.present(controller, in: .window(.root)) - self.animateOutToTool(inPlace: true) } } ) @@ -5084,35 +5106,46 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - let title: String - let save: String - if case .draft = self.node.actualSubject { - title = presentationData.strings.Story_Editor_DraftDiscardDraft - save = presentationData.strings.Story_Editor_DraftKeepDraft - } else { + var title: String + var text: String + var save: String? + switch self.mode { + case .storyEditor: + if case .draft = self.node.actualSubject { + title = presentationData.strings.Story_Editor_DraftDiscardDraft + save = presentationData.strings.Story_Editor_DraftKeepDraft + } else { + title = presentationData.strings.Story_Editor_DraftDiscardMedia + save = presentationData.strings.Story_Editor_DraftKeepMedia + } + text = presentationData.strings.Story_Editor_DraftDiscaedText + case .stickerEditor: title = presentationData.strings.Story_Editor_DraftDiscardMedia - save = presentationData.strings.Story_Editor_DraftKeepMedia + text = presentationData.strings.Story_Editor_DiscardText } + + var actions: [TextAlertAction] = [] + actions.append(TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: false, animated: true) + } + })) + if let save { + actions.append(TextAlertAction(type: .genericAction, title: save, action: { [weak self] in + if let self { + self.requestDismiss(saveDraft: true, animated: true) + } + })) + } + actions.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + + })) let controller = textAlertController( context: self.context, forceTheme: defaultDarkPresentationTheme, title: title, - text: presentationData.strings.Story_Editor_DraftDiscaedText, - actions: [ - TextAlertAction(type: .destructiveAction, title: presentationData.strings.Story_Editor_DraftDiscard, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: false, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: save, action: { [weak self] in - if let self { - self.requestDismiss(saveDraft: true, animated: true) - } - }), - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - - }) - ], + text: text, + actions: actions, actionLayout: .vertical ) self.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json new file mode 100644 index 0000000000..4a8e6450d0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "undo2_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf new file mode 100644 index 0000000000..e814c8183d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf @@ -0,0 +1,126 @@ +%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 5.000000 4.258789 cm +0.000000 0.000000 0.000000 scn +1.000000 2.571211 m +0.541604 2.571211 0.170000 2.199607 0.170000 1.741211 c +0.170000 1.282815 0.541604 0.911211 1.000000 0.911211 c +1.000000 2.571211 l +h +1.000000 13.571211 m +0.541604 13.571211 0.170000 13.199608 0.170000 12.741211 c +0.170000 12.282814 0.541604 11.911211 1.000000 11.911211 c +1.000000 13.571211 l +h +3.413101 8.154312 m +3.737236 7.830177 4.262764 7.830177 4.586899 8.154312 c +4.911034 8.478448 4.911034 9.003975 4.586899 9.328110 c +3.413101 8.154312 l +h +0.000000 12.741211 m +-0.586899 13.328110 l +-0.911034 13.003975 -0.911034 12.478447 -0.586899 12.154312 c +0.000000 12.741211 l +h +4.586899 16.154312 m +4.911034 16.478447 4.911034 17.003975 4.586899 17.328110 c +4.262764 17.652245 3.737236 17.652245 3.413101 17.328110 c +4.586899 16.154312 l +h +1.000000 0.911211 m +8.500000 0.911211 l +8.500000 2.571211 l +1.000000 2.571211 l +1.000000 0.911211 l +h +8.500000 13.571211 m +1.000000 13.571211 l +1.000000 11.911211 l +8.500000 11.911211 l +8.500000 13.571211 l +h +4.586899 9.328110 m +0.586899 13.328110 l +-0.586899 12.154312 l +3.413101 8.154312 l +4.586899 9.328110 l +h +0.586899 12.154312 m +4.586899 16.154312 l +3.413101 17.328110 l +-0.586899 13.328110 l +0.586899 12.154312 l +h +14.830000 7.241211 m +14.830000 10.737173 11.995962 13.571211 8.500000 13.571211 c +8.500000 11.911211 l +11.079169 11.911211 13.170000 9.820381 13.170000 7.241211 c +14.830000 7.241211 l +h +8.500000 0.911211 m +11.995962 0.911211 14.830000 3.745249 14.830000 7.241211 c +13.170000 7.241211 l +13.170000 4.662042 11.079169 2.571211 8.500000 2.571211 c +8.500000 0.911211 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1673 +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 +0000001763 00000 n +0000001786 00000 n +0000001959 00000 n +0000002033 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2092 +%%EOF \ No newline at end of file From 02a2c2d3592d367c03ebb3e637ea1741805ddc7c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Feb 2024 02:31:03 +0400 Subject: [PATCH 4/6] Update API --- submodules/TelegramApi/Sources/Api0.swift | 6 ++--- submodules/TelegramApi/Sources/Api2.swift | 26 +++++++++++-------- submodules/TelegramApi/Sources/Api7.swift | 26 +++++++++++-------- .../Messages/QuickReplyMessages.swift | 4 ++- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index d6ea57e18e..1a147326a5 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -102,7 +102,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-944407322] = { return Api.BotMenuButton.parse_botMenuButton($0) } dict[1113113093] = { return Api.BotMenuButton.parse_botMenuButtonCommands($0) } dict[1966318984] = { return Api.BotMenuButton.parse_botMenuButtonDefault($0) } - dict[467254972] = { return Api.BusinessAwayMessage.parse_businessAwayMessage($0) } + dict[-283809188] = { return Api.BusinessAwayMessage.parse_businessAwayMessage($0) } dict[-910564679] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleAlways($0) } dict[-867328308] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleCustom($0) } dict[-1007487743] = { return Api.BusinessAwayMessageSchedule.parse_businessAwayMessageScheduleOutsideWorkHours($0) } @@ -308,7 +308,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-459324] = { return Api.InputBotInlineResult.parse_inputBotInlineResultDocument($0) } dict[1336154098] = { return Api.InputBotInlineResult.parse_inputBotInlineResultGame($0) } dict[-1462213465] = { return Api.InputBotInlineResult.parse_inputBotInlineResultPhoto($0) } - dict[-307493900] = { return Api.InputBusinessAwayMessage.parse_inputBusinessAwayMessage($0) } + dict[-2094959136] = { return Api.InputBusinessAwayMessage.parse_inputBusinessAwayMessage($0) } dict[26528571] = { return Api.InputBusinessGreetingMessage.parse_inputBusinessGreetingMessage($0) } dict[1871393450] = { return Api.InputBusinessRecipients.parse_inputBusinessRecipients($0) } dict[-212145112] = { return Api.InputChannel.parse_inputChannel($0) } @@ -1315,7 +1315,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/Api2.swift b/submodules/TelegramApi/Sources/Api2.swift index aaea1b49c7..9436ea5239 100644 --- a/submodules/TelegramApi/Sources/Api2.swift +++ b/submodules/TelegramApi/Sources/Api2.swift @@ -590,14 +590,15 @@ public extension Api { } public extension Api { enum BusinessAwayMessage: TypeConstructorDescription { - case businessAwayMessage(shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.BusinessRecipients) + case businessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.BusinessRecipients) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .businessAwayMessage(let shortcutId, let schedule, let recipients): + case .businessAwayMessage(let flags, let shortcutId, let schedule, let recipients): if boxed { - buffer.appendInt32(467254972) + buffer.appendInt32(-283809188) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(shortcutId, buffer: buffer, boxed: false) schedule.serialize(buffer, true) recipients.serialize(buffer, true) @@ -607,27 +608,30 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .businessAwayMessage(let shortcutId, let schedule, let recipients): - return ("businessAwayMessage", [("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) + case .businessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + return ("businessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) } } public static func parse_businessAwayMessage(_ reader: BufferReader) -> BusinessAwayMessage? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.BusinessAwayMessageSchedule? + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule } - var _3: Api.BusinessRecipients? + var _4: Api.BusinessRecipients? if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients + _4 = Api.parse(reader, signature: signature) as? Api.BusinessRecipients } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.BusinessAwayMessage.businessAwayMessage(shortcutId: _1!, schedule: _2!, recipients: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.BusinessAwayMessage.businessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index 03c3e6512f..cbdf0ebf38 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -264,14 +264,15 @@ public extension Api { } public extension Api { enum InputBusinessAwayMessage: TypeConstructorDescription { - case inputBusinessAwayMessage(shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.InputBusinessRecipients) + case inputBusinessAwayMessage(flags: Int32, shortcutId: Int32, schedule: Api.BusinessAwayMessageSchedule, recipients: Api.InputBusinessRecipients) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .inputBusinessAwayMessage(let shortcutId, let schedule, let recipients): + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let recipients): if boxed { - buffer.appendInt32(-307493900) + buffer.appendInt32(-2094959136) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(shortcutId, buffer: buffer, boxed: false) schedule.serialize(buffer, true) recipients.serialize(buffer, true) @@ -281,27 +282,30 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .inputBusinessAwayMessage(let shortcutId, let schedule, let recipients): - return ("inputBusinessAwayMessage", [("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) + case .inputBusinessAwayMessage(let flags, let shortcutId, let schedule, let recipients): + return ("inputBusinessAwayMessage", [("flags", flags as Any), ("shortcutId", shortcutId as Any), ("schedule", schedule as Any), ("recipients", recipients as Any)]) } } public static func parse_inputBusinessAwayMessage(_ reader: BufferReader) -> InputBusinessAwayMessage? { var _1: Int32? _1 = reader.readInt32() - var _2: Api.BusinessAwayMessageSchedule? + var _2: Int32? + _2 = reader.readInt32() + var _3: Api.BusinessAwayMessageSchedule? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule + _3 = Api.parse(reader, signature: signature) as? Api.BusinessAwayMessageSchedule } - var _3: Api.InputBusinessRecipients? + var _4: Api.InputBusinessRecipients? if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients + _4 = Api.parse(reader, signature: signature) as? Api.InputBusinessRecipients } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(shortcutId: _1!, schedule: _2!, recipients: _3!) + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputBusinessAwayMessage.inputBusinessAwayMessage(flags: _1!, shortcutId: _2!, schedule: _3!, recipients: _4!) } else { return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 81d67f73ab..5f4a0f96ea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -590,7 +590,8 @@ public final class TelegramBusinessAwayMessage: Codable, Equatable { extension TelegramBusinessAwayMessage { convenience init(apiAwayMessage: Api.BusinessAwayMessage) { switch apiAwayMessage { - case let .businessAwayMessage(shortcutId, schedule, recipients): + case let .businessAwayMessage(flags, shortcutId, schedule, recipients): + let _ = flags let mappedSchedule: Schedule switch schedule { case .businessAwayMessageScheduleAlways: @@ -730,6 +731,7 @@ func _internal_updateBusinessAwayMessage(account: Account, awayMessage: Telegram } mappedMessage = .inputBusinessAwayMessage( + flags: 0, shortcutId: awayMessage.shortcutId, schedule: mappedSchedule, recipients: awayMessage.recipients.apiInputValue(additionalPeers: additionalPeers) From 840c80546fe562f53471bbca2af7d054ead7ce5f Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Feb 2024 02:31:14 +0400 Subject: [PATCH 5/6] Various fixes --- .../Sources/VideoMessageCameraScreen.swift | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 7f8af9e065..79f6fd787b 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -260,23 +260,25 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4)) controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) { - self.resultDisposable.set((camera.startRecording() - |> deliverOnMainQueue).start(next: { [weak self] recordingData in - let duration = initialDuration + recordingData.duration - if let self, let controller = self.getController() { - controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) - if isFirstRecording { - controller.node.setupLiveUpload(filePath: recordingData.filePath) + Queue.mainQueue().after(0.15) { + self.resultDisposable.set((camera.startRecording() + |> deliverOnMainQueue).start(next: { [weak self] recordingData in + let duration = initialDuration + recordingData.duration + if let self, let controller = self.getController() { + controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1)) + if isFirstRecording { + controller.node.setupLiveUpload(filePath: recordingData.filePath) + } + if duration > 59.5 { + controller.onStop() + } } - if duration > 59.5 { - controller.onStop() + }, error: { [weak self] _ in + if let self, let controller = self.getController() { + controller.completion(nil, nil, nil) } - } - }, error: { [weak self] _ in - if let self, let controller = self.getController() { - controller.completion(nil, nil, nil) - } - })) + })) + } } if initialDuration > 0.0 { From 96207f7d175fe045a7a1c892e4428152ca808394 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 29 Feb 2024 03:05:39 +0400 Subject: [PATCH 6/6] Various fixes --- submodules/TelegramUI/Sources/ChatController.swift | 9 +++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index e40ab50ba2..6eb9e1129f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -6144,6 +6144,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + var appliedBoosts: Int32? + var boostsToUnrestrict: Int32? + if let cachedChannelData = peerView.cachedData as? CachedChannelData { + appliedBoosts = cachedChannelData.appliedBoosts + boostsToUnrestrict = cachedChannelData.boostsToUnrestrict + } + strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, { return $0.updatedPeer { _ in return renderedPeer @@ -6152,6 +6159,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G .updatedHasSearchTags(hasSearchTags) .updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging) .updatedHasSavedChats(hasSavedChats) + .updatedAppliedBoosts(appliedBoosts) + .updatedBoostsToUnrestrict(boostsToUnrestrict) .updatedInterfaceState { interfaceState in var interfaceState = interfaceState diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 169383d6f7..28c66b0361 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -159,7 +159,7 @@ final class WebAppWebView: WKWebView { self.interactiveTransitionGestureRecognizerTest = { point -> Bool in return point.x > 30.0 } - self.allowsBackForwardNavigationGestures = false + self.allowsBackForwardNavigationGestures = true if #available(iOS 16.4, *) { self.isInspectable = true }