This commit is contained in:
Ilya Laktyushin 2023-06-22 05:01:19 +04:00
parent 75c01b0897
commit 53aa40353a
9 changed files with 282 additions and 177 deletions

View File

@ -939,6 +939,7 @@ public enum PremiumIntroSource {
case voiceToText
case fasterDownload
case translation
case stories
}
public enum PremiumDemoSubject {
@ -956,6 +957,7 @@ public enum PremiumDemoSubject {
case animatedEmoji
case emojiStatus
case translation
case stories
}
public enum PremiumLimitSubject {

View File

@ -158,7 +158,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
private var activeDownloadsDisposable: Disposable?
private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer?
private var isPremium: Bool = false
private(set) var isPremium: Bool = false
private var didSetupTabs = false
@ -2341,6 +2341,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
fileprivate func openStoryCamera() {
guard self.isPremium else {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: self.context, subject: .stories, action: {
let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.push(controller)
return
}
var cameraTransitionIn: StoryCameraTransitionIn?
if let componentView = self.chatListHeaderView() {
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
@ -2501,7 +2514,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})))
let isMuted = notificationSettings.storiesMuted == true
items.append(.action(ContextMenuActionItem(text: isMuted ? "Unmute" : "Mute", icon: { theme in
items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Muted": "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
f(.default)

View File

@ -1191,7 +1191,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
if case .compact = layout.metrics.widthClass {
if case .compact = layout.metrics.widthClass, self.controller?.isPremium == true {
let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
if selectedIndex <= 0 && translation.x > 0.0 {
transitionFraction = 0.0
@ -1204,6 +1204,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
transitionFraction = 0.0
return
}
} else {
if selectedIndex <= 0 && translation.x > 0.0 {
let overscroll = translation.x
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
}
}
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {

View File

@ -68,6 +68,9 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
private var presentationDataDisposable: Disposable?
private let stringsPromise = Promise<PresentationStrings>()
private var isPremium = false
private var isPremiumDisposable: Disposable?
weak var controller: ContactsController?
private var initialScrollingOffset: CGFloat?
@ -258,11 +261,22 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
self.storiesReady.set(.single(true))
})
self.isPremiumDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> map {
return $0?.isPremium ?? false
}
|> deliverOnMainQueue).start(next: { [weak self] isPremium in
if let self {
self.isPremium = isPremium
}
})
}
deinit {
self.presentationDataDisposable?.dispose()
self.storySubscriptionsDisposable?.dispose()
self.isPremiumDisposable?.dispose()
}
override func didLoad() {
@ -293,6 +307,10 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
return false
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return self.isPremium
}
private func updateThemeAndStrings() {
self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
self.searchDisplayController?.updatePresentationData(self.presentationData)

View File

@ -471,7 +471,7 @@ private final class DemoSheetContent: CombinedComponent {
self.context = context
self.subject = subject
self.source = source
self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .animatedEmoji, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons, .translation]
self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .animatedEmoji, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons, .translation, .stories]
self.action = action
self.dismiss = dismiss
}
@ -939,6 +939,25 @@ private final class DemoSheetContent: CombinedComponent {
)
)
)
//TODO:localize
availableItems[.stories] = DemoPagerComponent.Item(
AnyComponentWithIdentity(
id: PremiumDemoScreen.Subject.stories,
component: AnyComponent(
PageComponent(
content: AnyComponent(PhoneDemoComponent(
context: component.context,
position: .top,
videoFile: configuration.videos["voice_to_text"],
decoration: .badgeStars
)),
title: "Story Posting",
text: "Be one of the first to share your stories with your contacts or an unlimited audience.",
textColor: textColor
)
)
)
)
var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] }
let index: Int
@ -1029,6 +1048,10 @@ private final class DemoSheetContent: CombinedComponent {
buttonAnimationName = "premium_unlock"
case .translation:
buttonText = strings.Premium_Translation_Proceed
case .stories:
//TODO:localize
buttonText = "Unlock Story Posting"
buttonAnimationName = "premium_unlock"
default:
buttonText = strings.Common_OK
}
@ -1210,6 +1233,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer {
case animatedEmoji
case emojiStatus
case translation
case stories
}
public enum Source: Equatable {

View File

@ -397,6 +397,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
}
let buttonText: String

View File

