From 21f06b62fcc7ea509820254a596f75963a14b129 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 May 2022 16:32:10 +0400 Subject: [PATCH 01/11] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 18 +- .../AttachmentTextInputPanelNode.swift | 27 +- .../Sources/ChatListController.swift | 101 +- .../Sources/ChatListControllerNode.swift | 20 +- .../ChatListFilterPresetController.swift | 34 +- .../ChatListFilterTabContainerNode.swift | 2 +- .../Source/Components/List.swift | 24 +- .../Source/Host/ComponentHostView.swift | 8 +- .../ContextUI/Sources/ContextController.swift | 4 + .../PremiumUI/Sources/DemoComponent.swift | 44 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 892 ++++++++++++++++++ .../Sources/PremiumIntroScreen.swift | 324 ++----- .../Sources/PremiumLimitScreen.swift | 35 +- .../Sources/PremiumReactionsScreen.swift | 272 ------ .../Sources/PremiumStarComponent.swift | 11 +- ...swift => ReactionsCarouselComponent.swift} | 94 +- .../PremiumUI/Sources/RollingCountLabel.swift | 48 +- .../Sources/StickersCarouselComponent.swift | 498 ++++++++++ .../Sources/StickerPreviewPeekContent.swift | 4 +- .../Components/Dots.imageset/Contents.json | 21 + .../Components/Dots.imageset/dots@3x.png | Bin 0 -> 1077 bytes .../Premium/File.imageset/Contents.json | 11 + .../TelegramUI/Sources/ChatController.swift | 21 +- .../WebUI/Sources/WebAppController.swift | 3 + 24 files changed, 1894 insertions(+), 622 deletions(-) create mode 100644 submodules/PremiumUI/Sources/DemoComponent.swift create mode 100644 submodules/PremiumUI/Sources/PremiumDemoScreen.swift delete mode 100644 submodules/PremiumUI/Sources/PremiumReactionsScreen.swift rename submodules/PremiumUI/Sources/{ReactionCarouselNode.swift => ReactionsCarouselComponent.swift} (80%) create mode 100644 submodules/PremiumUI/Sources/StickersCarouselComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/File.imageset/Contents.json diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index af0f80fdd6..2bcef386e5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7541,9 +7541,6 @@ Sorry for the inconvenience."; "Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium."; "Premium.Stickers.Proceed" = "Unlock Premium Stickers"; -"Premium.Reactions.Description" = "Unlock additional reactions by subscribing to Telegram Premium."; -"Premium.Reactions.Proceed" = "Unlock Additional Reactions"; - "AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On."; "Chat.MultipleTypingPair" = "%@ and %@"; @@ -7557,11 +7554,12 @@ Sorry for the inconvenience."; "OldChannels.LeaveCommunities_1" = "Leave %@ Community"; "OldChannels.LeaveCommunities_any" = "Leave %@ Communities"; +"Premium.FileTooLarge" = "File Too Large"; "Premium.LimitReached" = "Limit Reached"; "Premium.IncreaseLimit" = "Increase Limit"; -"Premium.MaxFoldersCountText" = "You have reached the limit of **%@** folders. You can double the limit to **%@** folders by subscribing to **Telegram Premium**."; -"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%@** chats to a folder. You can increase this limit to **%@** by upgrading to **Telegram Premium**."; +"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **Telegram Premium**."; +"Premium.MaxChatsInFolderCountText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **Telegram Premium**."; "Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **Telegram Premium**."; "Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; "Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached"; @@ -7573,6 +7571,12 @@ Sorry for the inconvenience."; "Premium.Title" = "Telegram Premium"; "Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**."; +"Premium.PersonalTitle" = "%@ is a subscriber of Telegram Premium"; +"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features."; + +"Premium.SubscribedTitle" = "You are all set!"; +"Premium.SubscribedDescription" = "Thank you for subsribing to **Telegram Premium**. Here's what is now unlocked."; + "Premium.DoubledLimits" = "Doubled Limits"; "Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more."; @@ -7610,6 +7614,8 @@ Sorry for the inconvenience."; "Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy)."; +"Premium.MoreAboutPremium" = "More About Premium"; + "Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted"; "Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted"; @@ -7622,3 +7628,5 @@ Sorry for the inconvenience."; "Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB."; "SponsoredMessageMenu.Hide" = "Hide"; + +"ChatListFolder.MaxChatsInFolder" = "Sorry, you can't add more than %d chats to a folder."; diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index de43468a11..ba13d770d6 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -244,6 +244,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private var emojiViewProvider: ((String) -> UIView)? + private var maxCaptionLength: Int32? + public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) { self.context = context self.presentationInterfaceState = presentationInterfaceState @@ -323,6 +325,23 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS } self.updateSendButtonEnabled(isCaption || isAttachment, animated: false) + + if self.isCaption || self.isAttachment { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> mapToSignal { peer -> Signal in + if let peer = peer { + return self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits.init(isPremium: peer.isPremium)) + |> map { limits in + return limits.maxCaptionLengthCount + } + } else { + return .complete() + } + } + |> deliverOnMainQueue).start(next: { [weak self] maxCaptionLength in + self?.maxCaptionLength = maxCaptionLength + }) + } } public var sendPressed: ((NSAttributedString?) -> Void)? @@ -931,8 +950,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { let inputTextMaxLength: Int32? - if self.isCaption || self.isAttachment { - inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength + if let maxCaptionLength = self.maxCaptionLength { + inputTextMaxLength = maxCaptionLength } else { inputTextMaxLength = nil } @@ -1301,8 +1320,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS @objc func sendButtonPressed() { let inputTextMaxLength: Int32? - if self.isCaption || self.isAttachment { - inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength + if let maxCaptionLength = self.maxCaptionLength { + inputTextMaxLength = maxCaptionLength } else { inputTextMaxLength = nil } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 34a5f5ec82..09aa6e88f6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1461,7 +1461,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController tabContextGesture(id, sourceNode, gesture, true) } - self.ready.set(self.chatListDisplayNode.containerNode.ready) + if case .group = self.groupId { + self.ready.set(self.chatListDisplayNode.containerNode.ready) + } else { + self.ready.set(.never()) + } self.displayNodeDidLoad() } @@ -2060,10 +2064,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) - if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { - strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil) + if !strongSelf.initializedFilters { + if selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { + strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + } + }) + } else { + strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + } + strongSelf.initializedFilters = true } - strongSelf.initializedFilters = true + let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom @@ -3236,15 +3249,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let _ = (combineLatest(queue: .mainQueue(), self.context.engine.peers.currentChatListFilters(), chatListFilterItems(context: self.context) - |> take(1) + |> take(1), + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ) ) - |> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount in + |> deliverOnMainQueue).start(next: { [weak self] presetList, filterItemsAndTotalCount, result in guard let strongSelf = self else { return } - let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start() + let (accountPeer, limits, _) = result + let isPremium = accountPeer?.isPremium ?? false + + let _ = strongSelf.context.engine.peers.markChatListFeaturedFiltersAsSeen().start() let (_, filterItems) = filterItemsAndTotalCount var items: [ContextMenuItem] = [] @@ -3272,11 +3293,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if !presetList.isEmpty { - items.append(.separator) - + if presetList.count > 1 { + items.append(.separator) + } + var filterCount = 0 for case let .filter(id, title, _, data) in presetList { let filterType = chatListFilterType(data) var badge: ContextMenuActionBadge? + var isDisabled = false + if !isPremium && filterCount >= limits.maxFoldersCount { + isDisabled = true + } + for item in filterItems { if item.0.id == id && item.1 != 0 { badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) @@ -3284,23 +3312,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in let imageName: String - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" + if isDisabled { + imageName = "Chat/Context Menu/Lock" + } else { + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } } return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) }, action: { _, f in @@ -3308,8 +3340,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.selectTab(id: .filter(id)) + if isDisabled { + let context = strongSelf.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.tabContainerNode.filtersCount, action: { + let controller = PremiumIntroScreen(context: context, source: .folders) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + strongSelf.push(controller) + } else { + strongSelf.selectTab(id: .filter(id)) + } }))) + + filterCount += 1 } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index a3e3e0eb26..2ad548a7ca 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -878,20 +878,28 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) - disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady) - |> filter { $0 && $1 } + disposable.set((itemNode.listNode.ready |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else { return } - guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else { - return - } + strongSelf.pendingItemNode = nil - let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate + guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = strongSelf.validLayout else { + strongSelf.itemNodes[id] = itemNode + strongSelf.addSubnode(itemNode) + + strongSelf.selectedId = id + strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode) + strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, .immediate, false) + + completion?() + return + } + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) { let previousId = strongSelf.selectedId let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0 diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 123e201b0a..86e515a944 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -601,14 +601,22 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100)) controller.navigationPresentation = .modal - let _ = (controller.result - |> take(1) - |> deliverOnMainQueue).start(next: { [weak controller] result in + let _ = combineLatest( + queue: Queue.mainQueue(), + controller.result |> take(1), + context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ) + ) + .start(next: { [weak controller] result, data in guard case let .result(peerIds, additionalCategoryIds) = result else { controller?.dismiss() return } + let (limits, premiumLimits) = data + var includePeers: [PeerId] = [] for peerId in peerIds { switch peerId { @@ -620,6 +628,26 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } includePeers.sort() + if includePeers.count > limits.maxFolderChatsCount { + if includePeers.count > premiumLimits.maxFolderChatsCount { + let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.ChatListFolder_MaxChatsInFolder(Int(premiumLimits.maxFolderChatsCount)).string, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + controller?.present(alertController, in: .window(.root)) + return + } + + var replaceImpl: ((ViewController) -> Void)? + let limitController = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(includePeers.count), action: { + let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(introController) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + controller?.push(limitController) + + return + } + var categories: ChatListFilterPeerCategories = [] for id in additionalCategoryIds { if let index = categoryMapping.firstIndex(where: { $0.1.rawValue == id }) { diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 3c2f5af173..eea4b14b53 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -307,7 +307,7 @@ private final class ItemNode: ASDisplayNode { self.badgeTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((badgeBackgroundFrame.width - badgeSize.width) / 2.0), y: floor((badgeBackgroundFrame.height - badgeSize.height) / 2.0)), size: badgeSize) let width: CGFloat - if self.unreadCount == 0 || self.isReordering || self.isEditing { + if self.unreadCount == 0 || self.isReordering || self.isEditing || self.isDisabled { if !self.isReordering { self.badgeContainerNode.alpha = 0.0 } diff --git a/submodules/ComponentFlow/Source/Components/List.swift b/submodules/ComponentFlow/Source/Components/List.swift index b73b6d7efd..be3eeef286 100644 --- a/submodules/ComponentFlow/Source/Components/List.swift +++ b/submodules/ComponentFlow/Source/Components/List.swift @@ -2,13 +2,20 @@ import Foundation import UIKit public final class List: CombinedComponent { + public enum Direction { + case horizontal + case vertical + } + public typealias EnvironmentType = ChildEnvironment private let items: [AnyComponentWithIdentity] + private let direction: Direction private let appear: Transition.Appear - public init(_ items: [AnyComponentWithIdentity], appear: Transition.Appear = .default()) { + public init(_ items: [AnyComponentWithIdentity], direction: Direction = .vertical, appear: Transition.Appear = .default()) { self.items = items + self.direction = direction self.appear = appear } @@ -16,6 +23,9 @@ public final class List: CombinedComponent { if lhs.items != rhs.items { return false } + if lhs.direction != rhs.direction { + return false + } return true } @@ -35,11 +45,19 @@ public final class List: CombinedComponent { var nextOrigin: CGFloat = 0.0 for child in updatedChildren { + let position: CGPoint + switch context.component.direction { + case .horizontal: + position = CGPoint(x: nextOrigin + child.size.width / 2.0, y: child.size.height / 2.0) + nextOrigin += child.size.width + case .vertical: + position = CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0) + nextOrigin += child.size.height + } context.add(child - .position(CGPoint(x: child.size.width / 2.0, y: nextOrigin + child.size.height / 2.0)) + .position(position) .appear(context.component.appear) ) - nextOrigin += child.size.height } return context.availableSize diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index 22e64ba3a1..3c4698a38e 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -71,9 +71,7 @@ public final class ComponentHostView: UIView { } let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated() - if isEnvironmentUpdated { - context.erasedEnvironment._isUpdated = false - } + if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize { if currentContainerSize == containerSize && currentComponent == component { @@ -98,6 +96,10 @@ public final class ComponentHostView: UIView { transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize)) } + if isEnvironmentUpdated { + context.erasedEnvironment._isUpdated = false + } + self.isUpdating = false return updatedSize diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index ffb2f11e73..97bb0837db 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -2437,6 +2437,10 @@ public final class ContextController: ViewController, StandalonePresentableContr self.dismiss(result: .default, completion: completion) } + public func dismissWithoutContent() { + self.dismiss(result: .dismissWithoutContent, completion: nil) + } + public func dismissNow() { self.presentingViewController?.dismiss(animated: false, completion: nil) self.dismissed?() diff --git a/submodules/PremiumUI/Sources/DemoComponent.swift b/submodules/PremiumUI/Sources/DemoComponent.swift new file mode 100644 index 0000000000..8235b3d9a0 --- /dev/null +++ b/submodules/PremiumUI/Sources/DemoComponent.swift @@ -0,0 +1,44 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import ComponentFlow +import AccountContext + +final class DemoComponent: Component { + public typealias EnvironmentType = DemoPageEnvironment + + let context: AccountContext + + public init( + context: AccountContext + ) { + self.context = context + } + + public static func ==(lhs: DemoComponent, rhs: DemoComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + public final class View: UIView { + private var component: DemoComponent? + + public func update(component: DemoComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift new file mode 100644 index 0000000000..0f46a42536 --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -0,0 +1,892 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown + +private final class GradientBackgroundComponent: Component { + public let colors: [UIColor] + + public init( + colors: [UIColor] + ) { + self.colors = colors + } + + public static func ==(lhs: GradientBackgroundComponent, rhs: GradientBackgroundComponent) -> Bool { + if lhs.colors != rhs.colors { + return false + } + return true + } + + public final class View: UIView { + private let clipLayer: CALayer + private let gradientLayer: CAGradientLayer + + private var component: GradientBackgroundComponent? + + override init(frame: CGRect) { + self.clipLayer = CALayer() + self.clipLayer.cornerRadius = 10.0 + self.clipLayer.masksToBounds = true + + self.gradientLayer = CAGradientLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.clipLayer) + self.clipLayer.addSublayer(gradientLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + func update(component: GradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.clipLayer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 10.0)) + self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize) + + var locations: [NSNumber] = [] + let delta = 1.0 / CGFloat(component.colors.count - 1) + for i in 0 ..< component.colors.count { + locations.append((delta * CGFloat(i)) as NSNumber) + } + self.gradientLayer.locations = locations + self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor } + self.gradientLayer.type = .radial + self.gradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.gradientLayer.endPoint = CGPoint(x: -2.0, y: 3.0) + + self.component = component + + self.setupGradientAnimations() + + return availableSize + } + + private func setupGradientAnimations() { + if let _ = self.gradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.gradientLayer.endPoint + let value: CGFloat + if previousValue.x < -0.5 { + value = 0.5 + } else { + value = 2.0 + } + let newValue = CGPoint(x: -value, y: 1.0 + value) +// let secondNewValue = CGPoint(x: 3.0 - value, y: -2.0 + value) + self.gradientLayer.endPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "endPoint") + animation.duration = 4.5 + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + self?.setupGradientAnimations() + } + + self.gradientLayer.add(animation, forKey: "movement") + +// let secondPreviousValue = self.gradientLayer.startPoint +// let secondAnimation = CABasicAnimation(keyPath: "startPoint") +// secondAnimation.duration = 4.5 +// secondAnimation.fromValue = secondPreviousValue +// secondAnimation.toValue = secondNewValue +// +// self.gradientLayer.add(secondAnimation, forKey: "movement2") + + CATransaction.commit() + } + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class DemoPageEnvironment: Equatable { + public let isDisplaying: Bool + + public init(isDisplaying: Bool) { + self.isDisplaying = isDisplaying + } + + public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool { + if lhs.isDisplaying != rhs.isDisplaying { + return false + } + return true + } +} + +private final class PageComponent: CombinedComponent { + typealias EnvironmentType = ChildEnvironment + + private let content: AnyComponent + private let title: String + private let text: String + private let textColor: UIColor + + init( + content: AnyComponent, + title: String, + text: String, + textColor: UIColor + ) { + self.content = content + self.title = title + self.text = text + self.textColor = textColor + } + + static func ==(lhs: PageComponent, rhs: PageComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + return true + } + + static var body: Body { + let children = ChildMap(environment: ChildEnvironment.self, keyedBy: AnyHashable.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + + return { context in + let availableSize = context.availableSize + let component = context.component + + let sideInset: CGFloat = 16.0 //+ environment.safeInsets.left + let textSideInset: CGFloat = 24.0 //+ environment.safeInsets.left + + let textColor = component.textColor + let textFont = Font.regular(17.0) + let boldTextFont = Font.semibold(17.0) + + let content = children["main"].update( + component: component.content, + environment: { + context.environment[ChildEnvironment.self] + }, + availableSize: CGSize(width: availableSize.width, height: availableSize.width), + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: boldTextFont, + textColor: component.textColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in + return nil + }) + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.0 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 40.0)) + ) + context.add(text + .position(CGPoint(x: context.availableSize.width / 2.0, y: content.size.height + 80.0)) + ) + context.add(content + .position(CGPoint(x: content.size.width / 2.0, y: content.size.height / 2.0)) + ) + + return availableSize + } + } +} + +private final class DemoPagerComponent: Component { + public final class Item: Equatable { + public let content: AnyComponentWithIdentity + + public init(_ content: AnyComponentWithIdentity) { + self.content = content + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.content != rhs.content { + return false + } + + return true + } + } + + public let items: [Item] + public let index: Int + + public init( + items: [Item], + index: Int = 0 + ) { + self.items = items + self.index = index + } + + public static func ==(lhs: DemoPagerComponent, rhs: DemoPagerComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + private var itemViews: [AnyHashable: ComponentHostView] = [:] + + private var component: DemoPagerComponent? + + override init(frame: CGRect) { + self.scrollView = UIScrollView(frame: frame) + self.scrollView.isPagingEnabled = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.bounces = false + self.scrollView.layer.cornerRadius = 10.0 + + super.init(frame: frame) + + self.scrollView.delegate = self + + self.addSubview(self.scrollView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard let component = self.component else { + return + } + + for item in component.items { + if let itemView = self.itemViews[item.content.id] { + let isDisplaying = itemView.frame.intersects(self.scrollView.bounds) + + let environment = DemoPageEnvironment(isDisplaying: isDisplaying) + let _ = itemView.update( + transition: .immediate, + component: item.content.component, + environment: { environment }, + containerSize: self.bounds.size + ) + } + } + } + + func update(component: DemoPagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var validIds: [AnyHashable] = [] + + let firstTime = self.itemViews.isEmpty + + let contentSize = CGSize(width: availableSize.width * CGFloat(component.items.count), height: availableSize.height) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + self.scrollView.frame = CGRect(origin: .zero, size: availableSize) + + if firstTime { + self.scrollView.contentOffset = CGPoint(x: CGFloat(component.index) * availableSize.width, y: 0.0) + } + + var i = 0 + for item in component.items { + validIds.append(item.content.id) + + let itemView: ComponentHostView + var itemTransition = transition + + if let current = self.itemViews[item.content.id] { + itemView = current + } else { + itemTransition = transition.withAnimation(.none) + itemView = ComponentHostView() + self.itemViews[item.content.id] = itemView + self.scrollView.addSubview(itemView) + } + + + let isDisplaying = itemView.frame.intersects(self.scrollView.bounds) + let environment = DemoPageEnvironment(isDisplaying: isDisplaying) + let itemSize = itemView.update( + transition: itemTransition, + component: item.content.component, + environment: { environment }, + containerSize: availableSize + ) + + itemView.frame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: itemSize) + + i += 1 + } + + var removeIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removeIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removeIds { + self.itemViews.removeValue(forKey: id) + } + + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class DemoSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: PremiumDemoScreen.Subject + let source: PremiumDemoScreen.Source + let action: () -> Void + let dismiss: () -> Void + + init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, action: @escaping () -> Void, dismiss: @escaping () -> Void) { + self.context = context + self.subject = subject + self.source = source + self.action = action + self.dismiss = dismiss + } + + static func ==(lhs: DemoSheetContent, rhs: DemoSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.source != rhs.source { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + var cachedCloseImage: UIImage? + + var reactions: [AvailableReactions.Reaction]? + var stickers: [TelegramMediaFile]? + var reactionsDisposable: Disposable? + var stickersDisposable: Disposable? + + init(context: AccountContext) { + self.context = context + + super.init() + + self.reactionsDisposable = (self.context.engine.stickers.availableReactions() + |> map { reactions -> [AvailableReactions.Reaction] in + if let reactions = reactions { + return reactions.reactions.filter { $0.isPremium } + } else { + return [] + } + } + |> deliverOnMainQueue).start(next: { [weak self] reactions in + guard let strongSelf = self else { + return + } + strongSelf.reactions = reactions + strongSelf.updated(transition: .immediate) + }) + + self.stickersDisposable = (self.context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.PremiumStickers], namespaces: [Namespaces.ItemCollection.CloudDice], aroundIndex: nil, count: 100) + |> map { view -> [TelegramMediaFile] in + var result: [TelegramMediaFile] = [] + if let premiumStickers = view.orderedItemListsViews.first { + for i in 0 ..< premiumStickers.items.count { + if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) { + result.append(item.media) + } + } + } + return result + } + |> deliverOnMainQueue).start(next: { [weak self] stickers in + guard let strongSelf = self else { + return + } + strongSelf.stickers = stickers + strongSelf.updated(transition: .immediate) + }) + } + + deinit { + self.reactionsDisposable?.dispose() + self.stickersDisposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let closeButton = Child(Button.self) + let background = Child(GradientBackgroundComponent.self) + let pager = Child(DemoPagerComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + let dots = Child(BundleIconComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + + let state = context.state + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let background = background.update( + component: GradientBackgroundComponent(colors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ]), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let closeImage: UIImage + if let image = state.cachedCloseImage { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: UIColor(rgb: 0xffffff))! + state.cachedCloseImage = closeImage + } + + if let reactions = state.reactions, let stickers = state.stickers { + let textColor = theme.actionSheet.primaryTextColor + + let items: [DemoPagerComponent.Item] = [ + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.moreUpload, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_UploadSize, + text: strings.Premium_UploadSizeInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.fasterDownload, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_FasterSpeed, + text: strings.Premium_FasterSpeedInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.voiceToText, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_VoiceToText, + text: strings.Premium_VoiceToTextInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.noAds, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_NoAds, + text: strings.Premium_NoAdsInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.uniqueReactions, + component: AnyComponent( + PageComponent( + content: AnyComponent( + ReactionsCarouselComponent( + context: component.context, + theme: environment.theme, + reactions: reactions + ) + ), + title: strings.Premium_Reactions, + text: strings.Premium_ReactionsInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.premiumStickers, + component: AnyComponent( + PageComponent( + content: AnyComponent( + StickersCarouselComponent( + context: component.context, + stickers: stickers + ) + ), + title: strings.Premium_Stickers, + text: strings.Premium_StickersInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.advancedChatManagement, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_ChatManagement, + text: strings.Premium_ChatManagementInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.profileBadge, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_Badge, + text: strings.Premium_BadgeInfo, + textColor: textColor + ) + ) + ) + ), + DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.animatedUserpics, + component: AnyComponent( + PageComponent( + content: AnyComponent(DemoComponent( + context: component.context + )), + title: strings.Premium_Avatar, + text: strings.Premium_AvatarInfo, + textColor: textColor + ) + ) + ) + ) + ] + let index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0 + + let pager = pager.update( + component: DemoPagerComponent( + items: items, + index: index + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width + 154.0), + transition: .immediate + ) + context.add(pager + .position(CGPoint(x: context.availableSize.width / 2.0, y: pager.size.height / 2.0)) + ) + } + + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { [weak component] in + component?.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let buttonText: String + switch component.source { + case let .intro(price): + buttonText = strings.Premium_SubscribeFor(price ?? "–").string + case .other: + buttonText = strings.Premium_MoreAboutPremium + } + + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: .black, + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], + foregroundColor: .white + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: true, + iconPosition: .right, + action: { [weak component] in + guard let component = component else { + return + } + component.dismiss() + component.action() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: context.availableSize.width + 154.0 + 20.0), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + let dots = dots.update( + component: BundleIconComponent(name: "Components/Dots", tintColor: nil), + availableSize: CGSize(width: 110.0, height: 20.0), + transition: .immediate + ) + context.add(dots + .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - dots.size.height - 18.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + + +private final class DemoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: PremiumDemoScreen.Subject + let source: PremiumDemoScreen.Source + let action: () -> Void + + init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source, action: @escaping () -> Void) { + self.context = context + self.subject = subject + self.source = source + self.action = action + } + + static func ==(lhs: DemoSheetComponent, rhs: DemoSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + if lhs.source != rhs.source { + return false + } + + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(DemoSheetContent( + context: context.component.context, + subject: context.component.subject, + source: context.component.source, + action: context.component.action, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class PremiumDemoScreen: ViewControllerComponentContainer { + public enum Subject { + case moreUpload + case fasterDownload + case voiceToText + case noAds + case uniqueReactions + case premiumStickers + case advancedChatManagement + case profileBadge + case animatedUserpics + } + + public enum Source: Equatable { + case intro(String?) + case other + } + + var disposed: () -> Void = {} + + public init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, action: @escaping () -> Void) { + super.init(context: context, component: DemoSheetComponent(context: context, subject: subject, source: source, action: action), navigationBarAppearance: .none) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } +} + diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 928d69fc12..6e76b9bf09 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -718,15 +718,26 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) let context: AccountContext + let price: String? + let present: (ViewController) -> Void + let buy: () -> Void + let updateIsFocused: (Bool) -> Void - init(context: AccountContext) { + init(context: AccountContext, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { self.context = context + self.price = price + self.present = present + self.buy = buy + self.updateIsFocused = updateIsFocused } static func ==(lhs: PremiumIntroScreenContentComponent, rhs: PremiumIntroScreenContentComponent) -> Bool { if lhs.context !== rhs.context { return false } + if lhs.price != rhs.price { + return false + } return true } @@ -862,6 +873,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var items: [SectionGroupComponent.Item] = [] + let accountContext = context.component.context + let present = context.component.present + let buy = context.component.buy + let updateIsFocused = context.component.updateIsFocused + let price = context.component.price var i = 0 for perk in state.configuration.perks { let iconBackgroundColors = gradientColors[i] @@ -884,7 +900,48 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ) ), action: { + var demoSubject: PremiumDemoScreen.Subject + switch perk { + case .doubleLimits: + return + case .moreUpload: + demoSubject = .moreUpload + case .fasterDownload: + demoSubject = .fasterDownload + case .voiceToText: + demoSubject = .voiceToText + case .noAds: + demoSubject = .noAds + case .uniqueReactions: + demoSubject = .uniqueReactions + case .premiumStickers: + demoSubject = .premiumStickers + case .advancedChatManagement: + demoSubject = .advancedChatManagement + case .profileBadge: + demoSubject = .profileBadge + case .animatedUserpics: + demoSubject = .animatedUserpics + } + var dismissImpl: (() -> Void)? + let controller = PremiumDemoScreen( + context: accountContext, + subject: demoSubject, + source: .intro(price), + action: { + dismissImpl?() + buy() + } + ) + controller.disposed = { + updateIsFocused(false) + } + present(controller) + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true, completion: nil) + } + updateIsFocused(true) } )) i += 1 @@ -901,241 +958,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), transition: context.transition ) - -// - -// let section = section.update( -// component: SectionGroupComponent( -// items: [ -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "limits", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Limits", -// iconBackgroundColors: [ -// UIColor(rgb: 0xF28528), -// UIColor(rgb: 0xEF7633) -// ], -// title: strings.Premium_DoubledLimits, -// titleColor: titleColor, -// subtitle: strings.Premium_DoubledLimitsInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "upload", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Upload", -// iconBackgroundColors: [ -// UIColor(rgb: 0xEA5F43), -// UIColor(rgb: 0xE7504E) -// ], -// title: strings.Premium_UploadSize, -// titleColor: titleColor, -// subtitle: strings.Premium_UploadSizeInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "speed", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Speed", -// iconBackgroundColors: [ -// UIColor(rgb: 0xDE4768), -// UIColor(rgb: 0xD54D82) -// ], -// title: strings.Premium_FasterSpeed, -// titleColor: titleColor, -// subtitle: strings.Premium_FasterSpeedInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "voice", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Voice", -// iconBackgroundColors: [ -// UIColor(rgb: 0xDE4768), -// UIColor(rgb: 0xD54D82) -// ], -// title: strings.Premium_VoiceToText, -// titleColor: titleColor, -// subtitle: strings.Premium_VoiceToTextInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "noAds", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/NoAds", -// iconBackgroundColors: [ -// UIColor(rgb: 0xC654A8), -// UIColor(rgb: 0xBE5AC2) -// ], -// title: strings.Premium_NoAds, -// titleColor: titleColor, -// subtitle: strings.Premium_NoAdsInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "reactions", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Reactions", -// iconBackgroundColors: [ -// UIColor(rgb: 0xAF62E9), -// UIColor(rgb: 0xA668FF) -// ], -// title: strings.Premium_Reactions, -// titleColor: titleColor, -// subtitle: strings.Premium_ReactionsInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "stickers", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Stickers", -// iconBackgroundColors: [ -// UIColor(rgb: 0x9674FF), -// UIColor(rgb: 0x8C7DFF) -// ], -// title: strings.Premium_Stickers, -// titleColor: titleColor, -// subtitle: strings.Premium_StickersInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "chat", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Chat", -// iconBackgroundColors: [ -// UIColor(rgb: 0x9674FF), -// UIColor(rgb: 0x8C7DFF) -// ], -// title: strings.Premium_ChatManagement, -// titleColor: titleColor, -// subtitle: strings.Premium_ChatManagementInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "badge", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Badge", -// iconBackgroundColors: [ -// UIColor(rgb: 0x7B88FF), -// UIColor(rgb: 0x7091FF) -// ], -// title: strings.Premium_Badge, -// titleColor: titleColor, -// subtitle: strings.Premium_BadgeInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// SectionGroupComponent.Item( -// AnyComponentWithIdentity( -// id: "avatar", -// component: AnyComponent( -// PerkComponent( -// iconName: "Premium/Perk/Avatar", -// iconBackgroundColors: [ -// UIColor(rgb: 0x609DFF), -// UIColor(rgb: 0x56A5FF) -// ], -// title: strings.Premium_Avatar, -// titleColor: titleColor, -// subtitle: strings.Premium_AvatarInfo, -// subtitleColor: subtitleColor, -// arrowColor: arrowColor -// ) -// ) -// ), -// action: { -// -// } -// ), -// ], -// backgroundColor: environment.theme.list.itemBlocksBackgroundColor, -// selectionColor: environment.theme.list.itemHighlightedBackgroundColor, -// separatorColor: environment.theme.list.itemBlocksSeparatorColor -// ), -// environment: {}, -// availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), -// transition: context.transition -// ) context.add(section .position(CGPoint(x: availableWidth / 2.0, y: size.height + section.size.height / 2.0)) .clipsToBounds(true) @@ -1315,11 +1137,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let context: AccountContext let updateInProgress: (Bool) -> Void + let present: (ViewController) -> Void let completion: () -> Void - init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) { + init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { self.context = context self.updateInProgress = updateInProgress + self.present = present self.completion = completion } @@ -1338,6 +1162,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? + var hasIdleAnimations = true + var inProgress = false var premiumProduct: InAppPurchaseManager.Product? private var disposable: Disposable? @@ -1398,6 +1224,11 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } })) } + + func updateIsFocused(_ isFocused: Bool) { + self.hasIdleAnimations = !isFocused + self.updated(transition: .immediate) + } } func makeState() -> State { @@ -1427,7 +1258,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let star = star.update( - component: PremiumStarComponent(isVisible: starIsVisible), + component: PremiumStarComponent(isVisible: starIsVisible, hasIdleAnimations: state.hasIdleAnimations), availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0), transition: context.transition ) @@ -1504,7 +1335,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( - context: context.component.context + context: context.component.context, + price: state.premiumProduct?.price, + present: context.component.present, + buy: { [weak state] in + state?.buy() + }, updateIsFocused: { [weak state] isFocused in + state?.updateIsFocused(isFocused) + } )), contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in @@ -1610,12 +1448,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { self.context = context var updateInProgressImpl: ((Bool) -> Void)? + var presentImpl: ((ViewController) -> Void)? var completionImpl: (() -> Void)? super.init(context: context, component: PremiumIntroScreenComponent( context: context, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, + present: { c in + presentImpl?(c) + }, completion: { completionImpl?() } @@ -1639,6 +1481,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } } + presentImpl = { [weak self] c in + self?.push(c) + } + completionImpl = { [weak self] in if let strongSelf = self { strongSelf.view.addSubview(ConfettiView(frame: strongSelf.view.bounds)) diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index fdb6bea25e..15c8ae590c 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -16,16 +16,16 @@ import BundleIconComponent import SolidRoundedButtonComponent import Markdown -private func generateCloseButtonImage(theme: PresentationTheme) -> UIImage? { +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) + context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) - context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) + context.setStrokeColor(foregroundColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) @@ -143,7 +143,7 @@ private class PremiumLimitAnimationComponent: Component { self.badgeCountLabel = RollingLabel() self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) self.badgeCountLabel.textColor = .white - self.badgeCountLabel.text(num: 0) + self.badgeCountLabel.configure(with: "0") super.init(frame: frame) @@ -203,8 +203,8 @@ private class PremiumLimitAnimationComponent: Component { self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") self.badgeView.layer.add(returnAnimation, forKey: "appearance3") - if let badgeText = component.badgeText, let num = Int(badgeText) { - self.badgeCountLabel.text(num: num) + if let badgeText = component.badgeText { + self.badgeCountLabel.configure(with: badgeText) } } @@ -251,7 +251,18 @@ private class PremiumLimitAnimationComponent: Component { self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0)) self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize) - self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0) + if component.badgePosition > 1.0 - .ulpOfOne { + let offset = badgeWidth / 2.0 - 16.0 + self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition - offset, y: 82.0) + self.badgeMaskArrowView.frame = self.badgeMaskArrowView.frame.offsetBy(dx: offset - 18.0, dy: 0.0) + } else { + self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0) + + if self.badgeView.frame.maxX > availableSize.width { + let delta = self.badgeView.frame.maxX - availableSize.width - 6.0 + self.badgeView.center = self.badgeView.center.offsetBy(dx: -delta, dy: 0.0) + } + } self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0) @@ -616,7 +627,7 @@ private final class LimitSheetContent: CombinedComponent { if let (image, theme) = state.cachedCloseImage, theme === environment.theme { closeImage = image } else { - closeImage = generateCloseButtonImage(theme: theme)! + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! state.cachedCloseImage = (closeImage, theme) } @@ -634,6 +645,7 @@ private final class LimitSheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) ) + var titleText = strings.Premium_LimitReached let iconName: String let badgeText: String let string: String @@ -669,20 +681,21 @@ private final class LimitSheetContent: CombinedComponent { premiumValue = "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .files: - let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 - let premiumLimit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 + let premiumLimit = Int64(state.premiumLimits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 iconName = "Premium/File" badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string defaultValue = "" premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) badgePosition = 0.5 + titleText = strings.Premium_FileTooLarge } let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( - string: strings.Premium_LimitReached, + string: titleText, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center diff --git a/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift b/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift deleted file mode 100644 index 79f2703b7d..0000000000 --- a/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift +++ /dev/null @@ -1,272 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import Postbox -import TelegramCore -import SwiftSignalKit -import AccountContext -import TelegramPresentationData -import PresentationDataUtils -import SolidRoundedButtonNode -import AppBundle - -public final class PremiumReactionsScreen: ViewController { - private let context: AccountContext - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - private let updatedPresentationData: (initial: PresentationData, signal: Signal)? - private let reactions: [AvailableReactions.Reaction] - - public var proceed: (() -> Void)? - - private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { - private weak var controller: PremiumReactionsScreen? - private var presentationData: PresentationData - - private let blurView: UIVisualEffectView - private let vibrancyView: UIVisualEffectView - private let dimNode: ASDisplayNode - private let darkDimNode: ASDisplayNode - private let containerNode: ASDisplayNode - - private let textNode: ImmediateTextNode - private let overlayTextNode: ImmediateTextNode - private let proceedButton: SolidRoundedButtonNode - private let cancelButton: HighlightableButtonNode - private let carouselNode: ReactionCarouselNode - - private var validLayout: ContainerViewLayout? - - init(controller: PremiumReactionsScreen) { - self.controller = controller - self.presentationData = controller.presentationData - - self.dimNode = ASDisplayNode() - let blurEffect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light) - self.blurView = UIVisualEffectView(effect: blurEffect) - self.blurView.isUserInteractionEnabled = false - - self.vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect)) - - self.darkDimNode = ASDisplayNode() - self.darkDimNode.alpha = 0.0 - self.darkDimNode.backgroundColor = self.presentationData.theme.contextMenu.dimColor - self.darkDimNode.isUserInteractionEnabled = false - - self.dimNode.backgroundColor = UIColor(white: self.presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.5) - - self.containerNode = ASDisplayNode() - - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.textAlignment = .center - self.textNode.maximumNumberOfLines = 0 - self.textNode.lineSpacing = 0.1 - - self.overlayTextNode = ImmediateTextNode() - self.overlayTextNode.displaysAsynchronously = false - self.overlayTextNode.textAlignment = .center - self.overlayTextNode.maximumNumberOfLines = 0 - self.overlayTextNode.lineSpacing = 0.1 - - self.proceedButton = SolidRoundedButtonNode(title: self.presentationData.strings.Premium_Reactions_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme( - backgroundColor: .white, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) - - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) - - self.carouselNode = ReactionCarouselNode(context: controller.context, theme: controller.presentationData.theme, reactions: controller.reactions) - - super.init() - - self.addSubnode(self.dimNode) - self.addSubnode(self.darkDimNode) - self.addSubnode(self.containerNode) - - self.containerNode.addSubnode(self.proceedButton) - self.containerNode.addSubnode(self.cancelButton) - self.addSubnode(self.carouselNode) - - let textColor: UIColor - if self.presentationData.theme.overallDarkAppearance { - textColor = UIColor(white: 1.0, alpha: 1.0) - self.overlayTextNode.alpha = 0.2 - self.addSubnode(self.overlayTextNode) - } else { - textColor = self.presentationData.theme.contextMenu.secondaryColor - } - self.textNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor) - self.overlayTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor) - - self.proceedButton.pressed = { [weak self] in - if let strongSelf = self, let controller = strongSelf.controller, let navigationController = controller.navigationController { - strongSelf.animateOut() - navigationController.pushViewController(PremiumIntroScreen(context: controller.context, source: .reactions), animated: true) - } - } - - self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - } - - override func didLoad() { - super.didLoad() - - self.view.insertSubview(self.blurView, aboveSubview: self.dimNode.view) - self.blurView.contentView.addSubview(self.vibrancyView) - - self.vibrancyView.contentView.addSubview(self.textNode.view) - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:)))) - } - - func updatePresentationData(_ presentationData: PresentationData) { - self.presentationData = presentationData - - self.dimNode.backgroundColor = UIColor(white: self.presentationData.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.5) - self.darkDimNode.backgroundColor = self.presentationData.theme.contextMenu.dimColor - self.blurView.effect = UIBlurEffect(style: self.presentationData.theme.overallDarkAppearance ? .dark : .light) - } - - func animateIn() { - self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.blurView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - - self.carouselNode.layer.animatePosition(from: CGPoint(x: 312.0, y: 252.0), to: self.carouselNode.position, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring) - self.carouselNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring) - self.carouselNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.carouselNode.animateIn() - - self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.containerNode.layer.animateScale(from: 0.95, to: 1.0, duration: 0.3) - - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.vibrancyView.layer.animateScale(from: 0.95, to: 1.0, duration: 0.3) - } - - func animateOut() { - self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.carouselNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.carouselNode.animateOut() - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.blurView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.controller?.dismiss(animated: false, completion: nil) - } - }) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.validLayout = layout - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.darkDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(view: self.blurView, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(view: self.vibrancyView, frame: CGRect(origin: CGPoint(), size: layout.size)) - transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - - let carouselFrame = CGRect(origin: CGPoint(x: 0.0, y: 100.0), size: CGSize(width: layout.size.width, height: layout.size.width)) - self.carouselNode.updateLayout(size: carouselFrame.size, transition: transition) - transition.updateFrame(node: self.carouselNode, frame: carouselFrame) - - let sideInset: CGFloat = 16.0 - - let cancelSize = self.cancelButton.measure(layout.size) - self.cancelButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - cancelSize.width) / 2.0), y: layout.size.height - cancelSize.height - 49.0), size: cancelSize) - - let buttonWidth = layout.size.width - sideInset * 2.0 - let buttonHeight = self.proceedButton.updateLayout(width: buttonWidth, transition: transition) - self.proceedButton.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - cancelSize.height - 49.0 - buttonHeight - 23.0), size: CGSize(width: buttonWidth, height: buttonHeight)) - - let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 5.0, height: CGFloat.greatestFiniteMagnitude)) - let _ = self.overlayTextNode.updateLayout(CGSize(width: layout.size.width - sideInset * 5.0, height: CGFloat.greatestFiniteMagnitude)) - let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textSize.width) / 2.0), y: layout.size.height - cancelSize.height - 48.0 - buttonHeight - 20.0 - textSize.height - 31.0), size: textSize) - self.textNode.frame = textFrame - self.overlayTextNode.frame = textFrame - } - - - @objc private func dimNodeTap(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancelPressed() - } - } - - @objc private func cancelPressed() { - self.animateOut() - } - } - - private var controllerNode: Node { - return self.displayNode as! Node - } - - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, reactions: [AvailableReactions.Reaction]) { - self.context = context - self.reactions = reactions - - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - self.presentationData = presentationData - self.updatedPresentationData = updatedPresentationData - - super.init(navigationBarPresentationData: nil) - - self.navigationPresentation = .flatModal - - self.statusBar.statusBarStyle = .Ignore - - self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) - - self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.controllerNode.updatePresentationData(strongSelf.presentationData) - } - } - }) - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - override public func loadDisplayNode() { - self.displayNode = Node(controller: self) - - super.displayNodeDidLoad() - } - - private var didAppear = false - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if !self.didAppear { - self.didAppear = true - self.controllerNode.animateIn() - } - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.controllerNode.containerLayoutUpdated(layout, transition: transition) - } -} diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index ef55e2e26c..a29f49e5bc 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -46,13 +46,15 @@ private func generateDiffuseTexture() -> UIImage { class PremiumStarComponent: Component { let isVisible: Bool + let hasIdleAnimations: Bool - init(isVisible: Bool) { + init(isVisible: Bool, hasIdleAnimations: Bool) { self.isVisible = isVisible + self.hasIdleAnimations = hasIdleAnimations } static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool { - return lhs.isVisible == rhs.isVisible + return lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations } final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -75,6 +77,7 @@ class PremiumStarComponent: Component { private var previousInteractionTimestamp: Double = 0.0 private var timer: SwiftSignalKit.Timer? + private var hasIdleAnimations = false override init(frame: CGRect) { self.sceneView = SCNView(frame: frame) @@ -249,7 +252,7 @@ class PremiumStarComponent: Component { self.previousInteractionTimestamp = CACurrentMediaTime() self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in - if let strongSelf = self { + if let strongSelf = self, strongSelf.hasIdleAnimations { let currentTimestamp = CACurrentMediaTime() if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 { strongSelf.playAppearanceAnimation() @@ -359,6 +362,8 @@ class PremiumStarComponent: Component { self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0)) self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + self.hasIdleAnimations = component.hasIdleAnimations + return availableSize } } diff --git a/submodules/PremiumUI/Sources/ReactionCarouselNode.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift similarity index 80% rename from submodules/PremiumUI/Sources/ReactionCarouselNode.swift rename to submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index cfec7560c9..aae0a7d66f 100644 --- a/submodules/PremiumUI/Sources/ReactionCarouselNode.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -2,15 +2,86 @@ import Foundation import UIKit import Display import AsyncDisplayKit +import ComponentFlow import TelegramCore import AccountContext import ReactionSelectionNode import TelegramPresentationData import AccountContext +final class ReactionsCarouselComponent: Component { + public typealias EnvironmentType = DemoPageEnvironment + + let context: AccountContext + let theme: PresentationTheme + let reactions: [AvailableReactions.Reaction] + + public init( + context: AccountContext, + theme: PresentationTheme, + reactions: [AvailableReactions.Reaction] + ) { + self.context = context + self.theme = theme + self.reactions = reactions + } + + public static func ==(lhs: ReactionsCarouselComponent, rhs: ReactionsCarouselComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.reactions != rhs.reactions { + return false + } + return true + } + + public final class View: UIView { + private var component: ReactionsCarouselComponent? + private var node: ReactionCarouselNode? + + public func update(component: ReactionsCarouselComponent, availableSize: CGSize, transition: Transition) -> CGSize { + if self.node == nil { + let node = ReactionCarouselNode( + context: component.context, + theme: component.theme, + reactions: component.reactions + ) + self.node = node + self.addSubnode(node) + } + + let isFirstTime = self.component == nil + self.component = component + + if let node = self.node { + node.frame = CGRect(origin: CGPoint(x: 0.0, y: -20.0), size: availableSize) + node.updateLayout(size: availableSize, transition: .immediate) + } + + if isFirstTime { + self.node?.animateIn() + } + + return availableSize + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + private let itemSize = CGSize(width: 110.0, height: 110.0) -final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { +private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let theme: PresentationTheme private let reactions: [AvailableReactions.Reaction] @@ -167,7 +238,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { func playReaction() { let delta = self.positionDelta - let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta)))) + let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) guard !self.playingIndices.contains(index) else { return @@ -223,7 +294,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition) } } - + + private let hapticFeedback = HapticFeedback() func scrollViewDidScroll(_ scrollView: UIScrollView) { guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { return @@ -241,10 +313,12 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.currentPosition = updatedPosition let indexDelta = self.positionDelta - let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / indexDelta)))) + let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count) if index != self.currentIndex { self.currentIndex = index - print(index) + if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating { + self.hapticFeedback.tap() + } } if let size = self.validLayout { @@ -272,7 +346,7 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.resetScrollPosition() let delta = self.positionDelta - let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta)))) + let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) self.scrollTo(index, playReaction: true, duration: 0.2) } } @@ -287,14 +361,14 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.scrollNode.frame = CGRect(origin: CGPoint(), size: size) if self.scrollNode.view.contentSize.width.isZero { - self.scrollNode.view.contentSize = CGSize(width: 10000000, height: size.height) + self.scrollNode.view.contentSize = CGSize(width: 10000000.0, height: size.height) self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) self.resetScrollPosition() } let delta = self.positionDelta - let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.5) + let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.45) for i in 0 ..< self.itemNodes.count { let itemNode = self.itemNodes[i] @@ -326,8 +400,8 @@ final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize) containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) - containerNode.position = itemFrame.center - transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45) + containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55) itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size) itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition) diff --git a/submodules/PremiumUI/Sources/RollingCountLabel.swift b/submodules/PremiumUI/Sources/RollingCountLabel.swift index 694ab8030a..48195fe479 100644 --- a/submodules/PremiumUI/Sources/RollingCountLabel.swift +++ b/submodules/PremiumUI/Sources/RollingCountLabel.swift @@ -1,4 +1,5 @@ import UIKit +import Display private extension UILabel { func textWidth() -> CGFloat { @@ -32,22 +33,19 @@ open class RollingLabel: UILabel { private let duration = 1.12 private let durationOffset = 0.2 private let textsNotAnimated = [","] - - public func text(num: Int) { - self.configure(with: num) - self.text = " " - self.animate() + + public func setSuffix(suffix: String) { + self.suffix = suffix } - public func setPrefix(prefix: String) { - self.suffix = prefix - } - - private func configure(with number: Int) { - fullText = String(number) + func configure(with string: String) { + fullText = string clean() setupSubviews() + + self.text = " " + self.animate() } private func animate(ascending: Bool = true) { @@ -99,9 +97,10 @@ open class RollingLabel: UILabel { } stringArray.enumerated().forEach { index, text in - if textsNotAnimated.contains(text) { + let nonDigits = CharacterSet.decimalDigits.inverted + if text.rangeOfCharacter(from: nonDigits) != nil { let label = UILabel() - label.frame.origin = CGPoint(x: x, y: y) + label.frame.origin = CGPoint(x: x, y: y - 1.0 - UIScreenPixel) label.textColor = textColor label.font = font label.text = text @@ -118,28 +117,28 @@ open class RollingLabel: UILabel { label.text = "0" label.textAlignment = .center label.sizeToFit() - createScrollLayer(to: label, text: text) + createScrollLayer(to: label, text: text, index: index) x += label.bounds.width } } } - private func createScrollLayer(to label: UILabel, text: String) { + private func createScrollLayer(to label: UILabel, text: String, index: Int) { let scrollLayer = CAScrollLayer() - scrollLayer.frame = label.frame + scrollLayer.frame = CGRect(x: label.frame.minX, y: label.frame.minY - 10.0, width: label.frame.width, height: label.frame.height * 3.0) scrollLayers.append(scrollLayer) self.layer.addSublayer(scrollLayer) - createContentForLayer(scrollLayer: scrollLayer, text: text) + createContentForLayer(scrollLayer: scrollLayer, text: text, index: index) } - private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) { + private func createContentForLayer(scrollLayer: CAScrollLayer, text: String, index: Int) { var textsForScroll: [String] = [] let max: Int var found = false - if let val = Int(text) { + if let val = Int(text), index == 0 { max = val found = true } else { @@ -150,11 +149,11 @@ open class RollingLabel: UILabel { let str = String(i) textsForScroll.append(str) } - if !found { + if !found && text != "9" { textsForScroll.append(text) } - var height: CGFloat = 0 + var height: CGFloat = 0.0 for text in textsForScroll { let label = UILabel() label.text = text @@ -179,17 +178,18 @@ open class RollingLabel: UILabel { animation.duration = duration + offset animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + let verticalOffset = 20.0 if ascending { - animation.fromValue = maxY + animation.fromValue = maxY + verticalOffset animation.toValue = 0 } else { animation.fromValue = 0 - animation.toValue = maxY + animation.toValue = maxY + verticalOffset } scrollLayer.scrollMode = .vertically scrollLayer.add(animation, forKey: nil) - scrollLayer.scroll(to: CGPoint(x: 0, y: maxY)) + scrollLayer.scroll(to: CGPoint(x: 0, y: maxY + verticalOffset)) offset += self.durationOffset } diff --git a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift new file mode 100644 index 0000000000..8285c81ad6 --- /dev/null +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -0,0 +1,498 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import ComponentFlow +import TelegramCore +import AccountContext +import ReactionSelectionNode +import TelegramPresentationData +import AccountContext +import AnimatedStickerNode +import TelegramAnimatedStickerNode + +final class StickersCarouselComponent: Component { + public typealias EnvironmentType = DemoPageEnvironment + + let context: AccountContext + let stickers: [TelegramMediaFile] + + public init( + context: AccountContext, + stickers: [TelegramMediaFile] + ) { + self.context = context + self.stickers = stickers + } + + public static func ==(lhs: StickersCarouselComponent, rhs: StickersCarouselComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.stickers != rhs.stickers { + return false + } + return true + } + + public final class View: UIView { + private var component: StickersCarouselComponent? + private var node: StickersCarouselNode? + + public func update(component: StickersCarouselComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying + + if self.node == nil { + let node = StickersCarouselNode( + context: component.context, + stickers: component.stickers + ) + self.node = node + self.addSubnode(node) + } + + let isFirstTime = self.component == nil + self.component = component + + if let node = self.node { + node.setVisible(isDisplaying) + node.frame = CGRect(origin: .zero, size: availableSize) + node.updateLayout(size: availableSize, transition: .immediate) + } + + if isFirstTime { + self.node?.animateIn() + } + + return availableSize + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) + } +} + +private let itemSize = CGSize(width: 220.0, height: 220.0) + +private class StickerNode: ASDisplayNode { + private let context: AccountContext + private let file: TelegramMediaFile + + public var imageNode: TransformImageNode + public var animationNode: AnimatedStickerNode? + public var additionalAnimationNode: AnimatedStickerNode? + + private let disposable = MetaDisposable() + private let effectDisposable = MetaDisposable() + + init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + + self.imageNode = TransformImageNode() + + if file.isPremiumSticker { + let animationNode = AnimatedStickerNode() + self.animationNode = animationNode + + let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 400.0, height: 400.0)) + + let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) + animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) + + self.disposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: file.resource).start()) + + if let effect = file.videoThumbnails.first { + self.effectDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file), resource: effect.resource).start()) + + let source = AnimatedStickerResourceSource(account: self.context.account, resource: effect.resource, fitzModifier: nil) + let additionalAnimationNode = AnimatedStickerNode() + + let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(effect.resource.id) + additionalAnimationNode.setup(source: source, width: Int(fittedDimensions.width * 2.0), height: Int(fittedDimensions.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: pathPrefix)) + self.additionalAnimationNode = additionalAnimationNode + } + } else { + self.animationNode = nil + } + + super.init() + + self.isUserInteractionEnabled = false + + if let animationNode = self.animationNode { + self.addSubnode(animationNode) + } else { + self.addSubnode(self.imageNode) + } + + if let additionalAnimationNode = self.additionalAnimationNode { + self.addSubnode(additionalAnimationNode) + } + } + + deinit { + self.disposable.dispose() + self.effectDisposable.dispose() + } + + private var visibility: Bool = false + private var centrality: Bool = false + + public func setCentral(_ central: Bool) { + self.centrality = central + self.updatePlayback() + } + + public func setVisible(_ visible: Bool) { + self.visibility = visible + self.updatePlayback() + } + + private func updatePlayback() { + self.animationNode?.visibility = self.visibility + if let additionalAnimationNode = self.additionalAnimationNode { + let wasVisible = additionalAnimationNode.visibility + let isVisible = self.visibility && self.centrality + if wasVisible && !isVisible { + additionalAnimationNode.alpha = 0.0 + additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak additionalAnimationNode] _ in + additionalAnimationNode?.visibility = isVisible + }) + } else if isVisible { + additionalAnimationNode.visibility = isVisible + if !wasVisible { + additionalAnimationNode.play(fromIndex: 0) + Queue.mainQueue().after(0.05, { + additionalAnimationNode.alpha = 1.0 + }) + } + } + } + } + + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + let boundingSize = CGSize(width: 240.0, height: 240.0) + + if let dimensitons = self.file.dimensions { + let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: 0.0), size: imageSize) + + self.imageNode.frame = imageFrame + if let animationNode = self.animationNode { + animationNode.frame = imageFrame + animationNode.updateLayout(size: imageSize) + + if let additionalAnimationNode = self.additionalAnimationNode { + additionalAnimationNode.frame = imageFrame.offsetBy(dx: -imageFrame.width * 0.245 + 21, dy: -1.0).insetBy(dx: -imageFrame.width * 0.245, dy: -imageFrame.height * 0.245) + additionalAnimationNode.updateLayout(size: additionalAnimationNode.frame.size) + } + } + } + } +} + +private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { + private let context: AccountContext + private let stickers: [TelegramMediaFile] + private var itemContainerNodes: [ASDisplayNode] = [] + private var itemNodes: [StickerNode] = [] + private let scrollNode: ASScrollNode + private let tapNode: ASDisplayNode + + private var animator: DisplayLinkAnimator? + private var currentPosition: CGFloat = 0.0 + private var currentIndex: Int = 0 + + private var validLayout: CGSize? + + private var playingIndices = Set() + + private let positionDelta: Double + + init(context: AccountContext, stickers: [TelegramMediaFile]) { + self.context = context + self.stickers = Array(stickers.shuffled().prefix(14)) + + self.scrollNode = ASScrollNode() + self.tapNode = ASDisplayNode() + + self.positionDelta = 1.0 / CGFloat(self.stickers.count) + + super.init() + + self.clipsToBounds = true + + self.addSubnode(self.scrollNode) + self.scrollNode.addSubnode(self.tapNode) + + self.setup() + } + + override func didLoad() { + super.didLoad() + + self.scrollNode.view.delegate = self + self.scrollNode.view.showsHorizontalScrollIndicator = false + self.scrollNode.view.showsVerticalScrollIndicator = false + self.scrollNode.view.canCancelContentTouches = true + + self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.stickerTapped(_:)))) + } + + @objc private func stickerTapped(_ gestureRecognizer: UITapGestureRecognizer) { + guard self.animator == nil, self.scrollStartPosition == nil else { + return + } + + let point = gestureRecognizer.location(in: self.view) + guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else { + return + } + + self.scrollTo(index, playAnimation: true, duration: 0.4) + } + + func animateIn() { + self.scrollTo(1, playAnimation: true, duration: 0.5, clockwise: true) + } + + func scrollTo(_ index: Int, playAnimation: Bool, duration: Double, clockwise: Bool? = nil) { + guard index >= 0 && index < self.itemNodes.count else { + return + } + self.currentIndex = index + let delta = self.positionDelta + + let startPosition = self.currentPosition + let newPosition = delta * CGFloat(index) + var change = newPosition - startPosition + if let clockwise = clockwise { + if clockwise { + if change > 0.0 { + change = change - 1.0 + } + } else { + if change < 0.0 { + change = 1.0 + change + } + } + } else { + if change > 0.5 { + change = change - 1.0 + } else if change < -0.5 { + change = 1.0 + change + } + } + + self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in + let t = listViewAnimationCurveSystem(t) + var updatedPosition = startPosition + change * t + while updatedPosition >= 1.0 { + updatedPosition -= 1.0 + } + while updatedPosition < 0.0 { + updatedPosition += 1.0 + } + self?.currentPosition = updatedPosition + if let size = self?.validLayout { + self?.updateLayout(size: size, transition: .immediate) + } + }, completion: { [weak self] in + self?.animator = nil + if playAnimation { + self?.playSelectedSticker() + } + }) + } + + private var visibility = false + func setVisible(_ visible: Bool) { + guard self.visibility != visible else { + return + } + self.visibility = visible + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + + func setup() { + for sticker in self.stickers { + let containerNode = ASDisplayNode() + let itemNode = StickerNode(context: self.context, file: sticker) + containerNode.isUserInteractionEnabled = false + containerNode.addSubnode(itemNode) + self.addSubnode(containerNode) + + self.itemContainerNodes.append(containerNode) + self.itemNodes.append(itemNode) + } + } + + private var ignoreContentOffsetChange = false + private func resetScrollPosition() { + self.scrollStartPosition = nil + self.ignoreContentOffsetChange = true + self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: 5000.0 - self.scrollNode.frame.height * 0.5) + self.ignoreContentOffsetChange = false + } + + func playSelectedSticker() { + let delta = self.positionDelta + let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) + + guard !self.playingIndices.contains(index) else { + return + } + + for i in 0 ..< self.itemNodes.count { + let itemNode = self.itemNodes[i] + let containerNode = self.itemContainerNodes[i] + let isCentral = i == index + itemNode.setCentral(isCentral) + + if isCentral { + containerNode.view.superview?.bringSubviewToFront(containerNode.view) + } + } + } + + private var scrollStartPosition: (contentOffset: CGFloat, position: CGFloat)? + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if self.scrollStartPosition == nil { + self.scrollStartPosition = (scrollView.contentOffset.y, self.currentPosition) + } + } + + private let hapticFeedback = HapticFeedback() + func scrollViewDidScroll(_ scrollView: UIScrollView) { + guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { + return + } + + let delta = scrollView.contentOffset.y - startContentOffset + let positionDelta = delta * 0.0005 + var updatedPosition = startPosition + positionDelta + while updatedPosition >= 1.0 { + updatedPosition -= 1.0 + } + while updatedPosition < 0.0 { + updatedPosition += 1.0 + } + self.currentPosition = updatedPosition + + let indexDelta = self.positionDelta + let index = max(0, Int(round(self.currentPosition / indexDelta)) % self.itemNodes.count) + if index != self.currentIndex { + self.currentIndex = index + if self.scrollNode.view.isTracking || self.scrollNode.view.isDecelerating { + self.hapticFeedback.tap() + } + } + + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let (startContentOffset, _) = self.scrollStartPosition, abs(velocity.y) > 0.0 else { + return + } + + let delta = self.positionDelta + let scrollDelta = targetContentOffset.pointee.y - startContentOffset + let positionDelta = scrollDelta * 0.0005 + let positionCounts = round(positionDelta / delta) + let adjustedPositionDelta = delta * positionCounts + let adjustedScrollDelta = adjustedPositionDelta * 2000.0 + + targetContentOffset.pointee = CGPoint(x: 0.0, y: startContentOffset + adjustedScrollDelta) + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.resetScrollPosition() + + let delta = self.positionDelta + let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) + self.scrollTo(index, playAnimation: true, duration: 0.2) + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.resetScrollPosition() + self.playSelectedSticker() + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayout = size + + self.scrollNode.frame = CGRect(origin: CGPoint(), size: size) + if self.scrollNode.view.contentSize.width.isZero { + self.scrollNode.view.contentSize = CGSize(width: size.width, height: 10000000.0) + self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) + self.resetScrollPosition() + } + + let delta = self.positionDelta + + let bounds = CGRect(origin: .zero, size: size) + let areaSize = CGSize(width: floor(size.width * 4.0), height: size.height * 2.2) + + var visibleCount = 0 + for i in 0 ..< self.itemNodes.count { + let itemNode = self.itemNodes[i] + let containerNode = self.itemContainerNodes[i] + + var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0 - CGFloat.pi * 0.5 + if angle < 0.0 { + angle = CGFloat.pi * 2.0 + angle + } + if angle > CGFloat.pi * 2.0 { + angle = angle - CGFloat.pi * 2.0 + } + + func calculateRelativeAngle(_ angle: CGFloat) -> CGFloat { + var relativeAngle = angle + if relativeAngle > CGFloat.pi { + relativeAngle = (2.0 * CGFloat.pi - relativeAngle) * -1.0 + } + return relativeAngle + } + + let relativeAngle = calculateRelativeAngle(angle) + let distance = abs(relativeAngle) + + let point = CGPoint( + x: cos(angle), + y: sin(angle) + ) + + let itemFrame = CGRect(origin: CGPoint(x: -size.width - 0.5 * itemSize.width - 30.0 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize) + containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size) + containerNode.position = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.65) + transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.5) + + let isVisible = self.visibility && itemFrame.intersects(bounds) + itemNode.setVisible(isVisible) + if isVisible { + visibleCount += 1 + } + + itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size) + itemNode.updateLayout(size: itemFrame.size, transition: transition) + } + } +} diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 5f53499926..4532df2191 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -152,10 +152,10 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC if isPremiumSticker { animationNode.completed = { [weak self] _ in if let strongSelf = self, let animationNode = strongSelf.animationNode, let additionalAnimationNode = strongSelf.additionalAnimationNode { - Queue.mainQueue().after(0.1, { + Queue.mainQueue().async { animationNode.play() additionalAnimationNode.play() - }) + } } } } diff --git a/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json new file mode 100644 index 0000000000..718c1456b3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "dots@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png b/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..57fbe59d71e1433939afe5d825b4ba493b008299 GIT binary patch literal 1077 zcmeAS@N?(olHy`uVBq!ia0y~yVDtjAZ8+F~B!lYbTp-C=;1OBOz@TUW!i=ud3=COlzX_6sl0*y}2{On2?v`SOX=+OX`>{PjDpz5j0BV;I_9ANBV8@BF&| zcCu%m7A0hB$>n=zpZ)t?E$+>QUk{>YI!y0a$8te6=zW*%l-^_C%bTx@ z&3W;)D){TKcWtX*+ls|rys)%;UB;H(N&HVQUc4Cn_}NN>E&tA2$FE$dZL~||qUg-5 z@HGor?XEkEPtOYj$yi!eCf~6WZM$~+Z9(M2T@x>)_Uf^g zLgtExEb)wC%1p znWcpqYz!Ppys38fbuV7I<$bbQzqmA)GxpnyqJF*VsMw8dMKynB-pW7Z@8h%NTc9?Q zVW|rPR?I$_Dc5&3Ej@joMvS@8t~D2zHt1z%0d-yauAOE7#^n5^#i!+B-(5`gU#8I}X8}m;H?vir}cY0A3pcH#TW@&$-{iwClwwrd^ZX2T7h< z*xbCJs-|XBRbAaai$68#cJuAeJpX+BXj0+v@26M()eDy?vDF2l$TvZ Void)? + let controller = PremiumDemoScreen(context: context, subject: .uniqueReactions, action: { + let controller = PremiumIntroScreen(context: context, source: .reactions) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } strongSelf.push(controller) return } @@ -11530,7 +11533,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G for item in results { if let item = item { if item.fileSize > Int64(premiumLimits.maxUploadFileParts) * 512 * 1024 { - strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.Premium_FileTooLarge, text: strongSelf.presentationData.strings.Conversation_PremiumUploadFileTooLarge, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) return } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { let context = strongSelf.context diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 36c7b3e802..8e631ba4a4 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -159,11 +159,14 @@ public struct WebAppParameters { public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] { var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb + var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb if backgroundColor == 0x000000 { backgroundColor = presentationTheme.list.itemBlocksBackgroundColor.rgb + secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb } return [ "bg_color": Int32(bitPattern: backgroundColor), + "secondary_bg_color": Int32(bitPattern: secondaryBackgroundColor), "text_color": Int32(bitPattern: presentationTheme.list.itemPrimaryTextColor.rgb), "hint_color": Int32(bitPattern: presentationTheme.list.itemSecondaryTextColor.rgb), "link_color": Int32(bitPattern: presentationTheme.list.itemAccentColor.rgb), From a01fe7a60340e4a0c753ee7c442e511fe0fe8b8d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 May 2022 16:35:51 +0400 Subject: [PATCH 02/11] Various fixes --- submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift | 2 +- submodules/PremiumUI/Sources/PremiumLimitScreen.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift index 721af4b8b4..97ef96afc5 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -145,7 +145,7 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { let size = strongSelf.hostView.update( transition: .immediate, component: AnyComponent(PremiumLimitDisplayComponent( - inactiveColor: UIColor(rgb: 0xe3e3e9), + inactiveColor: item.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), activeColors: [ UIColor(rgb: 0x0077ff), UIColor(rgb: 0x6b93ff), diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 15c8ae590c..c660b7e8a9 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -113,6 +113,7 @@ private class PremiumLimitAnimationComponent: Component { self.activeBackground = SimpleLayer() self.badgeView = UIView() + self.badgeView.alpha = 0.0 self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) self.badgeMaskBackgroundView = UIView() @@ -203,6 +204,9 @@ private class PremiumLimitAnimationComponent: Component { self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") self.badgeView.layer.add(returnAnimation, forKey: "appearance3") + self.badgeView.alpha = 1.0 + self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + if let badgeText = component.badgeText { self.badgeCountLabel.configure(with: badgeText) } From f4d9c0e2f7ac9bed9728efa53b6d28afbc3a28b7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 May 2022 17:07:19 +0400 Subject: [PATCH 03/11] Various fixes --- submodules/PremiumUI/Sources/PremiumDemoScreen.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 0f46a42536..6f73951d60 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -359,17 +359,18 @@ private final class DemoPagerComponent: Component { self.scrollView.addSubview(itemView) } + let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize) + let isDisplaying = itemFrame.intersects(self.scrollView.bounds) - let isDisplaying = itemView.frame.intersects(self.scrollView.bounds) let environment = DemoPageEnvironment(isDisplaying: isDisplaying) - let itemSize = itemView.update( + let _ = itemView.update( transition: itemTransition, component: item.content.component, environment: { environment }, containerSize: availableSize ) - itemView.frame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: itemSize) + itemView.frame = itemFrame i += 1 } From 690045cf9933cf3dcd642fedf7276c164bd8440b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 01:06:19 +0400 Subject: [PATCH 04/11] Add support for web app header and background color customization --- .../ChatListFilterPresetListController.swift | 18 +++---- .../ChatListFilterPresetListItem.swift | 5 +- .../Sources/SolidRoundedButtonComponent.swift | 1 + .../PremiumUI/Sources/PremiumDemoScreen.swift | 11 +++-- .../Sources/PremiumIntroScreen.swift | 25 ++++++++-- .../Sources/ReactionsCarouselComponent.swift | 12 +++-- .../WebUI/Sources/WebAppController.swift | 49 ++++++++++++++++++- 7 files changed, 98 insertions(+), 23 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index c16f0865e3..acc88993e1 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -74,7 +74,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool) + case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool, isDisabled: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) @@ -95,7 +95,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 0 case .listHeader: return 100 - case let .preset(index, _, _, _, _, _, _, _): + case let .preset(index, _, _, _, _, _, _, _, _): return 101 + index.value case .addItem: return 1000 @@ -122,7 +122,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return .suggestedAddCustom case .listHeader: return .listHeader - case let .preset(_, _, _, preset, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _): return .preset(preset.id) case .addItem: return .addItem @@ -152,8 +152,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { }) case let .listHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) - case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats): - return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, sectionId: self.section, action: { + case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats, isDisabled): + return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, isDisabled: isDisabled, sectionId: self.section, action: { arguments.openPreset(preset) }, setItemWithRevealedOptions: { lhs, rhs in arguments.setItemWithRevealedOptions(lhs, rhs) @@ -219,10 +219,10 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { if isPremium, case .allChats = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: "", label: "", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: false, isEditing: state.isEditing, isAllChats: true, isDisabled: false)) } if case let .filter(_, title, _, _) = filter { - entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false)) + entries.append(.preset(index: PresetIndex(value: entries.count), title: title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing, isAllChats: false, isDisabled: false)) } } if actualFilters.count < limits.maxFoldersCount { @@ -528,7 +528,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal in let fromEntry = entries[fromIndex] - guard case let .preset(_, _, _, fromPreset, _, _, _, _) = fromEntry else { + guard case let .preset(_, _, _, fromPreset, _, _, _, _, _) = fromEntry else { return .single(false) } var referenceFilter: ChatListFilter? @@ -536,7 +536,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch var afterAll = false if toIndex < entries.count { switch entries[toIndex] { - case let .preset(_, _, _, preset, _, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _, _): referenceFilter = preset default: if entries[toIndex] < fromEntry { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 9e8ba9da0c..1489d68b6d 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -24,6 +24,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let canBeReordered: Bool let canBeDeleted: Bool let isAllChats: Bool + let isDisabled: Bool let sectionId: ItemListSectionId let action: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void @@ -38,6 +39,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { canBeReordered: Bool, canBeDeleted: Bool, isAllChats: Bool, + isDisabled: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, @@ -51,6 +53,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { self.canBeReordered = canBeReordered self.canBeDeleted = canBeDeleted self.isAllChats = isAllChats + self.isDisabled = isDisabled self.sectionId = sectionId self.action = action self.setItemWithRevealedOptions = setItemWithRevealedOptions @@ -380,7 +383,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN if let arrowImage = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) } - strongSelf.arrowNode.isHidden = item.isAllChats + strongSelf.arrowNode.isHidden = item.isAllChats || item.isDisabled strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index 13137aa79d..0706a81724 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -116,6 +116,7 @@ public final class SolidRoundedButtonComponent: Component { } if let button = self.button { + button.title = component.title button.updateTheme(component.theme) let height = button.updateLayout(width: availableSize.width, transition: .immediate) transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 6f73951d60..eaec2901f2 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -129,15 +129,20 @@ private final class GradientBackgroundComponent: Component { final class DemoPageEnvironment: Equatable { public let isDisplaying: Bool + public let isCentral: Bool - public init(isDisplaying: Bool) { + public init(isDisplaying: Bool, isCentral: Bool) { self.isDisplaying = isDisplaying + self.isCentral = isCentral } public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool { if lhs.isDisplaying != rhs.isDisplaying { return false } + if lhs.isCentral != rhs.isCentral { + return false + } return true } } @@ -317,7 +322,7 @@ private final class DemoPagerComponent: Component { if let itemView = self.itemViews[item.content.id] { let isDisplaying = itemView.frame.intersects(self.scrollView.bounds) - let environment = DemoPageEnvironment(isDisplaying: isDisplaying) + let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: isDisplaying) let _ = itemView.update( transition: .immediate, component: item.content.component, @@ -362,7 +367,7 @@ private final class DemoPagerComponent: Component { let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize) let isDisplaying = itemFrame.intersects(self.scrollView.bounds) - let environment = DemoPageEnvironment(isDisplaying: isDisplaying) + let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: isDisplaying) let _ = itemView.update( transition: itemTransition, component: item.content.component, diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6e76b9bf09..68212c7c62 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -718,13 +718,15 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) let context: AccountContext + let isPremium: Bool? let price: String? let present: (ViewController) -> Void let buy: () -> Void let updateIsFocused: (Bool) -> Void - init(context: AccountContext, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { + init(context: AccountContext, isPremium: Bool?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { self.context = context + self.isPremium = isPremium self.price = price self.present = present self.buy = buy @@ -735,6 +737,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.isPremium != rhs.isPremium { + return false + } if lhs.price != rhs.price { return false } @@ -841,7 +846,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let text = text.update( component: MultilineTextComponent( text: .markdown( - text: strings.Premium_Description, + text: context.component.isPremium == true ? strings.Premium_SubscribedDescription : strings.Premium_Description, attributes: markdownAttributes ), horizontalAlignment: .center, @@ -1166,6 +1171,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var inProgress = false var premiumProduct: InAppPurchaseManager.Product? + var isPremium: Bool? + private var disposable: Disposable? private var paymentDisposable = MetaDisposable() private var activationDisposable = MetaDisposable() @@ -1178,10 +1185,17 @@ private final class PremiumIntroScreenComponent: CombinedComponent { super.init() if let inAppPurchaseManager = context.sharedContext.inAppPurchaseManager { - self.disposable = (inAppPurchaseManager.availableProducts - |> deliverOnMainQueue).start(next: { [weak self] products in + self.disposable = combineLatest( + queue: Queue.mainQueue(), + inAppPurchaseManager.availableProducts, + context.account.postbox.peerView(id: context.account.peerId) + |> map { view -> Bool in + return view.peers[view.peerId]?.isPremium ?? false + } + ).start(next: { [weak self] products, isPremium in if let strongSelf = self { strongSelf.premiumProduct = products.first + strongSelf.isPremium = isPremium strongSelf.updated(transition: .immediate) } }) @@ -1281,7 +1295,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let title = title.update( component: Text( - text: environment.strings.Premium_Title, + text: state.isPremium == true ? environment.strings.Premium_SubscribedTitle : environment.strings.Premium_Title, font: Font.bold(28.0), color: environment.theme.rootController.navigationBar.primaryTextColor ), @@ -1336,6 +1350,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( context: context.component.context, + isPremium: state.isPremium, price: state.premiumProduct?.price, present: context.component.present, buy: { [weak state] in diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index aae0a7d66f..788a7d2f4f 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -42,8 +42,12 @@ final class ReactionsCarouselComponent: Component { public final class View: UIView { private var component: ReactionsCarouselComponent? private var node: ReactionCarouselNode? + + private var isVisible = false - public func update(component: ReactionsCarouselComponent, availableSize: CGSize, transition: Transition) -> CGSize { + public func update(component: ReactionsCarouselComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + let isDisplaying = environment[DemoPageEnvironment.self].isDisplaying + if self.node == nil { let node = ReactionCarouselNode( context: component.context, @@ -54,7 +58,6 @@ final class ReactionsCarouselComponent: Component { self.addSubnode(node) } - let isFirstTime = self.component == nil self.component = component if let node = self.node { @@ -62,9 +65,10 @@ final class ReactionsCarouselComponent: Component { node.updateLayout(size: availableSize, transition: .immediate) } - if isFirstTime { + if isDisplaying && !self.isVisible { self.node?.animateIn() } + self.isVisible = isDisplaying return availableSize } @@ -75,7 +79,7 @@ final class ReactionsCarouselComponent: Component { } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) + return view.update(component: self, availableSize: availableSize, environment: environment, transition: transition) } } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 8e631ba4a4..b3f9be33c7 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -162,7 +162,7 @@ public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> var secondaryBackgroundColor = presentationTheme.list.blocksBackgroundColor.rgb if backgroundColor == 0x000000 { backgroundColor = presentationTheme.list.itemBlocksBackgroundColor.rgb - secondaryBackgroundColor = presentationTheme.list.plainBackgroundColor.rgb + secondaryBackgroundColor = presentationTheme.list.itemBlocksBackgroundColor.rgb } return [ "bg_color": Int32(bitPattern: backgroundColor), @@ -186,6 +186,9 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private weak var controller: WebAppController? + private let backgroundNode: ASDisplayNode + private let headerBackgroundNode: ASDisplayNode + fileprivate var webView: WebAppWebView? private var placeholderIcon: (UIImage, Bool)? private var placeholderNode: ShimmerEffectNode? @@ -213,6 +216,9 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller = controller self.presentationData = controller.presentationData + self.backgroundNode = ASDisplayNode() + self.headerBackgroundNode = ASDisplayNode() + super.init() if self.presentationData.theme.list.plainBackgroundColor.rgb == 0x000000 { @@ -244,6 +250,9 @@ public final class WebAppController: ViewController, AttachmentContainable { self.addSubnode(placeholderNode) self.placeholderNode = placeholderNode + self.addSubnode(self.backgroundNode) + self.addSubnode(self.headerBackgroundNode) + let placeholder: Signal<(FileMediaReference, Bool)?, NoError> if durgerKingBotIds.contains(controller.botId.id._internalGetInt64Value()) { placeholder = .single(nil) @@ -471,6 +480,9 @@ public final class WebAppController: ViewController, AttachmentContainable { let previousLayout = self.validLayout?.0 self.validLayout = (layout, navigationBarHeight) + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size)) + transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) + if let webView = self.webView { let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom))) let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) @@ -669,11 +681,45 @@ public final class WebAppController: ViewController, AttachmentContainable { break } } + case "web_app_set_background_color": + if let json = json, let colorValue = json["color"] as? String, let color = UIColor(hexString: colorValue) { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .linear) + transition.updateBackgroundColor(node: self.backgroundNode, color: color) + } + case "web_app_set_header_color": + if let json = json, let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { + self.headerColorKey = colorKey + self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) + } default: break } } + private var headerColorKey: String? + private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) { + let color: UIColor? + var backgroundColor = self.presentationData.theme.list.plainBackgroundColor + var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor + if backgroundColor.rgb == 0x000000 { + backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor + secondaryBackgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor + } + if let headerColorKey = self.headerColorKey { + switch headerColorKey { + case "bg_color": + color = backgroundColor + case "secondary_bg_color": + color = secondaryBackgroundColor + default: + color = nil + } + } else { + color = nil + } + transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear) + } + private func handleSendData(data string: String) { guard let controller = self.controller, let buttonText = controller.buttonText, !self.dismissed else { return @@ -702,6 +748,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } else { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } + self.updateHeaderBackgroundColor(transition: .immediate) self.sendThemeChangedEvent() } From f7ec8a0bf2d9d7d8aa677f6d2fe1880434417cb8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 03:05:10 +0400 Subject: [PATCH 05/11] Various improvements --- .../Sources/InAppPurchaseManager.swift | 1 + .../Sources/PremiumIntroScreen.swift | 74 ++++++++++++++++--- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index f326830dc6..a00809bbd9 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -27,6 +27,7 @@ public final class InAppPurchaseManager: NSObject { public enum PurchaseError { case generic + case cancelled } private final class PaymentTransactionContext { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 68212c7c62..1be2281a79 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -19,7 +19,7 @@ import ConfettiEffect import TextFormat import InstantPageCache -public enum PremiumSource { +public enum PremiumSource: Equatable { case settings case stickers case reactions @@ -33,6 +33,7 @@ public enum PremiumSource { case folders case chatsPerFolder case accounts + case deeplink(String?) var identifier: String { switch self { @@ -62,6 +63,12 @@ public enum PremiumSource { return "double_limits__dialog_filters_chats" case .accounts: return "double_limits__accounts" + case let .deeplink(reference): + if let reference = reference { + return "deeplink_\(reference)" + } else { + return "deeplink" + } } } } @@ -718,14 +725,16 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { typealias EnvironmentType = (ViewControllerComponentContainer.Environment, ScrollChildEnvironment) let context: AccountContext + let source: PremiumSource let isPremium: Bool? let price: String? let present: (ViewController) -> Void let buy: () -> Void let updateIsFocused: (Bool) -> Void - init(context: AccountContext, isPremium: Bool?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { + init(context: AccountContext, source: PremiumSource, isPremium: Bool?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { self.context = context + self.source = source self.isPremium = isPremium self.price = price self.present = present @@ -737,6 +746,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.source != rhs.source { + return false + } if lhs.isPremium != rhs.isPremium { return false } @@ -749,11 +761,13 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + + var price: String? private var disposable: Disposable? - var configuration = PremiumIntroConfiguration.defaultValue + private(set) var configuration = PremiumIntroConfiguration.defaultValue - init(context: AccountContext) { + init(context: AccountContext, source: PremiumSource) { self.context = context super.init() @@ -768,6 +782,25 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if let strongSelf = self { strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) strongSelf.updated(transition: .immediate) + + var jsonString: String = "{" + jsonString += "\"source\": \"\(source.identifier)\"," + + jsonString += "\"data\": {\"premium_promo_order\":[" + var isFirst = true + for perk in strongSelf.configuration.perks { + if !isFirst { + jsonString += "," + } + isFirst = false + jsonString += "\"\(perk.identifier)\"" + } + jsonString += "]}}" + + + if let data = jsonString.data(using: .utf8), let json = JSON(data: data) { + addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json) + } } }) } @@ -778,7 +811,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context) + return State(context: self.context, source: self.source) } static var body: Body { @@ -797,6 +830,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state + state.price = context.component.price let theme = environment.theme let strings = environment.strings @@ -882,7 +916,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let present = context.component.present let buy = context.component.buy let updateIsFocused = context.component.updateIsFocused - let price = context.component.price + var i = 0 for perk in state.configuration.perks { let iconBackgroundColors = gradientColors[i] @@ -904,7 +938,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ) ) ), - action: { + action: { [weak state] in var demoSubject: PremiumDemoScreen.Subject switch perk { case .doubleLimits: @@ -933,7 +967,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let controller = PremiumDemoScreen( context: accountContext, subject: demoSubject, - source: .intro(price), + source: .intro(state?.price), action: { dismissImpl?() buy() @@ -947,6 +981,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { controller?.dismiss(animated: true, completion: nil) } updateIsFocused(true) + + addAppLogEvent(postbox: accountContext.account.postbox, type: "premium.promo_screen_tap", data: ["item": perk.identifier]) } )) i += 1 @@ -1141,12 +1177,14 @@ private final class PremiumIntroScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let source: PremiumSource let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void let completion: () -> Void - init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { + init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void) { self.context = context + self.source = source self.updateInProgress = updateInProgress self.present = present self.completion = completion @@ -1156,6 +1194,9 @@ private final class PremiumIntroScreenComponent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.source != rhs.source { + return false + } return true } @@ -1214,6 +1255,8 @@ private final class PremiumIntroScreenComponent: CombinedComponent { return } + addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept") + self.inProgress = true self.updateInProgress(true) self.updated(transition: .immediate) @@ -1230,11 +1273,18 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } })) } - }, error: { [weak self] _ in + }, error: { [weak self] error in if let strongSelf = self { strongSelf.inProgress = false strongSelf.updateInProgress(false) strongSelf.updated(transition: .immediate) + + switch error { + case .generic: + addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail") + case .cancelled: + break + } } })) } @@ -1350,6 +1400,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( context: context.component.context, + source: context.component.source, isPremium: state.isPremium, price: state.premiumProduct?.price, present: context.component.present, @@ -1459,7 +1510,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { return self._ready } - public init(context: AccountContext, modal: Bool = true, reference: String? = nil, source: PremiumSource? = nil) { + public init(context: AccountContext, modal: Bool = true, source: PremiumSource) { self.context = context var updateInProgressImpl: ((Bool) -> Void)? @@ -1467,6 +1518,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { var completionImpl: (() -> Void)? super.init(context: context, component: PremiumIntroScreenComponent( context: context, + source: source, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index b3ed446341..79648135f8 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -522,7 +522,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur |> deliverOnMainQueue).start(next: { peer in let isPremium = peer?.isPremium ?? false if !isPremium { - let controller = PremiumIntroScreen(context: context, reference: reference) + let controller = PremiumIntroScreen(context: context, source: .deeplink(reference)) if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } From 070609d969cf12cad29b49eb411b2f55aa0cc536 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 03:35:00 +0400 Subject: [PATCH 06/11] Add custom web app overscroll background --- submodules/WebUI/Sources/WebAppController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index b3f9be33c7..9e11b37c79 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -188,6 +188,7 @@ public final class WebAppController: ViewController, AttachmentContainable { private let backgroundNode: ASDisplayNode private let headerBackgroundNode: ASDisplayNode + private let topOverscrollNode: ASDisplayNode fileprivate var webView: WebAppWebView? private var placeholderIcon: (UIImage, Bool)? @@ -218,6 +219,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.backgroundNode = ASDisplayNode() self.headerBackgroundNode = ASDisplayNode() + self.topOverscrollNode = ASDisplayNode() super.init() @@ -387,6 +389,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } self.view.addSubview(webView) + webView.scrollView.addSubnode(self.topOverscrollNode) } @objc fileprivate func mainButtonPressed() { @@ -482,6 +485,7 @@ public final class WebAppController: ViewController, AttachmentContainable { transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size)) transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: .zero, size: CGSize(width: layout.size.width, height: navigationBarHeight))) + transition.updateFrame(node: self.topOverscrollNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -1000.0), size: CGSize(width: layout.size.width, height: 1000.0))) if let webView = self.webView { let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom))) @@ -718,6 +722,7 @@ public final class WebAppController: ViewController, AttachmentContainable { color = nil } transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear) + transition.updateBackgroundColor(node: self.topOverscrollNode, color: color ?? .clear) } private func handleSendData(data string: String) { From 9339fe6e5613da52b16259c9522ac53d883194f8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 06:13:07 +0400 Subject: [PATCH 07/11] Various improvements --- .../Sources/ChatListController.swift | 17 ++++++------ .../Sources/ChatListControllerNode.swift | 6 +++++ .../Sources/Node/ChatListViewTransition.swift | 2 +- .../Sources/PremiumStarComponent.swift | 27 ++++++++++++------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 09aa6e88f6..7b63309cb4 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1980,8 +1980,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false - strongSelf.isPremium = isPremium + let isPremium = peerView.peers[peerView.peerId]?.isPremium + strongSelf.isPremium = isPremium ?? false let (_, items) = countAndFilterItems var filterItems: [ChatListFilterTabEntry] = [] @@ -1989,7 +1989,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for (filter, unreadCount, hasUnmutedUnread) in items { switch filter { case .allChats: - if !isPremium && filterItems.count > 0 { + if let isPremium = isPremium, !isPremium && filterItems.count > 0 { filterItems.insert(.all(unreadCount: 0), at: 0) } else { filterItems.append(.all(unreadCount: 0)) @@ -2042,7 +2042,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController selectedEntryId = .all } } - let filtersLimit = !isPremium ? limits.maxFoldersCount : nil + let filtersLimit = isPremium == false ? limits.maxFoldersCount : nil strongSelf.tabContainerData = (resolvedItems, displayTabsAtBottom, filtersLimit) var availableFilters: [ChatListContainerNodeFilter] = [] var hasAllChats = false @@ -2050,7 +2050,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController switch item.0 { case .allChats: hasAllChats = true - if !isPremium && availableFilters.count > 0 { + if let isPremium = isPremium, !isPremium && availableFilters.count > 0 { availableFilters.insert(.all, at: 0) } else { availableFilters.append(.all) @@ -2064,7 +2064,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters, limit: filtersLimit) - if !strongSelf.initializedFilters { + if isPremium == nil && items.isEmpty { + strongSelf.ready.set(strongSelf.chatListDisplayNode.containerNode.currentItemNode.ready) + } else if !strongSelf.initializedFilters { if selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: { [weak self] in if let strongSelf = self { @@ -2077,7 +2079,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.initializedFilters = true } - let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom let animated = strongSelf.didSetupTabs @@ -2096,7 +2097,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.containerLayoutUpdated(layout, transition: transition) (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } else { - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, filtersLimit: filtersLimit, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 2ad548a7ca..f5abfabcf9 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -878,6 +878,12 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) + if !animated { + self.selectedId = id + self.applyItemNodeAsCurrent(id: id, itemNode: itemNode) + self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false) + } + disposable.set((itemNode.listNode.ready |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index bc9501cdf8..cb6d2f4be7 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -207,7 +207,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV } var adjustScrollToFirstItem = false - if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 1 { + if !previewing && !searchMode && fromEmptyView && scrollToItem == nil && toView.filteredEntries.count >= 2 { adjustScrollToFirstItem = true } diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index a29f49e5bc..608bda5c73 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -84,6 +84,7 @@ class PremiumStarComponent: Component { self.sceneView.backgroundColor = .clear self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) self.sceneView.isUserInteractionEnabled = false + self.sceneView.preferredFramesPerSecond = 60 super.init(frame: frame) @@ -213,18 +214,24 @@ class PremiumStarComponent: Component { } private func setup() { - guard let url = getAppBundle().url(forResource: "star", withExtension: ""), - let compressedData = try? Data(contentsOf: url), - let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { - return - } - let fileName = "star_\(sceneVersion).scn" - let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) - if !FileManager.default.fileExists(atPath: tmpURL.path) { - try? decompressedData.write(to: tmpURL) + let resourceUrl: URL + if let url = getAppBundle().url(forResource: "star", withExtension: "scn") { + resourceUrl = url + } else { + let fileName = "star_\(sceneVersion).scn" + let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) + if !FileManager.default.fileExists(atPath: tmpUrl.path) { + guard let url = getAppBundle().url(forResource: "star", withExtension: ""), + let compressedData = try? Data(contentsOf: url), + let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { + return + } + try? decompressedData.write(to: tmpUrl) + } + resourceUrl = tmpUrl } - guard let scene = try? SCNScene(url: tmpURL, options: nil) else { + guard let scene = try? SCNScene(url: resourceUrl, options: nil) else { return } From 72815547e242c6b9322e0b2ea3f54ae0a3bfd57a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 06:14:13 +0400 Subject: [PATCH 08/11] Various improvements --- submodules/PremiumUI/Resources/star | Bin 49022 -> 49037 bytes .../Sources/PremiumStarComponent.swift | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star index 16390db8f19af1e1e917e467871927d588fc4ad3..7b4f75d0139f19521314348ae406d97a0c450366 100644 GIT binary patch delta 43658 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 delta 43661 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 z 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 CWa}va diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 608bda5c73..89b6e16870 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -7,7 +7,7 @@ import SceneKit import GZip import AppBundle -private let sceneVersion: Int = 1 +private let sceneVersion: Int = 2 private func deg2rad(_ number: Float) -> Float { return number * .pi / 180 From f2d7c9ab9bca97c0fe65b53978637c3df81ad86d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 08:21:00 +0400 Subject: [PATCH 09/11] Various fixes --- .../ChatListUI/Sources/ChatListFilterTabContainerNode.swift | 2 +- .../Sources/ItemListStickerPackItem.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index eea4b14b53..38803090f6 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -239,7 +239,7 @@ private final class ItemNode: ASDisplayNode { }) } - transition.updateAlpha(node: self.badgeContainerNode, alpha: (isDisabled || isReordering || unreadCount == 0) ? 0.0 : 1.0) + transition.updateAlpha(node: self.badgeContainerNode, alpha: (isEditing || isDisabled || isReordering || unreadCount == 0) ? 0.0 : 1.0) let selectionAlpha: CGFloat = selectionFraction * selectionFraction let deselectionAlpha: CGFloat = isDisabled ? 0.5 : 1.0// - selectionFraction diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index f239cf8fa2..e2ae0aa42d 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -667,12 +667,12 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.installationActionBackgroundNode.image = backgroundImage } - let installationActionFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: 0.0), size: CGSize(width: 50.0, height: layout.contentSize.height)) + let installationActionFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: 0.0), size: CGSize(width: installWidth, height: layout.contentSize.height)) strongSelf.installationActionNode.frame = installationActionFrame let buttonFrame = CGRect(origin: CGPoint(x: params.width - rightInset - installWidth - 16.0, y: installationActionFrame.minY + floor((installationActionFrame.size.height - 28.0) / 2.0)), size: CGSize(width: installWidth, height: 28.0)) strongSelf.installationActionBackgroundNode.frame = buttonFrame - strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) + strongSelf.installTextNode.frame = CGRect(origin: CGPoint(x: buttonFrame.minX + floorToScreenPixels((buttonFrame.width - installLayout.size.width) / 2.0), y: buttonFrame.minY + floorToScreenPixels((buttonFrame.height - installLayout.size.height) / 2.0) + 1.0), size: installLayout.size) case .selection: strongSelf.installationActionNode.isHidden = true strongSelf.installationActionBackgroundNode.isHidden = true From ace926f2c8941093d5f82f3a842c0987b46c7993 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 15:02:20 +0400 Subject: [PATCH 10/11] Fix web app contenteditable focus --- submodules/WebUI/Sources/WebAppWebView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 61aa444e20..6c494aa817 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -55,7 +55,7 @@ final class WebAppWebView: WKWebView { handleScriptMessageImpl?(message) }, name: "performAction") - let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea){-webkit-user-select:none;}';" + let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';" + " var head = document.head || document.getElementsByTagName('head')[0];" + " var style = document.createElement('style'); style.type = 'text/css';" + " style.appendChild(document.createTextNode(css)); head.appendChild(style);" From 01d4131ce722047dbabf9acf6d307519dcb9081e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 26 May 2022 16:43:56 +0400 Subject: [PATCH 11/11] Fix web app top overscroll --- submodules/WebUI/Sources/WebAppController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 9e11b37c79..6a312e9077 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -389,7 +389,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } self.view.addSubview(webView) - webView.scrollView.addSubnode(self.topOverscrollNode) + webView.scrollView.insertSubview(self.topOverscrollNode.view, at: 0) } @objc fileprivate func mainButtonPressed() {