mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit '96207f7d175fe045a7a1c892e4428152ca808394'
# Conflicts: # submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift
This commit is contained in:
commit
fd32830411
@ -11316,6 +11316,11 @@ Sorry for the inconvenience.";
|
|||||||
"ChannelBoost.Header.Giveaway" = "giveaway";
|
"ChannelBoost.Header.Giveaway" = "giveaway";
|
||||||
"ChannelBoost.Header.Features" = "features";
|
"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.Business" = "Telegram Business";
|
||||||
"Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies.";
|
"Premium.BusinessInfo" = "Upgrade your account with business features such as location, opening hours and quick replies.";
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ public enum PremiumIntroSource {
|
|||||||
case presence
|
case presence
|
||||||
case readTime
|
case readTime
|
||||||
case messageTags
|
case messageTags
|
||||||
|
case folderTags
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PremiumGiftSource: Equatable {
|
public enum PremiumGiftSource: Equatable {
|
||||||
|
@ -558,14 +558,15 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||||||
}, updateDisplayTags: { value in
|
}, updateDisplayTags: { value in
|
||||||
context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value)
|
context.engine.peers.updateChatListFiltersDisplayTags(isEnabled: value)
|
||||||
}, updateDisplayTagsLocked: {
|
}, updateDisplayTagsLocked: {
|
||||||
//TODO:localize
|
var replaceImpl: ((ViewController) -> Void)?
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .folderTags, action: {
|
||||||
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
|
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folderTags, forceDark: false, dismissed: nil)
|
||||||
if case .undo = action {
|
replaceImpl?(controller)
|
||||||
pushControllerImpl?(PremiumIntroScreen(context: context, source: .folders))
|
})
|
||||||
}
|
replaceImpl = { [weak controller] c in
|
||||||
return false })
|
controller?.replace(with: c)
|
||||||
)
|
}
|
||||||
|
pushControllerImpl?(controller)
|
||||||
})
|
})
|
||||||
|
|
||||||
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|
||||||
|
@ -885,7 +885,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
|
} else if self.autoSelectEntities, gestureRecognizer.numberOfTouches == 1, let viewToSelect = self.entity(at: location) {
|
||||||
self.selectEntity(viewToSelect.entity, animate: false)
|
self.selectEntity(viewToSelect.entity, animate: false)
|
||||||
self.onInteractionUpdated(true)
|
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)
|
mediaEntityView.handlePan(gestureRecognizer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1004,7 +1004,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["last_seen"],
|
videoFile: configuration.videos["last_seen"],
|
||||||
decoration: .tag
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_LastSeen,
|
title: strings.Premium_LastSeen,
|
||||||
text: strings.Premium_LastSeenInfo,
|
text: strings.Premium_LastSeenInfo,
|
||||||
@ -1023,7 +1023,7 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["message_privacy"],
|
videoFile: configuration.videos["message_privacy"],
|
||||||
decoration: .tag
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_MessagePrivacy,
|
title: strings.Premium_MessagePrivacy,
|
||||||
text: strings.Premium_MessagePrivacyInfo,
|
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
|
let index: Int = 0
|
||||||
var items: [DemoPagerComponent.Item] = []
|
var items: [DemoPagerComponent.Item] = []
|
||||||
if let item = availableItems.first(where: { $0.value.content.id == component.subject as AnyHashable }) {
|
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
|
buttonText = strings.Premium_LastSeen_Proceed
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
buttonText = strings.Premium_MessagePrivacy_Proceed
|
buttonText = strings.Premium_MessagePrivacy_Proceed
|
||||||
|
case .folderTags:
|
||||||
|
buttonText = strings.Premium_FolderTags_Proceed
|
||||||
default:
|
default:
|
||||||
buttonText = strings.Common_OK
|
buttonText = strings.Common_OK
|
||||||
}
|
}
|
||||||
@ -1177,6 +1199,8 @@ private final class DemoSheetContent: CombinedComponent {
|
|||||||
text = strings.Premium_LastSeenInfo
|
text = strings.Premium_LastSeenInfo
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
text = strings.Premium_MessagePrivacyInfo
|
text = strings.Premium_MessagePrivacyInfo
|
||||||
|
case .folderTags:
|
||||||
|
text = strings.Premium_FolderTagsStandaloneInfo
|
||||||
default:
|
default:
|
||||||
text = ""
|
text = ""
|
||||||
}
|
}
|
||||||
|
@ -435,6 +435,7 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x9b4fed),
|
UIColor(rgb: 0x9b4fed),
|
||||||
UIColor(rgb: 0x8958ff),
|
UIColor(rgb: 0x8958ff),
|
||||||
UIColor(rgb: 0x676bff),
|
UIColor(rgb: 0x676bff),
|
||||||
|
UIColor(rgb: 0x676bff), //replace
|
||||||
UIColor(rgb: 0x6172ff),
|
UIColor(rgb: 0x6172ff),
|
||||||
UIColor(rgb: 0x5b79ff),
|
UIColor(rgb: 0x5b79ff),
|
||||||
UIColor(rgb: 0x4492ff),
|
UIColor(rgb: 0x4492ff),
|
||||||
|
@ -282,6 +282,12 @@ public enum PremiumSource: Equatable {
|
|||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
case .folderTags:
|
||||||
|
if case .folderTags = rhs {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,6 +332,7 @@ public enum PremiumSource: Equatable {
|
|||||||
case presence
|
case presence
|
||||||
case readTime
|
case readTime
|
||||||
case messageTags
|
case messageTags
|
||||||
|
case folderTags
|
||||||
|
|
||||||
var identifier: String? {
|
var identifier: String? {
|
||||||
switch self {
|
switch self {
|
||||||
@ -413,6 +420,8 @@ public enum PremiumSource: Equatable {
|
|||||||
return "read_time"
|
return "read_time"
|
||||||
case .messageTags:
|
case .messageTags:
|
||||||
return "saved_tags"
|
return "saved_tags"
|
||||||
|
case .folderTags:
|
||||||
|
return "folder_tags"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -448,7 +457,6 @@ public enum PremiumPerk: CaseIterable {
|
|||||||
case businessAwayMessage
|
case businessAwayMessage
|
||||||
case businessChatBots
|
case businessChatBots
|
||||||
|
|
||||||
|
|
||||||
public static var allCases: [PremiumPerk] {
|
public static var allCases: [PremiumPerk] {
|
||||||
return [
|
return [
|
||||||
.doubleLimits,
|
.doubleLimits,
|
||||||
@ -471,12 +479,28 @@ public enum PremiumPerk: CaseIterable {
|
|||||||
.messageTags,
|
.messageTags,
|
||||||
.lastSeen,
|
.lastSeen,
|
||||||
.messagePrivacy,
|
.messagePrivacy,
|
||||||
|
.folderTags,
|
||||||
.business
|
.business
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(identifier: String) {
|
public static var allBusinessCases: [PremiumPerk] {
|
||||||
for perk in PremiumPerk.allCases {
|
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 {
|
if perk.identifier == identifier {
|
||||||
self = perk
|
self = perk
|
||||||
return
|
return
|
||||||
@ -527,10 +551,22 @@ public enum PremiumPerk: CaseIterable {
|
|||||||
return "last_seen"
|
return "last_seen"
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
return "message_privacy"
|
return "message_privacy"
|
||||||
|
case .folderTags:
|
||||||
|
return "folder_tags"
|
||||||
case .business:
|
case .business:
|
||||||
return "business"
|
return "business"
|
||||||
default:
|
case .businessLocation:
|
||||||
return ""
|
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
|
return strings.Premium_LastSeen
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
return strings.Premium_MessagePrivacy
|
return strings.Premium_MessagePrivacy
|
||||||
|
case .folderTags:
|
||||||
|
return strings.Premium_FolderTags
|
||||||
case .business:
|
case .business:
|
||||||
return strings.Premium_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
|
return strings.Premium_LastSeenInfo
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
return strings.Premium_MessagePrivacyInfo
|
return strings.Premium_MessagePrivacyInfo
|
||||||
|
case .folderTags:
|
||||||
|
return strings.Premium_FolderTagsInfo
|
||||||
case .business:
|
case .business:
|
||||||
return strings.Premium_BusinessInfo
|
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"
|
return "Premium/Perk/LastSeen"
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
return "Premium/Perk/MessagePrivacy"
|
return "Premium/Perk/MessagePrivacy"
|
||||||
|
case .folderTags:
|
||||||
|
return "Premium/Perk/MessageTags"
|
||||||
case .business:
|
case .business:
|
||||||
return "Premium/Perk/Business"
|
return "Premium/Perk/Business"
|
||||||
default:
|
|
||||||
return ""
|
case .businessLocation:
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
|
||||||
return "Premium/BusinessPerk/Location"
|
return "Premium/BusinessPerk/Location"
|
||||||
case .hours:
|
case .businessHours:
|
||||||
return "Premium/BusinessPerk/Hours"
|
return "Premium/BusinessPerk/Hours"
|
||||||
case .quickReplies:
|
case .businessQuickReplies:
|
||||||
return "Premium/BusinessPerk/Replies"
|
return "Premium/BusinessPerk/Replies"
|
||||||
case .greetings:
|
case .businessGreetingMessage:
|
||||||
return "Premium/BusinessPerk/Greetings"
|
return "Premium/BusinessPerk/Greetings"
|
||||||
case .awayMessages:
|
case .businessAwayMessage:
|
||||||
return "Premium/BusinessPerk/Away"
|
return "Premium/BusinessPerk/Away"
|
||||||
case .chatbots:
|
case .businessChatBots:
|
||||||
return "Premium/BusinessPerk/Chatbots"
|
return "Premium/BusinessPerk/Chatbots"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -783,20 +781,32 @@ struct PremiumIntroConfiguration {
|
|||||||
.animatedUserpics,
|
.animatedUserpics,
|
||||||
.premiumStickers,
|
.premiumStickers,
|
||||||
.business
|
.business
|
||||||
|
], businessPerks: [
|
||||||
|
.businessGreetingMessage,
|
||||||
|
.businessAwayMessage,
|
||||||
|
.businessQuickReplies,
|
||||||
|
.businessChatBots,
|
||||||
|
.businessHours,
|
||||||
|
.businessLocation
|
||||||
|
// .emojiStatus,
|
||||||
|
// .folderTags,
|
||||||
|
// .stories
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
let perks: [PremiumPerk]
|
let perks: [PremiumPerk]
|
||||||
|
let businessPerks: [PremiumPerk]
|
||||||
|
|
||||||
fileprivate init(perks: [PremiumPerk]) {
|
fileprivate init(perks: [PremiumPerk], businessPerks: [PremiumPerk]) {
|
||||||
self.perks = perks
|
self.perks = perks
|
||||||
|
self.businessPerks = businessPerks
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration {
|
public static func with(appConfiguration: AppConfiguration) -> PremiumIntroConfiguration {
|
||||||
if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] {
|
if let data = appConfiguration.data, let values = data["premium_promo_order"] as? [String] {
|
||||||
var perks: [PremiumPerk] = []
|
var perks: [PremiumPerk] = []
|
||||||
for value in values {
|
for value in values {
|
||||||
if let perk = PremiumPerk(identifier: value) {
|
if let perk = PremiumPerk(identifier: value, business: false) {
|
||||||
if !perks.contains(perk) {
|
if !perks.contains(perk) {
|
||||||
perks.append(perk)
|
perks.append(perk)
|
||||||
} else {
|
} else {
|
||||||
@ -825,7 +835,29 @@ struct PremiumIntroConfiguration {
|
|||||||
perks.append(.business)
|
perks.append(.business)
|
||||||
}
|
}
|
||||||
#endif
|
#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 {
|
} else {
|
||||||
return .defaultValue
|
return .defaultValue
|
||||||
}
|
}
|
||||||
@ -1531,8 +1563,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
ApplicationSpecificNotice.dismissedPremiumColorsBadge(accountManager: context.sharedContext.accountManager),
|
ApplicationSpecificNotice.dismissedPremiumColorsBadge(accountManager: context.sharedContext.accountManager),
|
||||||
ApplicationSpecificNotice.dismissedMessageTagsBadge(accountManager: context.sharedContext.accountManager),
|
ApplicationSpecificNotice.dismissedMessageTagsBadge(accountManager: context.sharedContext.accountManager),
|
||||||
ApplicationSpecificNotice.dismissedLastSeenBadge(accountManager: context.sharedContext.accountManager),
|
ApplicationSpecificNotice.dismissedLastSeenBadge(accountManager: context.sharedContext.accountManager),
|
||||||
ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager)
|
ApplicationSpecificNotice.dismissedMessagePrivacyBadge(accountManager: context.sharedContext.accountManager),
|
||||||
).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge in
|
ApplicationSpecificNotice.dismissedBusinessBadge(accountManager: context.sharedContext.accountManager)
|
||||||
|
).startStrict(next: { [weak self] dismissedPremiumAppIconsBadge, dismissedPremiumWallpapersBadge, dismissedPremiumColorsBadge, dismissedMessageTagsBadge, dismissedLastSeenBadge, dismissedMessagePrivacyBadge, dismissedBusinessBadge in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1552,8 +1585,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
if !dismissedMessagePrivacyBadge {
|
if !dismissedMessagePrivacyBadge {
|
||||||
newPerks.append(PremiumPerk.messagePrivacy.identifier)
|
newPerks.append(PremiumPerk.messagePrivacy.identifier)
|
||||||
}
|
}
|
||||||
//TODO:
|
if !dismissedBusinessBadge {
|
||||||
newPerks.append(PremiumPerk.business.identifier)
|
newPerks.append(PremiumPerk.business.identifier)
|
||||||
|
}
|
||||||
self.newPerks = newPerks
|
self.newPerks = newPerks
|
||||||
self.updated()
|
self.updated()
|
||||||
})
|
})
|
||||||
@ -1796,6 +1830,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x9b4fed),
|
UIColor(rgb: 0x9b4fed),
|
||||||
UIColor(rgb: 0x8958ff),
|
UIColor(rgb: 0x8958ff),
|
||||||
UIColor(rgb: 0x676bff),
|
UIColor(rgb: 0x676bff),
|
||||||
|
UIColor(rgb: 0x676bff), //replace
|
||||||
UIColor(rgb: 0x6172ff),
|
UIColor(rgb: 0x6172ff),
|
||||||
UIColor(rgb: 0x5b79ff),
|
UIColor(rgb: 0x5b79ff),
|
||||||
UIColor(rgb: 0x4492ff),
|
UIColor(rgb: 0x4492ff),
|
||||||
@ -1937,17 +1972,31 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
if case .business = context.component.mode, case .business = perk {
|
if case .business = context.component.mode, case .business = perk {
|
||||||
continue
|
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<Empty>
|
||||||
|
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(
|
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(0), component: titleCombinedComponent),
|
||||||
text: .plain(NSAttributedString(
|
|
||||||
string: perk.title(strings: strings),
|
|
||||||
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
|
||||||
textColor: environment.theme.list.itemPrimaryTextColor
|
|
||||||
)),
|
|
||||||
maximumNumberOfLines: 0
|
|
||||||
))),
|
|
||||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
||||||
text: .plain(NSAttributedString(
|
text: .plain(NSAttributedString(
|
||||||
string: perk.subtitle(strings: strings),
|
string: perk.subtitle(strings: strings),
|
||||||
@ -2013,6 +2062,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
|
let _ = ApplicationSpecificNotice.setDismissedMessagePrivacyBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
|
||||||
case .business:
|
case .business:
|
||||||
demoSubject = .business
|
demoSubject = .business
|
||||||
|
let _ = ApplicationSpecificNotice.setDismissedBusinessBadge(accountManager: accountContext.sharedContext.accountManager).startStandalone()
|
||||||
default:
|
default:
|
||||||
demoSubject = .doubleLimits
|
demoSubject = .doubleLimits
|
||||||
}
|
}
|
||||||
@ -2092,7 +2142,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
var perksItems: [AnyComponentWithIdentity<Empty>] = []
|
var perksItems: [AnyComponentWithIdentity<Empty>] = []
|
||||||
for perk in BusinessPerk.allCases {
|
for perk in state.configuration.businessPerks {
|
||||||
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
|
perksItems.append(AnyComponentWithIdentity(id: perksItems.count, component: AnyComponent(ListActionItemComponent(
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
title: AnyComponent(VStack([
|
title: AnyComponent(VStack([
|
||||||
@ -2123,7 +2173,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
let isPremium = state?.isPremium == true
|
let isPremium = state?.isPremium == true
|
||||||
if isPremium {
|
if isPremium {
|
||||||
switch perk {
|
switch perk {
|
||||||
case .location:
|
case .businessLocation:
|
||||||
let _ = (accountContext.engine.data.get(
|
let _ = (accountContext.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
|
TelegramEngine.EngineData.Item.Peer.BusinessLocation(id: accountContext.account.peerId)
|
||||||
)
|
)
|
||||||
@ -2133,7 +2183,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
|
push(accountContext.sharedContext.makeBusinessLocationSetupScreen(context: accountContext, initialValue: businessLocation, completion: { _ in }))
|
||||||
})
|
})
|
||||||
case .hours:
|
case .businessHours:
|
||||||
let _ = (accountContext.engine.data.get(
|
let _ = (accountContext.engine.data.get(
|
||||||
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
|
TelegramEngine.EngineData.Item.Peer.BusinessHours(id: accountContext.account.peerId)
|
||||||
)
|
)
|
||||||
@ -2143,7 +2193,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
|
push(accountContext.sharedContext.makeBusinessHoursSetupScreen(context: accountContext, initialValue: businessHours, completion: { _ in }))
|
||||||
})
|
})
|
||||||
case .quickReplies:
|
case .businessQuickReplies:
|
||||||
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|
let _ = (accountContext.sharedContext.makeQuickReplySetupScreenInitialData(context: accountContext)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||||
@ -2152,7 +2202,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
|
push(accountContext.sharedContext.makeQuickReplySetupScreen(context: accountContext, initialData: initialData))
|
||||||
})
|
})
|
||||||
case .greetings:
|
case .businessGreetingMessage:
|
||||||
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||||
@ -2161,7 +2211,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
|
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: false))
|
||||||
})
|
})
|
||||||
case .awayMessages:
|
case .businessAwayMessage:
|
||||||
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
let _ = (accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreenInitialData(context: accountContext)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||||
@ -2170,7 +2220,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
|
push(accountContext.sharedContext.makeAutomaticBusinessMessageSetupScreen(context: accountContext, initialData: initialData, isAwayMode: true))
|
||||||
})
|
})
|
||||||
case .chatbots:
|
case .businessChatBots:
|
||||||
let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext)
|
let _ = (accountContext.sharedContext.makeChatbotSetupScreenInitialData(context: accountContext)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
|> deliverOnMainQueue).start(next: { [weak accountContext] initialData in
|
||||||
@ -2179,25 +2229,29 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
}
|
}
|
||||||
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
|
push(accountContext.sharedContext.makeChatbotSetupScreen(context: accountContext, initialData: initialData))
|
||||||
})
|
})
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var demoSubject: PremiumDemoScreen.Subject
|
var demoSubject: PremiumDemoScreen.Subject
|
||||||
switch perk {
|
switch perk {
|
||||||
case .location:
|
case .businessLocation:
|
||||||
demoSubject = .businessLocation
|
demoSubject = .businessLocation
|
||||||
case .hours:
|
case .businessHours:
|
||||||
demoSubject = .businessHours
|
demoSubject = .businessHours
|
||||||
case .quickReplies:
|
case .businessQuickReplies:
|
||||||
demoSubject = .businessQuickReplies
|
demoSubject = .businessQuickReplies
|
||||||
case .greetings:
|
case .businessGreetingMessage:
|
||||||
demoSubject = .businessGreetingMessage
|
demoSubject = .businessGreetingMessage
|
||||||
case .awayMessages:
|
case .businessAwayMessage:
|
||||||
demoSubject = .businessAwayMessage
|
demoSubject = .businessAwayMessage
|
||||||
case .chatbots:
|
case .businessChatBots:
|
||||||
demoSubject = .businessChatBots
|
demoSubject = .businessChatBots
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
var dismissImpl: (() -> Void)?
|
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
|
controller.action = { [weak state] in
|
||||||
dismissImpl?()
|
dismissImpl?()
|
||||||
if state?.isPremium == false {
|
if state?.isPremium == false {
|
||||||
@ -3721,3 +3775,61 @@ private final class EmojiActionIconComponent: Component {
|
|||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -384,7 +384,27 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
let theme = self.presentationData.theme
|
let theme = self.presentationData.theme
|
||||||
let strings = self.presentationData.strings
|
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 context = controller.context
|
||||||
|
|
||||||
let textColor = theme.actionSheet.primaryTextColor
|
let textColor = theme.actionSheet.primaryTextColor
|
||||||
@ -482,7 +502,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoFile: configuration.videos["more_upload"],
|
videoFile: videos["more_upload"],
|
||||||
decoration: .dataRain
|
decoration: .dataRain
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_UploadSize,
|
title: strings.Premium_UploadSize,
|
||||||
@ -500,7 +520,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["faster_download"],
|
videoFile: videos["faster_download"],
|
||||||
decoration: .fasterStars
|
decoration: .fasterStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_FasterSpeed,
|
title: strings.Premium_FasterSpeed,
|
||||||
@ -518,7 +538,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["voice_to_text"],
|
videoFile: videos["voice_to_text"],
|
||||||
decoration: .badgeStars
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_VoiceToText,
|
title: strings.Premium_VoiceToText,
|
||||||
@ -536,7 +556,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoFile: configuration.videos["no_ads"],
|
videoFile: videos["no_ads"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_NoAds,
|
title: strings.Premium_NoAds,
|
||||||
@ -554,7 +574,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["infinite_reactions"],
|
videoFile: videos["infinite_reactions"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_InfiniteReactions,
|
title: strings.Premium_InfiniteReactions,
|
||||||
@ -593,7 +613,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["emoji_status"],
|
videoFile: videos["emoji_status"],
|
||||||
decoration: .badgeStars
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_EmojiStatus,
|
title: strings.Premium_EmojiStatus,
|
||||||
@ -611,7 +631,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["advanced_chat_management"],
|
videoFile: videos["advanced_chat_management"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_ChatManagement,
|
title: strings.Premium_ChatManagement,
|
||||||
@ -629,7 +649,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["profile_badge"],
|
videoFile: videos["profile_badge"],
|
||||||
decoration: .badgeStars
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Badge,
|
title: strings.Premium_Badge,
|
||||||
@ -647,7 +667,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["animated_userpics"],
|
videoFile: videos["animated_userpics"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Avatar,
|
title: strings.Premium_Avatar,
|
||||||
@ -681,7 +701,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .bottom,
|
position: .bottom,
|
||||||
videoFile: configuration.videos["animated_emoji"],
|
videoFile: videos["animated_emoji"],
|
||||||
decoration: .emoji
|
decoration: .emoji
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_AnimatedEmoji,
|
title: strings.Premium_AnimatedEmoji,
|
||||||
@ -700,7 +720,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["translations"],
|
videoFile: videos["translations"],
|
||||||
decoration: .hello
|
decoration: .hello
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Translation,
|
title: strings.Premium_Translation,
|
||||||
@ -718,7 +738,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
content: AnyComponent(PhoneDemoComponent(
|
content: AnyComponent(PhoneDemoComponent(
|
||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
videoFile: configuration.videos["peer_colors"],
|
videoFile: videos["peer_colors"],
|
||||||
decoration: .badgeStars
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Colors,
|
title: strings.Premium_Colors,
|
||||||
@ -737,7 +757,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["wallpapers"],
|
videoFile: videos["wallpapers"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_Wallpapers,
|
title: strings.Premium_Wallpapers,
|
||||||
@ -756,7 +776,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["saved_tags"],
|
videoFile: videos["saved_tags"],
|
||||||
decoration: .tag
|
decoration: .tag
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_MessageTags,
|
title: strings.Premium_MessageTags,
|
||||||
@ -775,7 +795,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["last_seen"],
|
videoFile: videos["last_seen"],
|
||||||
decoration: .badgeStars
|
decoration: .badgeStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_LastSeen,
|
title: strings.Premium_LastSeen,
|
||||||
@ -794,7 +814,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["message_privacy"],
|
videoFile: videos["message_privacy"],
|
||||||
decoration: .swirlStars
|
decoration: .swirlStars
|
||||||
)),
|
)),
|
||||||
title: strings.Premium_MessagePrivacy,
|
title: strings.Premium_MessagePrivacy,
|
||||||
@ -846,7 +866,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["business_location"],
|
videoFile: videos["business_location"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_Location,
|
title: strings.Business_Location,
|
||||||
@ -866,7 +886,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["business_hours"],
|
videoFile: videos["business_hours"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_OpeningHours,
|
title: strings.Business_OpeningHours,
|
||||||
@ -886,7 +906,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["quick_replies"],
|
videoFile: videos["quick_replies"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_QuickReplies,
|
title: strings.Business_QuickReplies,
|
||||||
@ -906,7 +926,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["greeting_message"],
|
videoFile: videos["greeting_message"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_GreetingMessages,
|
title: strings.Business_GreetingMessages,
|
||||||
@ -926,7 +946,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["away_message"],
|
videoFile: videos["away_message"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_AwayMessages,
|
title: strings.Business_AwayMessages,
|
||||||
@ -946,7 +966,7 @@ public class PremiumLimitsListScreen: ViewController {
|
|||||||
context: context,
|
context: context,
|
||||||
position: .top,
|
position: .top,
|
||||||
model: .island,
|
model: .island,
|
||||||
videoFile: configuration.videos["business_bots"],
|
videoFile: videos["business_bots"],
|
||||||
decoration: .business
|
decoration: .business
|
||||||
)),
|
)),
|
||||||
title: strings.Business_Chatbots,
|
title: strings.Business_Chatbots,
|
||||||
|
@ -1315,7 +1315,7 @@ public extension Api {
|
|||||||
return parser(reader)
|
return parser(reader)
|
||||||
}
|
}
|
||||||
else {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,6 +257,12 @@ public extension TelegramEngine {
|
|||||||
return (items.map(\.file), isFinalResult)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,6 +199,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
|
|||||||
case savedMessageTagLabelSuggestion = 65
|
case savedMessageTagLabelSuggestion = 65
|
||||||
case dismissedLastSeenBadge = 66
|
case dismissedLastSeenBadge = 66
|
||||||
case dismissedMessagePrivacyBadge = 67
|
case dismissedMessagePrivacyBadge = 67
|
||||||
|
case dismissedBusinessBadge = 68
|
||||||
|
|
||||||
var key: ValueBoxKey {
|
var key: ValueBoxKey {
|
||||||
let v = ValueBoxKey(length: 4)
|
let v = ValueBoxKey(length: 4)
|
||||||
@ -529,6 +530,10 @@ private struct ApplicationSpecificNoticeKeys {
|
|||||||
static func dismissedMessagePrivacyBadge() -> NoticeEntryKey {
|
static func dismissedMessagePrivacyBadge() -> NoticeEntryKey {
|
||||||
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedMessagePrivacyBadge.key)
|
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 {
|
public struct ApplicationSpecificNotice {
|
||||||
@ -2223,4 +2228,25 @@ public struct ApplicationSpecificNotice {
|
|||||||
}
|
}
|
||||||
|> take(1)
|
|> take(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func setDismissedBusinessBadge(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Never, NoError> {
|
||||||
|
return accountManager.transaction { transaction -> Void in
|
||||||
|
if let entry = CodableEntry(ApplicationSpecificBoolNotice()) {
|
||||||
|
transaction.setNotice(ApplicationSpecificNoticeKeys.dismissedBusinessBadge(), entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func dismissedBusinessBadge(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Bool, NoError> {
|
||||||
|
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.dismissedBusinessBadge())
|
||||||
|
|> map { view -> Bool in
|
||||||
|
if let _ = view.value?.get(ApplicationSpecificBoolNotice.self) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> take(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
using namespace metal;
|
using namespace metal;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float2 dimensions;
|
||||||
|
float roundness;
|
||||||
|
float alpha;
|
||||||
|
float isOpaque;
|
||||||
|
float empty;
|
||||||
|
} VideoEncodeParameters;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
float4 pos;
|
float4 pos;
|
||||||
float2 texCoord;
|
float2 texCoord;
|
||||||
@ -17,11 +25,10 @@ float sdfRoundedRectangle(float2 uv, float2 position, float2 size, float radius)
|
|||||||
|
|
||||||
fragment half4 dualFragmentShader(RasterizerData in [[stage_in]],
|
fragment half4 dualFragmentShader(RasterizerData in [[stage_in]],
|
||||||
texture2d<half, access::sample> texture [[texture(0)]],
|
texture2d<half, access::sample> texture [[texture(0)]],
|
||||||
constant uint2 &resolution[[buffer(0)]],
|
texture2d<half, access::sample> mask [[texture(1)]],
|
||||||
constant float &roundness[[buffer(1)]],
|
constant VideoEncodeParameters& adjustments [[buffer(0)]]
|
||||||
constant float &alpha[[buffer(2)]]
|
|
||||||
) {
|
) {
|
||||||
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;
|
float2 uv = (in.localPos - float2(0.5, 0.5)) * 2.0;
|
||||||
if (R.x > R.y) {
|
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);
|
constexpr sampler samplr(filter::linear, mag_filter::linear, min_filter::linear);
|
||||||
half3 color = texture.sample(samplr, in.texCoord).rgb;
|
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 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);
|
||||||
}
|
}
|
||||||
|
@ -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<CutoutResult?, NoError> {
|
||||||
|
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, *)
|
@available(iOS 17.0, *)
|
||||||
private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet {
|
private func instances(atPoint maybePoint: CGPoint?, inObservation observation: VNInstanceMaskObservation) -> IndexSet {
|
||||||
guard let point = maybePoint else {
|
guard let point = maybePoint else {
|
||||||
|
@ -190,6 +190,7 @@ public final class MediaEditor {
|
|||||||
|
|
||||||
public private(set) var canCutout: Bool = false
|
public private(set) var canCutout: Bool = false
|
||||||
public var canCutoutUpdated: (Bool) -> Void = { _ in }
|
public var canCutoutUpdated: (Bool) -> Void = { _ in }
|
||||||
|
public var isCutoutUpdated: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
private var textureCache: CVMetalTextureCache!
|
private var textureCache: CVMetalTextureCache!
|
||||||
|
|
||||||
@ -1682,6 +1683,51 @@ public final class MediaEditor {
|
|||||||
self.renderer.renderFrame()
|
self.renderer.renderFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getSeparatedImage(point: CGPoint?) -> Signal<UIImage?, NoError> {
|
||||||
|
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?) {
|
private func maybeGeneratePersonSegmentation(_ image: UIImage?) {
|
||||||
if #available(iOS 15.0, *), let cgImage = image?.cgImage {
|
if #available(iOS 15.0, *), let cgImage = image?.cgImage {
|
||||||
let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in
|
let faceRequest = VNDetectFaceRectanglesRequest { [weak self] request, _ in
|
||||||
|
@ -98,6 +98,7 @@ final class MediaEditorRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var currentMainInput: Input?
|
private var currentMainInput: Input?
|
||||||
|
var currentMainInputMask: MTLTexture?
|
||||||
private var currentAdditionalInput: Input?
|
private var currentAdditionalInput: Input?
|
||||||
private(set) var resultTexture: MTLTexture?
|
private(set) var resultTexture: MTLTexture?
|
||||||
|
|
||||||
@ -202,7 +203,7 @@ final class MediaEditorRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let mainTexture {
|
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 {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
func setMainInput(_ input: Input) {
|
||||||
guard let renderTarget = self.renderTarget else {
|
guard let renderTarget = self.renderTarget else {
|
||||||
return
|
return
|
||||||
|
@ -144,6 +144,14 @@ private var transitionDuration = 0.5
|
|||||||
private var apperanceDuration = 0.2
|
private var apperanceDuration = 0.2
|
||||||
private var videoRemovalDuration: Double = 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 {
|
final class VideoFinishPass: RenderPass {
|
||||||
private var cachedTexture: MTLTexture?
|
private var cachedTexture: MTLTexture?
|
||||||
|
|
||||||
@ -195,6 +203,7 @@ final class VideoFinishPass: RenderPass {
|
|||||||
containerSize: CGSize,
|
containerSize: CGSize,
|
||||||
texture: MTLTexture,
|
texture: MTLTexture,
|
||||||
textureRotation: TextureRotation,
|
textureRotation: TextureRotation,
|
||||||
|
maskTexture: MTLTexture?,
|
||||||
position: VideoPosition,
|
position: VideoPosition,
|
||||||
roundness: Float,
|
roundness: Float,
|
||||||
alpha: Float,
|
alpha: Float,
|
||||||
@ -202,6 +211,11 @@ final class VideoFinishPass: RenderPass {
|
|||||||
device: MTLDevice
|
device: MTLDevice
|
||||||
) {
|
) {
|
||||||
encoder.setFragmentTexture(texture, index: 0)
|
encoder.setFragmentTexture(texture, index: 0)
|
||||||
|
if let maskTexture {
|
||||||
|
encoder.setFragmentTexture(maskTexture, index: 1)
|
||||||
|
} else {
|
||||||
|
encoder.setFragmentTexture(texture, index: 1)
|
||||||
|
}
|
||||||
|
|
||||||
let center = CGPoint(
|
let center = CGPoint(
|
||||||
x: position.position.x - containerSize.width / 2.0,
|
x: position.position.x - containerSize.width / 2.0,
|
||||||
@ -220,14 +234,25 @@ final class VideoFinishPass: RenderPass {
|
|||||||
options: [])
|
options: [])
|
||||||
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
|
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
|
||||||
|
|
||||||
var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
var parameters = VideoEncodeParameters(
|
||||||
encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
dimensions: simd_float2(Float(size.width), Float(size.height)),
|
||||||
|
roundness: roundness,
|
||||||
var roundness = roundness
|
alpha: alpha,
|
||||||
encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
isOpaque: maskTexture == nil ? 1.0 : 0.0,
|
||||||
|
empty: 0
|
||||||
var alpha = alpha
|
)
|
||||||
encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
encoder.setFragmentBytes(¶meters, length: MemoryLayout<VideoEncodeParameters>.size, index: 0)
|
||||||
|
// var resolution = simd_uint2(UInt32(size.width), UInt32(size.height))
|
||||||
|
// encoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||||
|
//
|
||||||
|
// var roundness = roundness
|
||||||
|
// encoder.setFragmentBytes(&roundness, length: MemoryLayout<simd_float1>.size, index: 1)
|
||||||
|
//
|
||||||
|
// var alpha = alpha
|
||||||
|
// encoder.setFragmentBytes(&alpha, length: MemoryLayout<simd_float1>.size, index: 2)
|
||||||
|
//
|
||||||
|
// var isOpaque = maskTexture == nil ? 1.0 : 0.0
|
||||||
|
// encoder.setFragmentBytes(&isOpaque, length: MemoryLayout<simd_float1>.size, index: 3)
|
||||||
|
|
||||||
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
|
||||||
}
|
}
|
||||||
@ -478,7 +503,14 @@ final class VideoFinishPass: RenderPass {
|
|||||||
return (backgroundVideoState, foregroundVideoState, disappearingVideoState)
|
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 {
|
if !self.isStory {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
@ -536,7 +568,6 @@ final class VideoFinishPass: RenderPass {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.gradientColors.topColor.w > 0.0 {
|
if self.gradientColors.topColor.w > 0.0 {
|
||||||
renderCommandEncoder.setRenderPipelineState(self.gradientPipelineState!)
|
|
||||||
self.encodeGradient(
|
self.encodeGradient(
|
||||||
using: renderCommandEncoder,
|
using: renderCommandEncoder,
|
||||||
containerSize: containerSize,
|
containerSize: containerSize,
|
||||||
@ -554,6 +585,7 @@ final class VideoFinishPass: RenderPass {
|
|||||||
containerSize: containerSize,
|
containerSize: containerSize,
|
||||||
texture: transitionVideoState.texture,
|
texture: transitionVideoState.texture,
|
||||||
textureRotation: transitionVideoState.textureRotation,
|
textureRotation: transitionVideoState.textureRotation,
|
||||||
|
maskTexture: nil,
|
||||||
position: transitionVideoState.position,
|
position: transitionVideoState.position,
|
||||||
roundness: transitionVideoState.roundness,
|
roundness: transitionVideoState.roundness,
|
||||||
alpha: transitionVideoState.alpha,
|
alpha: transitionVideoState.alpha,
|
||||||
@ -567,6 +599,7 @@ final class VideoFinishPass: RenderPass {
|
|||||||
containerSize: containerSize,
|
containerSize: containerSize,
|
||||||
texture: mainVideoState.texture,
|
texture: mainVideoState.texture,
|
||||||
textureRotation: mainVideoState.textureRotation,
|
textureRotation: mainVideoState.textureRotation,
|
||||||
|
maskTexture: inputMask,
|
||||||
position: mainVideoState.position,
|
position: mainVideoState.position,
|
||||||
roundness: mainVideoState.roundness,
|
roundness: mainVideoState.roundness,
|
||||||
alpha: mainVideoState.alpha,
|
alpha: mainVideoState.alpha,
|
||||||
@ -580,6 +613,7 @@ final class VideoFinishPass: RenderPass {
|
|||||||
containerSize: containerSize,
|
containerSize: containerSize,
|
||||||
texture: additionalVideoState.texture,
|
texture: additionalVideoState.texture,
|
||||||
textureRotation: additionalVideoState.textureRotation,
|
textureRotation: additionalVideoState.textureRotation,
|
||||||
|
maskTexture: nil,
|
||||||
position: additionalVideoState.position,
|
position: additionalVideoState.position,
|
||||||
roundness: additionalVideoState.roundness,
|
roundness: additionalVideoState.roundness,
|
||||||
alpha: additionalVideoState.alpha,
|
alpha: additionalVideoState.alpha,
|
||||||
@ -603,6 +637,7 @@ final class VideoFinishPass: RenderPass {
|
|||||||
containerSize: CGSize,
|
containerSize: CGSize,
|
||||||
device: MTLDevice
|
device: MTLDevice
|
||||||
) {
|
) {
|
||||||
|
encoder.setRenderPipelineState(self.gradientPipelineState!)
|
||||||
|
|
||||||
let vertices = verticesDataForRotation(.rotate0Degrees)
|
let vertices = verticesDataForRotation(.rotate0Degrees)
|
||||||
let buffer = device.makeBuffer(
|
let buffer = device.makeBuffer(
|
||||||
|
@ -51,6 +51,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
|
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
|
||||||
"//submodules/TelegramUI/Components/MediaScrubberComponent",
|
"//submodules/TelegramUI/Components/MediaScrubberComponent",
|
||||||
"//submodules/Components/BlurredBackgroundComponent",
|
"//submodules/Components/BlurredBackgroundComponent",
|
||||||
|
"//submodules/TelegramUI/Components/DustEffect",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -15,6 +15,7 @@ import MediaEditor
|
|||||||
import Photos
|
import Photos
|
||||||
import LottieAnimationComponent
|
import LottieAnimationComponent
|
||||||
import MessageInputPanelComponent
|
import MessageInputPanelComponent
|
||||||
|
import DustEffect
|
||||||
|
|
||||||
private final class MediaCutoutScreenComponent: Component {
|
private final class MediaCutoutScreenComponent: Component {
|
||||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||||
@ -40,9 +41,13 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
public final class View: UIView {
|
public final class View: UIView {
|
||||||
private let buttonsContainerView = UIView()
|
private let buttonsContainerView = UIView()
|
||||||
private let buttonsBackgroundView = UIView()
|
private let buttonsBackgroundView = UIView()
|
||||||
|
private let previewContainerView = UIView()
|
||||||
private let cancelButton = ComponentView<Empty>()
|
private let cancelButton = ComponentView<Empty>()
|
||||||
private let label = ComponentView<Empty>()
|
private let label = ComponentView<Empty>()
|
||||||
private let doneButton = ComponentView<Empty>()
|
private let doneButton = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private let fadeView = UIView()
|
||||||
|
private let separatedImageView = UIImageView()
|
||||||
|
|
||||||
private var component: MediaCutoutScreenComponent?
|
private var component: MediaCutoutScreenComponent?
|
||||||
private weak var state: State?
|
private weak var state: State?
|
||||||
@ -51,18 +56,44 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.buttonsContainerView.clipsToBounds = true
|
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)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.backgroundColor = .clear
|
self.backgroundColor = .clear
|
||||||
|
|
||||||
self.addSubview(self.buttonsContainerView)
|
self.addSubview(self.buttonsContainerView)
|
||||||
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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() {
|
func animateInFromEditor() {
|
||||||
self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
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)
|
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.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
|
self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||||
completion()
|
completion()
|
||||||
})
|
})
|
||||||
@ -82,14 +114,35 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
self.state?.updated()
|
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<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
|
||||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||||
self.environment = environment
|
self.environment = environment
|
||||||
|
|
||||||
|
let isFirstTime = self.component == nil
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
// let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
||||||
let isTablet: Bool
|
let isTablet: Bool
|
||||||
if case .regular = environment.metrics.widthClass {
|
if case .regular = environment.metrics.widthClass {
|
||||||
isTablet = true
|
isTablet = true
|
||||||
@ -97,8 +150,6 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
isTablet = false
|
isTablet = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// let mediaEditor = (environment.controller() as? MediaCutoutScreen)?.mediaEditor
|
|
||||||
|
|
||||||
let buttonSideInset: CGFloat
|
let buttonSideInset: CGFloat
|
||||||
let buttonBottomInset: CGFloat = 8.0
|
let buttonBottomInset: CGFloat = 8.0
|
||||||
var controlsBottomInset: CGFloat = 0.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 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(
|
let cancelButtonSize = self.cancelButton.update(
|
||||||
@ -140,7 +191,7 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
guard let controller = environment.controller() as? MediaCutoutScreen else {
|
guard let controller = environment.controller() as? MediaCutoutScreen else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
controller.requestDismiss(reset: true, animated: true)
|
controller.requestDismiss(animated: true)
|
||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
@ -177,6 +228,30 @@ private final class MediaCutoutScreenComponent: Component {
|
|||||||
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
|
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
|
||||||
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
|
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
|
return availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,14 +391,16 @@ public final class MediaCutoutScreen: ViewController {
|
|||||||
|
|
||||||
fileprivate let context: AccountContext
|
fileprivate let context: AccountContext
|
||||||
fileprivate let mediaEditor: MediaEditor
|
fileprivate let mediaEditor: MediaEditor
|
||||||
|
fileprivate let previewView: MediaEditorPreviewView
|
||||||
|
|
||||||
public var dismissed: () -> Void = {}
|
public var dismissed: () -> Void = {}
|
||||||
|
|
||||||
private var initialValues: MediaEditorValues
|
private var initialValues: MediaEditorValues
|
||||||
|
|
||||||
public init(context: AccountContext, mediaEditor: MediaEditor) {
|
public init(context: AccountContext, mediaEditor: MediaEditor, previewView: MediaEditorPreviewView) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.mediaEditor = mediaEditor
|
self.mediaEditor = mediaEditor
|
||||||
|
self.previewView = previewView
|
||||||
self.initialValues = mediaEditor.values.makeCopy()
|
self.initialValues = mediaEditor.values.makeCopy()
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: nil)
|
super.init(navigationBarPresentationData: nil)
|
||||||
@ -344,11 +421,7 @@ public final class MediaCutoutScreen: ViewController {
|
|||||||
super.displayNodeDidLoad()
|
super.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestDismiss(reset: Bool, animated: Bool) {
|
func requestDismiss(animated: Bool) {
|
||||||
if reset {
|
|
||||||
self.mediaEditor.values = self.initialValues
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dismissed()
|
self.dismissed()
|
||||||
|
|
||||||
self.node.animateOutToEditor(completion: {
|
self.node.animateOutToEditor(completion: {
|
||||||
|
@ -154,6 +154,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
case tools
|
case tools
|
||||||
case done
|
case done
|
||||||
case cutout
|
case cutout
|
||||||
|
case undo
|
||||||
}
|
}
|
||||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||||
func image(_ key: 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)!
|
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Tools"), color: .white)!
|
||||||
case .cutout:
|
case .cutout:
|
||||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)!
|
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Cutout"), color: .white)!
|
||||||
|
case .undo:
|
||||||
|
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/CutoutUndo"), color: .white)!
|
||||||
case .done:
|
case .done:
|
||||||
image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
image = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
@ -981,13 +984,14 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if controller.node.canCutout {
|
if controller.node.canCutout {
|
||||||
|
let isCutout = controller.node.isCutout
|
||||||
let cutoutButtonSize = self.cutoutButton.update(
|
let cutoutButtonSize = self.cutoutButton.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(PlainButtonComponent(
|
component: AnyComponent(PlainButtonComponent(
|
||||||
content: AnyComponent(CutoutButtonContentComponent(
|
content: AnyComponent(CutoutButtonContentComponent(
|
||||||
backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18),
|
backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.18),
|
||||||
icon: state.image(.cutout),
|
icon: state.image(isCutout ? .undo : .cutout),
|
||||||
title: "Cut Out an Object"
|
title: isCutout ? "Undo Cut Out" : "Cut Out an Object"
|
||||||
)),
|
)),
|
||||||
effectAlignment: .center,
|
effectAlignment: .center,
|
||||||
action: {
|
action: {
|
||||||
@ -2161,6 +2165,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
private var isDismissBySwipeSuppressed = false
|
private var isDismissBySwipeSuppressed = false
|
||||||
|
|
||||||
fileprivate var canCutout = false
|
fileprivate var canCutout = false
|
||||||
|
fileprivate var isCutout = false
|
||||||
|
|
||||||
private (set) var hasAnyChanges = false
|
private (set) var hasAnyChanges = false
|
||||||
|
|
||||||
@ -2513,7 +2518,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.canCutout = canCutout
|
self.canCutout = canCutout
|
||||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
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 {
|
if case .message = effectiveSubject {
|
||||||
} else {
|
} else {
|
||||||
@ -4232,14 +4243,25 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.entitiesView.selectEntity(nil)
|
self.entitiesView.selectEntity(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
let controller = MediaCutoutScreen(context: self.context, mediaEditor: mediaEditor)
|
if controller.node.isCutout {
|
||||||
controller.dismissed = { [weak self] in
|
let snapshotView = self.previewView.snapshotView(afterScreenUpdates: false)
|
||||||
if let self {
|
if let snapshotView {
|
||||||
self.animateInFromTool(inPlace: true)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -5085,35 +5107,46 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let title: String
|
var title: String
|
||||||
let save: String
|
var text: String
|
||||||
if case .draft = self.node.actualSubject {
|
var save: String?
|
||||||
title = presentationData.strings.Story_Editor_DraftDiscardDraft
|
switch self.mode {
|
||||||
save = presentationData.strings.Story_Editor_DraftKeepDraft
|
case .storyEditor:
|
||||||
} else {
|
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
|
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(
|
let controller = textAlertController(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
forceTheme: defaultDarkPresentationTheme,
|
forceTheme: defaultDarkPresentationTheme,
|
||||||
title: title,
|
title: title,
|
||||||
text: presentationData.strings.Story_Editor_DraftDiscaedText,
|
text: text,
|
||||||
actions: [
|
actions: 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: {
|
|
||||||
|
|
||||||
})
|
|
||||||
],
|
|
||||||
actionLayout: .vertical
|
actionLayout: .vertical
|
||||||
)
|
)
|
||||||
self.present(controller, in: .window(.root))
|
self.present(controller, in: .window(.root))
|
||||||
|
@ -345,16 +345,17 @@ public final class AvatarStoryIndicatorComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
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())
|
||||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth
|
let lineWidth: CGFloat = component.hasUnseen ? component.activeLineWidth : component.inactiveLineWidth
|
||||||
context.setLineWidth(lineWidth)
|
context.setLineWidth(lineWidth)
|
||||||
if component.isRoundedRect {
|
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 {
|
} 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))
|
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))
|
||||||
}
|
}
|
||||||
|
@ -260,23 +260,25 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
|
|||||||
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4))
|
controller.updateCameraState({ $0.updatedRecording(pressing ? .holding : .handsFree).updatedDuration(initialDuration) }, transition: .spring(duration: 0.4))
|
||||||
|
|
||||||
controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
|
controller.node.withReadyCamera(isFirstTime: !controller.node.cameraIsActive) {
|
||||||
self.resultDisposable.set((camera.startRecording()
|
Queue.mainQueue().after(0.15) {
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] recordingData in
|
self.resultDisposable.set((camera.startRecording()
|
||||||
let duration = initialDuration + recordingData.duration
|
|> deliverOnMainQueue).start(next: { [weak self] recordingData in
|
||||||
if let self, let controller = self.getController() {
|
let duration = initialDuration + recordingData.duration
|
||||||
controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1))
|
if let self, let controller = self.getController() {
|
||||||
if isFirstRecording {
|
controller.updateCameraState({ $0.updatedDuration(duration) }, transition: .easeInOut(duration: 0.1))
|
||||||
controller.node.setupLiveUpload(filePath: recordingData.filePath)
|
if isFirstRecording {
|
||||||
|
controller.node.setupLiveUpload(filePath: recordingData.filePath)
|
||||||
|
}
|
||||||
|
if duration > 59.5 {
|
||||||
|
controller.onStop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if duration > 59.5 {
|
}, error: { [weak self] _ in
|
||||||
controller.onStop()
|
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 {
|
if initialDuration > 0.0 {
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "undo2_30.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
126
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf
vendored
Normal file
126
submodules/TelegramUI/Images.xcassets/Media Editor/CutoutUndo.imageset/undo2_30.pdf
vendored
Normal file
@ -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
|
@ -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, {
|
strongSelf.updateChatPresentationInterfaceState(animated: animated, interactive: false, {
|
||||||
return $0.updatedPeer { _ in
|
return $0.updatedPeer { _ in
|
||||||
return renderedPeer
|
return renderedPeer
|
||||||
@ -6152,6 +6159,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
.updatedHasSearchTags(hasSearchTags)
|
.updatedHasSearchTags(hasSearchTags)
|
||||||
.updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging)
|
.updatedIsPremiumRequiredForMessaging(isPremiumRequiredForMessaging)
|
||||||
.updatedHasSavedChats(hasSavedChats)
|
.updatedHasSavedChats(hasSavedChats)
|
||||||
|
.updatedAppliedBoosts(appliedBoosts)
|
||||||
|
.updatedBoostsToUnrestrict(boostsToUnrestrict)
|
||||||
.updatedInterfaceState { interfaceState in
|
.updatedInterfaceState { interfaceState in
|
||||||
var interfaceState = interfaceState
|
var interfaceState = interfaceState
|
||||||
|
|
||||||
|
@ -2000,6 +2000,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
mappedSource = .readTime
|
mappedSource = .readTime
|
||||||
case .messageTags:
|
case .messageTags:
|
||||||
mappedSource = .messageTags
|
mappedSource = .messageTags
|
||||||
|
case .folderTags:
|
||||||
|
mappedSource = .folderTags
|
||||||
}
|
}
|
||||||
let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark)
|
let controller = PremiumIntroScreen(context: context, source: mappedSource, modal: modal, forceDark: forceDark)
|
||||||
controller.wasDismissed = dismissed
|
controller.wasDismissed = dismissed
|
||||||
@ -2049,6 +2051,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
mappedSubject = .lastSeen
|
mappedSubject = .lastSeen
|
||||||
case .messagePrivacy:
|
case .messagePrivacy:
|
||||||
mappedSubject = .messagePrivacy
|
mappedSubject = .messagePrivacy
|
||||||
|
case .folderTags:
|
||||||
|
mappedSubject = .folderTags
|
||||||
default:
|
default:
|
||||||
mappedSubject = .doubleLimits
|
mappedSubject = .doubleLimits
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ final class WebAppWebView: WKWebView {
|
|||||||
self.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
self.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||||
return point.x > 30.0
|
return point.x > 30.0
|
||||||
}
|
}
|
||||||
self.allowsBackForwardNavigationGestures = false
|
self.allowsBackForwardNavigationGestures = true
|
||||||
if #available(iOS 16.4, *) {
|
if #available(iOS 16.4, *) {
|
||||||
self.isInspectable = true
|
self.isInspectable = true
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user