@ -184,6 +184,12 @@ public enum PremiumSource: Equatable {
} else {
return false
}
case .stories:
if case .stories = rhs {
return true
} else {
return false
}
}
}
@ -213,63 +219,66 @@ public enum PremiumSource: Equatable {
case translation
case linksPerSharedFolder
case membershipInSharedFolders
case stories
var identifier: String? {
switch self {
case .settings:
return "settings"
case .stickers:
return "premium_stickers"
case .reactions:
return "infinite_reactions"
case .ads:
return "no_ads"
case .upload:
return "more_upload"
case .appIcons:
return "app_icons"
case .groupsAndChannels:
return "double_limits__channels"
case .pinnedChats:
return "double_limits__dialog_pinned"
case .publicLinks:
return "double_limits__channels_public"
case .savedGifs:
return "double_limits__saved_gifs"
case .savedStickers:
return "double_limits__stickers_faved"
case .folders:
return "double_limits__dialog_filters"
case .chatsPerFolder:
return "double_limits__dialog_filters_chats"
case .accounts:
return "double_limits__accounts"
case .about:
return "double_limits__about"
case .animatedEmoji:
return "animated_emoji"
case let .profile(id):
return "profile__\(id.id._internalGetInt64Value())"
case .emojiStatus:
return "emoji_status"
case .voiceToText:
return "voice_to_text"
case .fasterDownload:
return "faster_download"
case .gift, .giftTerms:
return nil
case let .deeplink(reference):
if let reference = reference {
return "deeplink_\(reference)"
} else {
return "deeplink"
}
case .translation:
return "translations"
case .linksPerSharedFolder:
return "double_limits__community_invites"
case .membershipInSharedFolders:
return "double_limits__communities_joined"
case .settings:
return "settings"
case .stickers:
return "premium_stickers"
case .reactions:
return "infinite_reactions"
case .ads:
return "no_ads"
case .upload:
return "more_upload"
case .appIcons:
return "app_icons"
case .groupsAndChannels:
return "double_limits__channels"
case .pinnedChats:
return "double_limits__dialog_pinned"
case .publicLinks:
return "double_limits__channels_public"
case .savedGifs:
return "double_limits__saved_gifs"
case .savedStickers:
return "double_limits__stickers_faved"
case .folders:
return "double_limits__dialog_filters"
case .chatsPerFolder:
return "double_limits__dialog_filters_chats"
case .accounts:
return "double_limits__accounts"
case .about:
return "double_limits__about"
case .animatedEmoji:
return "animated_emoji"
case let .profile(id):
return "profile__\(id.id._internalGetInt64Value())"
case .emojiStatus:
return "emoji_status"
case .voiceToText:
return "voice_to_text"
case .fasterDownload:
return "faster_download"
case .gift, .giftTerms:
return nil
case let .deeplink(reference):
if let reference = reference {
return "deeplink_\(reference)"
} else {
return "deeplink"
}
case .translation:
return "translations"
case .linksPerSharedFolder:
return "double_limits__community_invites"
case .membershipInSharedFolders:
return "double_limits__communities_joined"
case .stories:
return "stories"
}
}
}
@ -289,6 +298,7 @@ enum PremiumPerk: CaseIterable {
case animatedEmoji
case emojiStatus
case translation
case stories
static var allCases: [PremiumPerk] {
return [
@ -321,133 +331,142 @@ enum PremiumPerk: CaseIterable {
var identifier: String {
switch self {
case .doubleLimits:
return "double_limits"
case .moreUpload:
return "more_upload"
case .fasterDownload:
return "faster_download"
case .voiceToText:
return "voice_to_text"
case .noAds:
return "no_ads"
case .uniqueReactions:
return "infinite_reactions"
case .premiumStickers:
return "premium_stickers"
case .advancedChatManagement:
return "advanced_chat_management"
case .profileBadge:
return "profile_badge"
case .animatedUserpics:
return "animated_userpics"
case .appIcons:
return "app_icons"
case .animatedEmoji:
return "animated_emoji"
case .emojiStatus:
return "emoji_status"
case .translation:
return "translations"
case .doubleLimits:
return "double_limits"
case .moreUpload:
return "more_upload"
case .fasterDownload:
return "faster_download"
case .voiceToText:
return "voice_to_text"
case .noAds:
return "no_ads"
case .uniqueReactions:
return "infinite_reactions"
case .premiumStickers:
return "premium_stickers"
case .advancedChatManagement:
return "advanced_chat_management"
case .profileBadge:
return "profile_badge"
case .animatedUserpics:
return "animated_userpics"
case .appIcons:
return "app_icons"
case .animatedEmoji:
return "animated_emoji"
case .emojiStatus:
return "emoji_status"
case .translation:
return "translations"
case .stories:
return "stories"
}
}
func title(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
return strings.Premium_DoubledLimits
case .moreUpload:
return strings.Premium_UploadSize
case .fasterDownload:
return strings.Premium_FasterSpeed
case .voiceToText:
return strings.Premium_VoiceToText
case .noAds:
return strings.Premium_NoAds
case .uniqueReactions:
return strings.Premium_InfiniteReactions
case .premiumStickers:
return strings.Premium_Stickers
case .advancedChatManagement:
return strings.Premium_ChatManagement
case .profileBadge:
return strings.Premium_Badge
case .animatedUserpics:
return strings.Premium_Avatar
case .appIcons:
return strings.Premium_AppIcon
case .animatedEmoji:
return strings.Premium_AnimatedEmoji
case .emojiStatus:
return strings.Premium_EmojiStatus
case .translation:
return strings.Premium_Translation
case .doubleLimits:
return strings.Premium_DoubledLimits
case .moreUpload:
return strings.Premium_UploadSize
case .fasterDownload:
return strings.Premium_FasterSpeed
case .voiceToText:
return strings.Premium_VoiceToText
case .noAds:
return strings.Premium_NoAds
case .uniqueReactions:
return strings.Premium_InfiniteReactions
case .premiumStickers:
return strings.Premium_Stickers
case .advancedChatManagement:
return strings.Premium_ChatManagement
case .profileBadge:
return strings.Premium_Badge
case .animatedUserpics:
return strings.Premium_Avatar
case .appIcons:
return strings.Premium_AppIcon
case .animatedEmoji:
return strings.Premium_AnimatedEmoji
case .emojiStatus:
return strings.Premium_EmojiStatus
case .translation:
return strings.Premium_Translation
case .stories:
//TODO:localize
return "Story Posting"
}
}
func subtitle(strings: PresentationStrings) -> String {
switch self {
case .doubleLimits:
return strings.Premium_DoubledLimitsInfo
case .moreUpload:
return strings.Premium_UploadSizeInfo
case .fasterDownload:
return strings.Premium_FasterSpeedInfo
case .voiceToText:
return strings.Premium_VoiceToTextInfo
case .noAds:
return strings.Premium_NoAdsInfo
case .uniqueReactions:
return strings.Premium_InfiniteReactionsInfo
case .premiumStickers:
return strings.Premium_StickersInfo
case .advancedChatManagement:
return strings.Premium_ChatManagementInfo
case .profileBadge:
return strings.Premium_BadgeInfo
case .animatedUserpics:
return strings.Premium_AvatarInfo
case .appIcons:
return strings.Premium_AppIconInfo
case .animatedEmoji:
return strings.Premium_AnimatedEmojiInfo
case .emojiStatus:
return strings.Premium_EmojiStatusInfo
case .translation:
return strings.Premium_TranslationInfo
case .doubleLimits:
return strings.Premium_DoubledLimitsInfo
case .moreUpload:
return strings.Premium_UploadSizeInfo
case .fasterDownload:
return strings.Premium_FasterSpeedInfo
case .voiceToText:
return strings.Premium_VoiceToTextInfo
case .noAds:
return strings.Premium_NoAdsInfo
case .uniqueReactions:
return strings.Premium_InfiniteReactionsInfo
case .premiumStickers:
return strings.Premium_StickersInfo
case .advancedChatManagement:
return strings.Premium_ChatManagementInfo
case .profileBadge:
return strings.Premium_BadgeInfo
case .animatedUserpics:
return strings.Premium_AvatarInfo
case .appIcons:
return strings.Premium_AppIconInfo
case .animatedEmoji:
return strings.Premium_AnimatedEmojiInfo
case .emojiStatus:
return strings.Premium_EmojiStatusInfo
case .translation:
return strings.Premium_TranslationInfo
case .stories:
return "Be one of the first to share your stories with your contacts or an unlimited audience."
}
}
var iconName: String {
switch self {
case .doubleLimits:
return "Premium/Perk/Limits"
case .moreUpload:
return "Premium/Perk/Upload"
case .fasterDownload:
return "Premium/Perk/Speed"
case .voiceToText:
return "Premium/Perk/Voice"
case .noAds:
return "Premium/Perk/NoAds"
case .uniqueReactions:
return "Premium/Perk/Reactions"
case .premiumStickers:
return "Premium/Perk/Stickers"
case .advancedChatManagement:
return "Premium/Perk/Chat"
case .profileBadge:
return "Premium/Perk/Badge"
case .animatedUserpics:
return "Premium/Perk/Avatar"
case .appIcons:
return "Premium/Perk/AppIcon"
case .animatedEmoji:
return "Premium/Perk/Emoji"
case .emojiStatus:
return "Premium/Perk/Status"
case .translation:
return "Premium/Perk/Translation"
case .doubleLimits:
return "Premium/Perk/Limits"
case .moreUpload:
return "Premium/Perk/Upload"
case .fasterDownload:
return "Premium/Perk/Speed"
case .voiceToText:
return "Premium/Perk/Voice"
case .noAds:
return "Premium/Perk/NoAds"
case .uniqueReactions:
return "Premium/Perk/Reactions"
case .premiumStickers:
return "Premium/Perk/Stickers"
case .advancedChatManagement:
return "Premium/Perk/Chat"
case .profileBadge:
return "Premium/Perk/Badge"
case .animatedUserpics:
return "Premium/Perk/Avatar"
case .appIcons:
return "Premium/Perk/AppIcon"
case .animatedEmoji:
return "Premium/Perk/Emoji"
case .emojiStatus:
return "Premium/Perk/Status"
case .translation:
return "Premium/Perk/Translation"
case .stories:
return "Premium/Perk/Translation"
}
}
}
@ -1714,6 +1733,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
demoSubject = .emojiStatus
case .translation:
demoSubject = .translation
case .stories:
demoSubject = .stories
}
let isPremium = state?.isPremium == true

View File

@ -888,7 +888,7 @@ final class MediaEditorScreenComponent: Component {
timeoutSelected = false
var inputPanelAvailableWidth = previewSize.width
var inputPanelAvailableHeight = 115.0
var inputPanelAvailableHeight = 103.0
if case .regular = environment.metrics.widthClass {
if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) {
inputPanelAvailableWidth += 200.0
@ -1547,6 +1547,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
private var isDismissing = false
private var dismissOffset: CGFloat = 0.0
private var isDismissed = false
private var isDismissBySwipeSuppressed = false
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
@ -1955,6 +1956,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if abs(translation.y) > 10.0 && !self.isEnhancing && hasSwipeToDismiss {
if !self.isDismissing {
self.isDismissing = true
self.isDismissBySwipeSuppressed = controller.isEligibleForDraft()
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
}
} else if abs(translation.x) > 10.0 && !self.isDismissing {
@ -1965,6 +1967,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
if self.isDismissing {
self.dismissOffset = translation.y
controller.requestLayout(transition: .immediate)
if abs(self.dismissOffset) > 20.0, controller.isEligibleForDraft() {
gestureRecognizer.isEnabled = false
gestureRecognizer.isEnabled = true
controller.maybePresentDiscardAlert()
}
} else if self.isEnhancing {
if let mediaEditor = self.mediaEditor {
let value = mediaEditor.getToolValue(.enhance) as? Float ?? 0.0
@ -1977,7 +1985,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
case .ended, .cancelled:
if self.isDismissing {
if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0 {
if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0, !controller.isEligibleForDraft() {
controller.requestDismiss(saveDraft: false, animated: true)
} else {
self.dismissOffset = 0.0
@ -2532,7 +2540,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
isInteractingWithEntities: self.isInteractingWithEntities,
isSavingAvailable: controller.isSavingAvailable,
hasAppeared: self.hasAppeared,
isDismissing: self.isDismissing,
isDismissing: self.isDismissing && !self.isDismissBySwipeSuppressed,
bottomSafeInset: layout.intrinsicInsets.bottom,
mediaEditor: self.mediaEditor,
privacy: controller.state.privacy,
@ -2697,7 +2705,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0)
transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing && !self.isDismissBySwipeSuppressed ? 0.0 : 1.0)
var bottomInputOffset: CGFloat = 0.0
if inputHeight > 0.0 {
@ -3042,19 +3050,27 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.present(controller, in: .current)
}
func maybePresentDiscardAlert() {
func isEligibleForDraft() -> Bool {
guard let mediaEditor = self.node.mediaEditor else {
return
return false
}
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
self.hapticFeedback.impact(.light)
if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false {
return false
}
return true
}
func maybePresentDiscardAlert() {
self.hapticFeedback.impact(.light)
if !self.isEligibleForDraft() {
self.requestDismiss(saveDraft: false, animated: true)
return
}
let title: String
let save: String
if case .draft = self.node.subject {

View File

@ -1768,6 +1768,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSource = .fasterDownload
case .translation:
mappedSource = .translation
case .stories:
mappedSource = .stories
}
return PremiumIntroScreen(context: context, source: mappedSource)
}
@ -1803,6 +1805,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mappedSubject = .emojiStatus
case .translation:
mappedSubject = .translation
case .stories:
mappedSubject = .stories
}
return PremiumDemoScreen(context: context, subject: mappedSubject, action: action)
}