diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 35344f4193..587d924916 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7564,6 +7564,7 @@ Sorry for the inconvenience."; "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"; "Premium.MaxFavedStickersText" = "An older sticker was replaced with this one. You can [increase the limit]() to %@ stickers."; +"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can free one place by subscribing to **Telegram Premium**."; "Premium.Free" = "Free"; "Premium.Premium" = "Premium"; @@ -7571,7 +7572,7 @@ 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.PersonalTitle" = "[%@]() is a subscriber\nof Telegram Premium"; "Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features."; "Premium.SubscribedTitle" = "You are all set!"; @@ -7595,6 +7596,9 @@ Sorry for the inconvenience."; "Premium.Reactions" = "Unique Reactions"; "Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to the Premium subscribers."; +"Premium.ReactionsStandalone" = "Additional Reactions"; +"Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions by subscribing to **Telegram Premium**."; + "Premium.Stickers" = "Premium Stickers"; "Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly."; @@ -7614,6 +7618,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.ChargeInfo" = "Next charge: %1$@ on %2$@. [Cancel](cancel)."; + "Premium.MoreAboutPremium" = "More About Premium"; "Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 7b63309cb4..d200e99fc9 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1323,7 +1323,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) - if let filter = filters.first(where: { $0.id == id }), case let .filter(_, _, _, data) = filter, data.includePeers.peers.count < premiumLimits.maxFolderChatsCount { + if let _ = filters.first(where: { $0.id == id }) { items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_AddChatsToFolder, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { c, f in @@ -1348,27 +1348,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController for filter in presetList { if filter.id == id, case let .filter(_, _, _, data) = filter { let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false + let limit = limits.maxFolderChatsCount let premiumLimit = premiumLimits.maxFolderChatsCount - if let accountPeer = accountPeer, accountPeer.isPremium { - if data.includePeers.peers.count >= premiumLimit { - return - } - } else { - if data.includePeers.peers.count >= limit { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(data.includePeers.peers.count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - strongSelf.push(controller) - f(.dismissWithoutContent) - return + if data.includePeers.peers.count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: {}) + strongSelf.push(controller) + f(.dismissWithoutContent) + return + } else if data.includePeers.peers.count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(data.includePeers.peers.count), action: { + let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } + strongSelf.push(controller) + f(.dismissWithoutContent) + return } let _ = (strongSelf.context.engine.peers.currentChatListFilters() diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 86e515a944..f97d88fdc3 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -605,6 +605,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f queue: Queue.mainQueue(), controller.result |> 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) ) @@ -615,8 +616,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f return } - let (limits, premiumLimits) = data - + let (accountPeer, limits, premiumLimits) = data + let isPremium = accountPeer?.isPremium ?? false + var includePeers: [PeerId] = [] for peerId in peerIds { switch peerId { @@ -628,15 +630,13 @@ 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 - } - + if includePeers.count > premiumLimits.maxFolderChatsCount { + let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(includePeers.count), action: {}) + controller?.push(limitController) + return + } else if includePeers.count > limits.maxFolderChatsCount && !isPremium { var replaceImpl: ((ViewController) -> Void)? - let limitController = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(includePeers.count), action: { + let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(includePeers.count), action: { let introController = PremiumIntroScreen(context: context, source: .chatsPerFolder) replaceImpl?(introController) }) @@ -980,28 +980,28 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat stateWithPeers |> take(1) ).start(next: { result, state in let (accountPeer, limits, premiumLimits) = result - + let isPremium = accountPeer?.isPremium ?? false + let (_, currentIncludePeers, _) = state let limit = limits.maxFolderChatsCount let premiumLimit = premiumLimits.maxFolderChatsCount - if let accountPeer = accountPeer, accountPeer.isPremium { - if currentIncludePeers.count >= premiumLimit { - return - } - } else { - if currentIncludePeers.count >= limit { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(currentIncludePeers.count), action: { - let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - pushControllerImpl?(controller) - return + + if currentIncludePeers.count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: {}) + pushControllerImpl?(controller) + return + } else if currentIncludePeers.count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(currentIncludePeers.count), action: { + let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } + pushControllerImpl?(controller) + return } let state = stateValue.with { $0 } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index acc88993e1..f624a09491 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -154,7 +154,11 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) 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) + if isDisabled { + arguments.addNew() + } else { + arguments.openPreset(preset) + } }, setItemWithRevealedOptions: { lhs, rhs in arguments.setItemWithRevealedOptions(lhs, rhs) }, remove: { @@ -191,7 +195,7 @@ private func filtersWithAppliedOrder(filters: [(ChatListFilter, Int)], order: [I return sortedFilters } -private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool, limits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] { +private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool, limits: EngineConfiguration.UserLimits, premiumLimits: EngineConfiguration.UserLimits) -> [ChatListFilterPresetListEntry] { var entries: [ChatListFilterPresetListEntry] = [] entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info)) @@ -217,17 +221,19 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present if !filters.isEmpty || suggestedFilters.isEmpty { entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection)) + var folderCount = 0 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, 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, isDisabled: false)) + folderCount += 1 + 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: !isPremium && folderCount > limits.maxFoldersCount)) } } - if actualFilters.count < limits.maxFoldersCount { - entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing)) - } + + entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing)) + entries.append(.listFooter(presentationData.strings.ChatListFolderSettings_EditFoldersInfo)) } @@ -284,6 +290,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch filtersWithCounts.get() |> take(1) ).start(next: { result, filters in let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false let filters = filters.filter { filter in if case .allChats = filter.0 { @@ -291,25 +298,24 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } return true } + let limit = limits.maxFoldersCount let premiumLimit = premiumLimits.maxFoldersCount - if let accountPeer = accountPeer, accountPeer.isPremium { - if filters.count >= premiumLimit { - return - } - } else { - if filters.count >= limit { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { - let controller = PremiumIntroScreen(context: context, source: .folders) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - pushControllerImpl?(controller) - return + if filters.count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {}) + pushControllerImpl?(controller) + return + } else if filters.count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { + let controller = PremiumIntroScreen(context: context, source: .folders) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } + pushControllerImpl?(controller) + return } let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters @@ -333,6 +339,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch filtersWithCounts.get() |> take(1) ).start(next: { result, filters in let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false let filters = filters.filter { filter in if case .allChats = filter.0 { @@ -340,25 +347,24 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } return true } + let limit = limits.maxFoldersCount let premiumLimit = premiumLimits.maxFoldersCount - if let accountPeer = accountPeer, accountPeer.isPremium { - if filters.count >= premiumLimit { - return - } - } else { - if filters.count >= limit { - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { - let controller = PremiumIntroScreen(context: context, source: .folders) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) - } - pushControllerImpl?(controller) - return + if filters.count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: {}) + pushControllerImpl?(controller) + return + } else if filters.count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { + let controller = PremiumIntroScreen(context: context, source: .folders) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) } + pushControllerImpl?(controller) + return } pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in })) }) @@ -426,9 +432,10 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), limits ) - |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, limits -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits -> (ItemListControllerState, (ItemListNodeState, Any)) in let isPremium = peer?.isPremium ?? false - let effectiveLimits = limits.1 + let limits = allLimits.0 + let premiumLimits = allLimits.1 let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings]?.get(ChatListFilterSettings.self) ?? ChatListFilterSettings.default let leftNavigationButton: ItemListNavigationButton? @@ -498,7 +505,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChatListFolderSettings_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium, limits: effectiveLimits), style: .blocks, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, settings: filterSettings, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits), style: .blocks, animateChanges: true) return (controllerState, (listState, arguments)) } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index 1489d68b6d..431fc37e46 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -200,9 +200,13 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN var updatedTheme: PresentationTheme? var updateArrowImage: UIImage? - if currentItem?.presentationData.theme !== item.presentationData.theme { + if currentItem?.presentationData.theme !== item.presentationData.theme || currentItem?.isDisabled != item.isDisabled { updatedTheme = item.presentationData.theme - updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + if item.isDisabled { + updateArrowImage = PresentationResourcesItemList.disclosureLockedImage(item.presentationData.theme) + } else { + updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme) + } } let peerRevealOptions: [ItemListRevealOption] @@ -381,9 +385,13 @@ 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) + var rightArrowInset = 0.0 + if item.isDisabled == true { + rightArrowInset -= 3.0 + } + strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + rightArrowInset + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) } - strongSelf.arrowNode.isHidden = item.isAllChats || item.isDisabled + strongSelf.arrowNode.isHidden = item.isAllChats 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)) @@ -452,8 +460,10 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } let leftInset: CGFloat = 16.0 + params.leftInset - let rightArrowInset: CGFloat = 34.0 + params.rightInset - + var rightArrowInset: CGFloat = 34.0 + params.rightInset + if self.item?.isDisabled == true { + rightArrowInset -= 3.0 + } let editingOffset: CGFloat if let editableControlNode = self.editableControlNode { editingOffset = editableControlNode.bounds.size.width diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index edd1eded0f..6ce6594fda 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -845,15 +845,10 @@ public final class ChatListNode: ListView { switch result { case .done: break - case let .limitExceeded(count, limit): + case let .limitExceeded(count, _): if isPremium { - let text: String - if chatListFilter != nil { - text = strongSelf.currentState.presentationData.strings.DialogList_UnknownPinLimitError - } else { - text = strongSelf.currentState.presentationData.strings.DialogList_PinLimitError("\(limit)").string - } - strongSelf.presentAlert?(text) + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {}) + strongSelf.push?(controller) } else { var replaceImpl: ((ViewController) -> Void)? let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { diff --git a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift index cc0eea2a92..df3d4fae55 100644 --- a/submodules/ComponentFlow/Source/Base/CombinedComponent.swift +++ b/submodules/ComponentFlow/Source/Base/CombinedComponent.swift @@ -99,7 +99,7 @@ public final class _ConcreteChildComponent: _AnyChildC view = current.view as! ComponentType.View } else { view = component.makeView() - transition = .immediate + transition = transition.withAnimation(.none) } let context = view.context(component: component) @@ -342,7 +342,7 @@ public final class _EnvironmentChildComponentFromMap: _AnyChild view = current.view } else { view = component._makeView() - transition = .immediate + transition = transition.withAnimation(.none) } let viewContext = view.context(component: component) diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 50c79ffad5..3e163879ad 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -115,6 +115,7 @@ public final class SheetComponent: Component { } private func animateOut(completion: @escaping () -> Void) { + self.isUserInteractionEnabled = false self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.bounds.height - self.scrollView.contentInset.top), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, additive: true, completion: { _ in completion() diff --git a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift index 0706a81724..2021776113 100644 --- a/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift +++ b/submodules/Components/SolidRoundedButtonComponent/Sources/SolidRoundedButtonComponent.swift @@ -104,9 +104,7 @@ public final class SolidRoundedButtonComponent: Component { cornerRadius: component.cornerRadius, gloss: component.gloss ) - button.iconPosition = component.iconPosition button.progressType = .embedded - button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) } self.button = button self.addSubview(button) @@ -117,6 +115,10 @@ public final class SolidRoundedButtonComponent: Component { if let button = self.button { button.title = component.title + button.iconPosition = component.iconPosition + button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) } + button.gloss = component.gloss + 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/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 9ea4f9cda7..5304bfa600 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -382,7 +382,7 @@ public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = tr }, opaque: opaque, scale: scale) } -private func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? { +public func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? { return generateImage(size, contextGenerator: { size, context in context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 3bfca16d19..589f0cc29d 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -45,6 +45,8 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/GZip:GZip", "//submodules/InstantPageCache:InstantPageCache", + "//submodules/MediaPlayer:UniversalMediaPlayer", + "//submodules/TelegramUniversalVideoContent:TelegramUniversalVideoContent", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/4gb.mp4 b/submodules/PremiumUI/Resources/4gb.mp4 new file mode 100644 index 0000000000..7c880e833f Binary files /dev/null and b/submodules/PremiumUI/Resources/4gb.mp4 differ diff --git a/submodules/PremiumUI/Resources/badge.mp4 b/submodules/PremiumUI/Resources/badge.mp4 new file mode 100644 index 0000000000..86e9b061a7 Binary files /dev/null and b/submodules/PremiumUI/Resources/badge.mp4 differ diff --git a/submodules/PremiumUI/Resources/fastdownload.mp4 b/submodules/PremiumUI/Resources/fastdownload.mp4 new file mode 100644 index 0000000000..f889234900 Binary files /dev/null and b/submodules/PremiumUI/Resources/fastdownload.mp4 differ diff --git a/submodules/PremiumUI/Resources/noads.mp4 b/submodules/PremiumUI/Resources/noads.mp4 new file mode 100644 index 0000000000..aaaafdd624 Binary files /dev/null and b/submodules/PremiumUI/Resources/noads.mp4 differ diff --git a/submodules/PremiumUI/Resources/voice.mp4 b/submodules/PremiumUI/Resources/voice.mp4 new file mode 100644 index 0000000000..eca8757fd2 Binary files /dev/null and b/submodules/PremiumUI/Resources/voice.mp4 differ diff --git a/submodules/PremiumUI/Sources/DemoComponent.swift b/submodules/PremiumUI/Sources/DemoComponent.swift deleted file mode 100644 index 8235b3d9a0..0000000000 --- a/submodules/PremiumUI/Sources/DemoComponent.swift +++ /dev/null @@ -1,44 +0,0 @@ -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/PageIndicatorComponent.swift b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift new file mode 100644 index 0000000000..bebb8907c9 --- /dev/null +++ b/submodules/PremiumUI/Sources/PageIndicatorComponent.swift @@ -0,0 +1,405 @@ +import Foundation +import UIKit +import ComponentFlow + +public final class PageIndicatorComponent: Component { + private let pageCount: Int + private let position: CGFloat + private let inactiveColor: UIColor + private let activeColor: UIColor + + public init( + pageCount: Int, + position: CGFloat, + inactiveColor: UIColor, + activeColor: UIColor + ) { + self.pageCount = pageCount + self.position = position + self.inactiveColor = inactiveColor + self.activeColor = activeColor + } + + public static func ==(lhs: PageIndicatorComponent, rhs: PageIndicatorComponent) -> Bool { + if lhs.pageCount != rhs.pageCount { + return false + } + if lhs.position != rhs.position { + return false + } + if !lhs.inactiveColor.isEqual(rhs.inactiveColor) { + return false + } + if !lhs.activeColor.isEqual(rhs.activeColor) { + return false + } + return true + } + + public final class View: UIView { + private var component: PageIndicatorComponent? + + private let indicatorView: PageIndicatorView + + public override init(frame: CGRect) { + self.indicatorView = PageIndicatorView(frame: frame) + + super.init(frame: frame) + + self.addSubview(self.indicatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(component: PageIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + self.indicatorView.pageCount = component.pageCount + self.indicatorView.setProgress(progress: component.position) + + self.indicatorView.activeColor = component.activeColor + self.indicatorView.inactiveColor = component.inactiveColor + + let size = self.indicatorView.intrinsicContentSize + self.indicatorView.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + 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 final class PageIndicatorView: UIView { + var displayCount: Int { + return min(9, self.pageCount) + } + var dotSize: CGFloat = 8.0 + var dotSpace: CGFloat = 10.0 + var smallDotSizeRatio: CGFloat = 0.5 + var mediumDotSizeRatio: CGFloat = 0.75 + + public func setCurrentPage(at currentPage: Int, animated: Bool = false) { + guard (currentPage < self.pageCount && currentPage >= 0) else { return } + guard currentPage != self.currentPage else { return } + + self.scrollView.layer.removeAllAnimations() + self.updateDot(at: currentPage, animated: animated) + self.currentPage = currentPage + } + + public private(set) var currentPage: Int = 0 + + public var pageCount: Int = 0 { + didSet { + guard self.pageCount != oldValue else { + return + } + self.update(currentPage: self.currentPage) + } + } + + public var inactiveColor: UIColor = .gray { + didSet { + guard !self.inactiveColor.isEqual(oldValue) else { + return + } + self.updateDotColor(currentPage: self.currentPage) + } + } + + public var activeColor: UIColor = .blue { + didSet { + guard !self.activeColor.isEqual(oldValue) else { + return + } + self.updateDotColor(currentPage: self.currentPage) + } + } + + public var animationDuration: Double = 0.3 + + public init() { + super.init(frame: .zero) + + self.setup() + self.updateViewSize() + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + self.setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func layoutSubviews() { + super.layoutSubviews() + + self.scrollView.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2) + } + + public override var intrinsicContentSize: CGSize { + return CGSize(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize) + } + + public func setProgress(progress: CGFloat) { + let currentPage = Int(round(progress * CGFloat(self.pageCount - 1))) + self.setCurrentPage(at: currentPage, animated: true) + } + + public func updateViewSize() { + self.bounds.size = intrinsicContentSize + } + + private let scrollView = UIScrollView() + + private var itemSize: CGFloat { + return self.dotSize + self.dotSpace + } + + private var items: [ItemView] = [] + + private func setup() { + self.backgroundColor = .clear + + self.scrollView.backgroundColor = .clear + self.scrollView.isUserInteractionEnabled = false + self.scrollView.showsHorizontalScrollIndicator = false + + self.addSubview(self.scrollView) + } + + private func update(currentPage: Int) { + if currentPage < self.displayCount { + self.items = (-2..<(self.displayCount + 2)) + .map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) } + } + else { + guard let firstItem = self.items.first else { return } + guard let lastItem = self.items.last else { return } + self.items = (firstItem.index...lastItem.index) + .map { ItemView(itemSize: self.itemSize, dotSize: self.dotSize, smallDotSizeRatio: self.smallDotSizeRatio, mediumDotSizeRatio: self.mediumDotSizeRatio, index: $0) } + } + + self.scrollView.contentSize = .init(width: self.itemSize * CGFloat(self.pageCount), height: self.itemSize) + + self.scrollView.subviews.forEach { $0.removeFromSuperview() } + self.items.forEach { self.scrollView.addSubview($0) } + + let size: CGSize = .init(width: self.itemSize * CGFloat(self.displayCount), height: self.itemSize) + + self.scrollView.bounds.size = size + + if self.displayCount < self.pageCount { + self.scrollView.contentInset = .init(top: 0.0, left: self.itemSize * 2.0, bottom: 0, right: self.itemSize * 2.0) + } else { + self.scrollView.contentInset = .zero + } + + self.updateDot(at: currentPage, animated: false) + } + + private func updateDot(at currentPage: Int, animated: Bool) { + self.updateDotColor(currentPage: currentPage) + + if self.pageCount > self.displayCount { + self.updateDotPosition(currentPage: currentPage, animated: animated) + self.updateDotSize(currentPage: currentPage, animated: animated) + } + } + + private func updateDotColor(currentPage: Int) { + self.items.forEach { + $0.dotColor = ($0.index == currentPage) ? + self.activeColor : self.inactiveColor + } + } + + private func updateDotPosition(currentPage: Int, animated: Bool) { + let duration = animated ? self.animationDuration : 0 + + if currentPage == 0 { + let x = -self.scrollView.contentInset.left + self.moveScrollView(x: x, duration: duration) + } + else if currentPage == self.pageCount - 1 { + let x = self.scrollView.contentSize.width - self.scrollView.bounds.width + self.scrollView.contentInset.right + self.moveScrollView(x: x, duration: duration) + } + else if CGFloat(currentPage) * self.itemSize <= self.scrollView.contentOffset.x + self.itemSize { + let x = self.scrollView.contentOffset.x - self.itemSize + self.moveScrollView(x: x, duration: duration) + } + else if CGFloat(currentPage) * self.itemSize + self.itemSize >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize { + let x = self.scrollView.contentOffset.x + self.itemSize + self.moveScrollView(x: x, duration: duration) + } + } + + private func updateDotSize(currentPage: Int, animated: Bool) { + let duration = animated ? self.animationDuration : 0 + + self.items.forEach { item in + item.animateDuration = duration + if item.index == currentPage { + item.state = .normal + } else if item.index < 0 { + item.state = .none + } else if item.index > self.pageCount - 1 { + item.state = .none + } else if item.frame.minX <= self.scrollView.contentOffset.x { + item.state = .small + } else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width { + item.state = .small + } else if item.frame.minX <= self.scrollView.contentOffset.x + self.itemSize { + item.state = .medium + } else if item.frame.maxX >= self.scrollView.contentOffset.x + self.scrollView.bounds.width - self.itemSize { + item.state = .medium + } else { + item.state = .normal + } + } + } + + private func moveScrollView(x: CGFloat, duration: TimeInterval) { + let direction = self.behaviorDirection(x: x) + self.reusedView(direction: direction) + UIView.animate(withDuration: duration, animations: { [unowned self] in + self.scrollView.contentOffset.x = x + }) + } + + private enum Direction { + case left + case right + case stay + } + + private func behaviorDirection(x: CGFloat) -> Direction { + switch x { + case let x where x > self.scrollView.contentOffset.x: + return .right + case let x where x < self.scrollView.contentOffset.x: + return .left + default: + return .stay + } + } + + private func reusedView(direction: Direction) { + guard let firstItem = self.items.first else { return } + guard let lastItem = self.items.last else { return } + + switch direction { + case .left: + lastItem.index = firstItem.index - 1 + lastItem.frame = CGRect(origin: CGPoint(x: CGFloat(lastItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize)) + self.items.insert(lastItem, at: 0) + self.items.removeLast() + + case .right: + firstItem.index = lastItem.index + 1 + firstItem.frame = CGRect(origin: CGPoint(x: CGFloat(firstItem.index) * self.itemSize, y: 0.0), size: CGSize(width: self.itemSize, height: self.itemSize)) + self.items.insert(firstItem, at: self.items.count) + self.items.removeFirst() + + case .stay: + break + } + } +} + + +private class ItemView: UIView { + enum State { + case none + case small + case medium + case normal + } + + private let dotView = UIView() + + var index: Int + + var dotColor = UIColor.lightGray { + didSet { + self.dotView.backgroundColor = dotColor + } + } + + var state: State = .normal { + didSet { + self.updateDotSize(state: state) + } + } + + private let itemSize: CGFloat + private let dotSize: CGFloat + private let smallSizeRatio: CGFloat + private let mediumSizeRatio: CGFloat + + var animateDuration: Double = 0.3 + + init(itemSize: CGFloat, dotSize: CGFloat, smallDotSizeRatio: CGFloat, mediumDotSizeRatio: CGFloat, index: Int) { + self.itemSize = itemSize + self.dotSize = dotSize + self.mediumSizeRatio = mediumDotSizeRatio + self.smallSizeRatio = smallDotSizeRatio + self.index = index + + let x = itemSize * CGFloat(index) + let frame = CGRect(x: x, y: 0, width: itemSize, height: itemSize) + + super.init(frame: frame) + + self.backgroundColor = UIColor.clear + + self.dotView.frame.size = CGSize(width: dotSize, height: dotSize) + self.dotView.center = CGPoint(x: itemSize / 2.0, y: itemSize / 2.0) + self.dotView.backgroundColor = self.dotColor + self.dotView.layer.cornerRadius = dotSize / 2.0 + self.dotView.layer.masksToBounds = true + + addSubview(dotView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateDotSize(state: State) { + var size: CGSize + + switch state { + case .normal: + size = CGSize(width: self.dotSize, height: self.dotSize) + case .medium: + size = CGSize(width: self.dotSize * self.mediumSizeRatio, height: self.dotSize * self.mediumSizeRatio) + case .small: + size = CGSize( width: self.dotSize * self.smallSizeRatio, height: self.dotSize * self.smallSizeRatio + ) + case .none: + size = CGSize.zero + } + + UIView.animate(withDuration: self.animateDuration, animations: { [unowned self] in + self.dotView.layer.cornerRadius = size.height / 2.0 + self.dotView.layer.bounds.size = size + }) + } +} + + diff --git a/submodules/PremiumUI/Sources/PhoneDemoComponent.swift b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift new file mode 100644 index 0000000000..28f59a8ca2 --- /dev/null +++ b/submodules/PremiumUI/Sources/PhoneDemoComponent.swift @@ -0,0 +1,392 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import ComponentFlow +import AccountContext + +import AppBundle +import UniversalMediaPlayer +import TelegramUniversalVideoContent + +private let phoneSize = CGSize(width: 262.0, height: 539.0) +private var phoneBorderImage = { + return generateImage(phoneSize, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.5).cgColor) + try? drawSvgPath(context, path: "M203.506,7.0 C211.281,0.0 217.844,0.0 223.221,0.439253 C228.851,0.899605 234.245,1.90219 239.377,4.51905 C247.173,8.49411 253.512,14.8369 257.484,22.6384 C260.099,27.7743 261.101,33.1718 261.561,38.8062 C262.0,44.1865 262.0,50.754 262.0,58.5351 V480.465 C262.0,488.246 262.0,494.813 261.561,500.194 C261.101,505.828 260.099,511.226 257.484,516.362 C253.512,524.163 247.173,530.506 239.377,534.481 C234.245,537.098 228.851,538.1 223.221,538.561 C217.844,539.0 211.281,539.0 203.506,539.0 H58.4942 C50.7185,539 44.1556,539.0 38.7791,538.561 C33.1486,538.1 27.7549,537.098 22.6226,534.481 C14.8265,530.506 8.48817,524.163 4.51589,516.362 C1.90086,511.226 0.898976,505.828 0.438946,500.194 C0.0,494.813 0.0,488.246 7.0,480.465 V58.5354 C0.0,50.7541 0.0,44.1866 0.438946,38.8062 C0.898976,33.1718 1.90086,27.7743 4.51589,22.6384 C8.48817,14.8369 14.8265,8.49411 22.6226,4.51905 C27.7549,1.90219 33.1486,0.899605 38.7791,0.439253 C44.1557,-0.0 50.7187,0.0 58.4945,7.0 H203.506 Z ") + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(x: 43.0, y: UIScreenPixel), size: CGSize(width: 175.0, height: 8.0))) + context.fill(CGRect(origin: CGPoint(x: UIScreenPixel, y: 43.0), size: CGSize(width: 8.0, height: 452.0))) + + context.setBlendMode(.clear) + try? drawSvgPath(context, path: "M15.3737,28.1746 C12.1861,34.4352 12.1861,42.6307 12.1861,59.0217 V479.978 C12.1861,496.369 12.1861,504.565 15.3737,510.825 C18.1777,516.332 22.6518,520.81 28.1549,523.615 C34.4111,526.805 42.6009,526.805 58.9805,526.805 H203.02 C219.399,526.805 227.589,526.805 233.845,523.615 C239.348,520.81 243.822,516.332 246.626,510.825 C249.814,504.565 249.814,496.369 249.814,479.978 V59.0217 C249.814,42.6307 249.814,34.4352 246.626,28.1746 C243.822,22.6677 239.348,18.1904 233.845,15.3845 C227.589,12.1946 219.399,12.1946 203.02,12.1946 H58.9805 C42.6009,12.1946 34.4111,12.1946 28.1549,15.3845 C22.6518,18.1904 18.1777,22.6677 15.3737,28.1746 Z ") + + context.setBlendMode(.copy) + context.setFillColor(UIColor.black.cgColor) + try? drawSvgPath(context, path: "M222.923,4.08542 C217.697,3.65815 211.263,3.65823 203.378,3.65833 H58.6219 C50.7366,3.65823 44.3026,3.65815 39.0768,4.08542 C33.6724,4.52729 28.8133,5.46834 24.2823,7.77863 C17.1741,11.4029 11.395,17.1861 7.77325,24.2992 C5.46457,28.8334 4.52418,33.6959 4.08262,39.1041 C3.65565,44.3336 3.65573,50.7721 3.65583,58.6628 V480.337 C3.65573,488.228 3.65565,494.666 4.08262,499.896 C4.52418,505.304 5.46457,510.167 7.77325,514.701 C11.395,521.814 17.1741,527.597 24.2823,531.221 C28.8133,533.532 33.6724,534.473 39.0768,534.915 C44.3028,535.342 50.737,535.342 58.6226,535.342 H203.377 C211.263,535.342 217.697,535.342 222.923,534.915 C228.328,534.473 233.187,533.532 237.718,531.221 C244.826,527.597 250.605,521.814 254.227,514.701 C256.535,510.167 257.476,505.304 257.917,499.896 C258.344,494.667 258.344,488.228 258.344,480.338 V58.6617 C258.344,50.7714 258.344,44.3333 257.917,39.1041 C257.476,33.6959 256.535,28.8334 254.227,24.2992 C250.605,17.1861 244.826,11.4029 237.718,7.77863 C233.187,5.46834 228.328,4.52729 222.923,4.08542 Z ") + + context.setBlendMode(.clear) + try? drawSvgPath(context, path: "M12.1861,59.0217 C12.1861,42.6306 12.1861,34.4351 15.3737,28.1746 C18.1777,22.6676 22.6519,18.1904 28.1549,15.3844 C34.4111,12.1945 42.6009,12.1945 58.9805,12.1945 H76.6868 L76.8652,12.1966 C78.1834,12.2201 79.0316,12.4428 79.7804,12.8418 C80.5733,13.2644 81.1963,13.8848 81.6226,14.6761 C81.9735,15.3276 82.1908,16.0553 82.2606,17.1064 C82.3128,22.5093 82.9306,24.5829 84.0474,26.6727 C85.2157,28.8587 86.9301,30.5743 89.1145,31.7434 C91.299,32.9124 93.4658,33.535 99.441,33.535 H162.561 C168.537,33.535 170.703,32.9124 172.888,31.7434 C175.072,30.5743 176.787,28.8587 177.955,26.6727 C179.072,24.5829 179.69,22.5093 179.742,17.1051 C179.812,16.0553 180.029,15.3276 180.38,14.6761 C180.806,13.8848 181.429,13.2644 182.222,12.8418 C182.971,12.4428 183.819,12.2201 185.137,12.1966 L185.316,12.1945 H203.02 C219.399,12.1945 227.589,12.1945 233.845,15.3844 C239.348,18.1904 243.822,22.6676 246.626,28.1746 C249.814,34.4351 249.814,42.6306 249.814,59.0217 V479.978 C249.814,496.369 249.814,504.565 246.626,510.825 C243.822,516.332 239.348,520.81 233.845,523.615 C227.589,526.805 219.399,526.805 203.02,526.805 H58.9805 C42.6009,526.805 34.4111,526.805 28.1549,523.615 C22.6519,520.81 18.1777,516.332 15.3737,510.825 C12.1861,504.565 12.1861,496.369 12.1861,479.978 V59.0217 Z") + }) +}() + +private final class PhoneView: UIView { + let contentContainerView: UIView + + let overlayView: UIView + let borderView: UIImageView + + fileprivate var videoNode: UniversalVideoNode? + + var screenRotation: CGFloat = 0.0 { + didSet { + if self.screenRotation > 0.0 { + self.overlayView.backgroundColor = .white + } else { + self.overlayView.backgroundColor = .black + } + self.contentContainerView.alpha = self.screenRotation > 0.0 ? 1.0 - self.screenRotation : 1.0 + self.overlayView.alpha = self.screenRotation > 0.0 ? self.screenRotation * 0.5 : self.screenRotation * -1.0 + } + } + + override init(frame: CGRect) { + self.contentContainerView = UIView() + self.contentContainerView.clipsToBounds = true + self.contentContainerView.backgroundColor = .darkGray + self.contentContainerView.layer.cornerRadius = 10.0 + + self.overlayView = UIView() + self.overlayView.backgroundColor = .black + + self.borderView = UIImageView(image: phoneBorderImage) + + super.init(frame: frame) + + self.addSubview(self.contentContainerView) + self.contentContainerView.addSubview(self.overlayView) + self.addSubview(self.borderView) + } + + + private var position: PhoneDemoComponent.Position = .top + + func setup(context: AccountContext, videoName: String?, position: PhoneDemoComponent.Position) { + self.position = position + + guard self.videoNode == nil, let videoName = videoName, let path = getAppBundle().path(forResource: videoName, ofType: "mp4"), let size = fileSize(path) else { + return + } + + self.contentContainerView.backgroundColor = .clear + + let dimensions = PixelDimensions(width: 1170, height: 1754) + + let id = Int64.random(in: 0.. Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.position != rhs.position { + return false + } + if lhs.videoName != rhs.videoName { + return false + } + return true + } + + final class View: UIView, ComponentTaggedView { + final class Tag { + } + + func matches(tag: Any) -> Bool { + if let _ = tag as? Tag, self.isCentral { + return true + } + return false + } + + private var isCentral = false + private var component: PhoneDemoComponent? + + private let containerView: UIView + private let phoneView: PhoneView + + public var ready: Signal { + if let videoNode = self.phoneView.videoNode { + return videoNode.ready + |> map { _ in + return true + } + } else { + return .single(true) + } + } + + public override init(frame: CGRect) { + self.containerView = UIView(frame: frame) + self.containerView.clipsToBounds = true + self.phoneView = PhoneView(frame: CGRect(origin: .zero, size: phoneSize)) + + super.init(frame: frame) + + self.addSubview(self.containerView) + self.containerView.addSubview(self.phoneView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func update(component: PhoneDemoComponent, availableSize: CGSize, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + + self.phoneView.setup(context: component.context, videoName: component.videoName, position: component.position) + + self.containerView.frame = CGRect(origin: .zero, size: availableSize) + self.phoneView.bounds = CGRect(origin: .zero, size: phoneSize) + + var mappedPosition = environment[DemoPageEnvironment.self].position + mappedPosition *= abs(mappedPosition) + + let phoneY: CGFloat + switch component.position { + case .top: + phoneY = phoneSize.height / 2.0 + 24.0 + abs(mappedPosition) * 24.0 + case .bottom: + phoneY = availableSize.height - phoneSize.height / 2.0 - 24.0 - abs(mappedPosition) * 24.0 + } + + let isVisible = environment[DemoPageEnvironment.self].isDisplaying + let isCentral = environment[DemoPageEnvironment.self].isCentral + self.isCentral = isCentral + + self.phoneView.center = CGPoint(x: availableSize.width / 2.0 + mappedPosition * 50.0, y: phoneY) + self.phoneView.screenRotation = mappedPosition * -0.7 + + var perspective = CATransform3DIdentity + perspective.m34 = mappedPosition / 50.0 + self.phoneView.layer.transform = CATransform3DRotate(perspective, 0.1, 0, 1, 0) + + if abs(mappedPosition) < .ulpOfOne { + self.phoneView.play() + } else if !isVisible { + self.phoneView.reset() + } + + if let _ = transition.userData(DemoAnimateInTransition.self), abs(mappedPosition) < .ulpOfOne { + let from: CGFloat + switch component.position { + case .top: + from = -200.0 + case .bottom: + from = 200.0 + } + self.containerView.layer.animateBoundsOriginYAdditive(from: from, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + 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 final class VideoDecoration: UniversalVideoDecoration { + public let backgroundNode: ASDisplayNode? = nil + public let contentContainerNode: ASDisplayNode + public let foregroundNode: ASDisplayNode? = nil + + private var contentNode: (ASDisplayNode & UniversalVideoContentNode)? + + private var validLayoutSize: CGSize? + + public init() { + self.contentContainerNode = ASDisplayNode() + } + + public func updateContentNode(_ contentNode: (UniversalVideoContentNode & ASDisplayNode)?) { + if self.contentNode !== contentNode { + let previous = self.contentNode + self.contentNode = contentNode + + if let previous = previous { + if previous.supernode === self.contentContainerNode { + previous.removeFromSupernode() + } + } + + if let contentNode = contentNode { + if contentNode.supernode !== self.contentContainerNode { + self.contentContainerNode.addSubnode(contentNode) + if let validLayoutSize = self.validLayoutSize { + contentNode.frame = CGRect(origin: CGPoint(), size: validLayoutSize) + contentNode.updateLayout(size: validLayoutSize, transition: .immediate) + } + } + } + } + } + + public func updateCorners(_ corners: ImageCorners) { + self.contentContainerNode.clipsToBounds = true + if isRoundEqualCorners(corners) { + self.contentContainerNode.cornerRadius = corners.topLeft.radius + } else { + let boundingSize: CGSize = CGSize(width: max(corners.topLeft.radius, corners.bottomLeft.radius) + max(corners.topRight.radius, corners.bottomRight.radius), height: max(corners.topLeft.radius, corners.topRight.radius) + max(corners.bottomLeft.radius, corners.bottomRight.radius)) + let size: CGSize = CGSize(width: boundingSize.width + corners.extendedEdges.left + corners.extendedEdges.right, height: boundingSize.height + corners.extendedEdges.top + corners.extendedEdges.bottom) + let arguments = TransformImageArguments(corners: corners, imageSize: size, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) + let context = DrawingContext(size: size, clear: true) + context.withContext { ctx in + ctx.setFillColor(UIColor.black.cgColor) + ctx.fill(arguments.drawingRect) + } + addCorners(context, arguments: arguments) + + if let maskImage = context.generateImage() { + let mask = CALayer() + mask.contents = maskImage.cgImage + mask.contentsScale = maskImage.scale + mask.contentsCenter = CGRect(x: max(corners.topLeft.radius, corners.bottomLeft.radius) / maskImage.size.width, y: max(corners.topLeft.radius, corners.topRight.radius) / maskImage.size.height, width: (maskImage.size.width - max(corners.topLeft.radius, corners.bottomLeft.radius) - max(corners.topRight.radius, corners.bottomRight.radius)) / maskImage.size.width, height: (maskImage.size.height - max(corners.topLeft.radius, corners.topRight.radius) - max(corners.bottomLeft.radius, corners.bottomRight.radius)) / maskImage.size.height) + + self.contentContainerNode.layer.mask = mask + self.contentContainerNode.layer.mask?.frame = self.contentContainerNode.bounds + } + } + } + + public func updateClippingFrame(_ frame: CGRect, completion: (() -> Void)?) { + self.contentContainerNode.layer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + + if let maskLayer = self.contentContainerNode.layer.mask { + maskLayer.animate(from: NSValue(cgRect: self.contentContainerNode.bounds), to: NSValue(cgRect: frame), keyPath: "bounds", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + + maskLayer.animate(from: NSValue(cgPoint: maskLayer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + }) + } + + if let contentNode = self.contentNode { + contentNode.layer.animate(from: NSValue(cgPoint: contentNode.layer.position), to: NSValue(cgPoint: CGPoint(x: frame.midX, y: frame.midY)), keyPath: "position", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + completion?() + }) + } + } + + public func updateContentNodeSnapshot(_ snapshot: UIView?) { + } + + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + self.validLayoutSize = size + + let bounds = CGRect(origin: CGPoint(), size: size) + if let backgroundNode = self.backgroundNode { + transition.updateFrame(node: backgroundNode, frame: bounds) + } + if let foregroundNode = self.foregroundNode { + transition.updateFrame(node: foregroundNode, frame: bounds) + } + transition.updateFrame(node: self.contentContainerNode, frame: bounds) + if let maskLayer = self.contentContainerNode.layer.mask { + transition.updateFrame(layer: maskLayer, frame: bounds) + } + if let contentNode = self.contentNode { + transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(), size: size)) + contentNode.updateLayout(size: size, transition: transition) + } + } + + public func setStatus(_ status: Signal) { + } + + public func tap() { + } +} diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index eaec2901f2..73f0196f60 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -130,10 +130,12 @@ private final class GradientBackgroundComponent: Component { final class DemoPageEnvironment: Equatable { public let isDisplaying: Bool public let isCentral: Bool + public let position: CGFloat - public init(isDisplaying: Bool, isCentral: Bool) { + public init(isDisplaying: Bool, isCentral: Bool, position: CGFloat) { self.isDisplaying = isDisplaying self.isCentral = isCentral + self.position = position } public static func ==(lhs: DemoPageEnvironment, rhs: DemoPageEnvironment) -> Bool { @@ -143,6 +145,9 @@ final class DemoPageEnvironment: Equatable { if lhs.isCentral != rhs.isCentral { return false } + if lhs.position != rhs.position { + return false + } return true } } @@ -192,8 +197,8 @@ private final class PageComponent: CombinedComponen 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 sideInset: CGFloat = 16.0 + let textSideInset: CGFloat = 24.0 let textColor = component.textColor let textFont = Font.regular(17.0) @@ -223,9 +228,14 @@ private final class PageComponent: CombinedComponen 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 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), @@ -269,28 +279,42 @@ private final class DemoPagerComponent: Component { } } - public let items: [Item] - public let index: Int + let items: [Item] + let index: Int + let activeColor: UIColor + let inactiveColor: UIColor public init( items: [Item], - index: Int = 0 + index: Int = 0, + activeColor: UIColor, + inactiveColor: UIColor ) { self.items = items self.index = index + self.activeColor = activeColor + self.inactiveColor = inactiveColor } public static func ==(lhs: DemoPagerComponent, rhs: DemoPagerComponent) -> Bool { if lhs.items != rhs.items { return false } + if !lhs.activeColor.isEqual(rhs.activeColor) { + return false + } + if !lhs.inactiveColor.isEqual(rhs.inactiveColor) { + return false + } return true } - public final class View: UIView, UIScrollViewDelegate { + fileprivate final class View: UIView, UIScrollViewDelegate { private let scrollView: UIScrollView private var itemViews: [AnyHashable: ComponentHostView] = [:] + private let pageIndicatorView: ComponentHostView + private var component: DemoPagerComponent? override init(frame: CGRect) { @@ -302,11 +326,15 @@ private final class DemoPagerComponent: Component { self.scrollView.bounces = false self.scrollView.layer.cornerRadius = 10.0 + self.pageIndicatorView = ComponentHostView() + self.pageIndicatorView.isUserInteractionEnabled = false + super.init(frame: frame) self.scrollView.delegate = self self.addSubview(self.scrollView) + self.addSubview(self.pageIndicatorView) } required init?(coder: NSCoder) { @@ -318,22 +346,10 @@ private final class DemoPagerComponent: Component { 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, isCentral: isDisplaying) - let _ = itemView.update( - transition: .immediate, - component: item.content.component, - environment: { environment }, - containerSize: self.bounds.size - ) - } - } + let _ = self.update(component: component, availableSize: self.bounds.size, transition: .immediate) } - func update(component: DemoPagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + func update(component: DemoPagerComponent, availableSize: CGSize, transition: Transition) -> CGSize { var validIds: [AnyHashable] = [] let firstTime = self.itemViews.isEmpty @@ -342,14 +358,30 @@ private final class DemoPagerComponent: Component { if self.scrollView.contentSize != contentSize { self.scrollView.contentSize = contentSize } - self.scrollView.frame = CGRect(origin: .zero, size: availableSize) + let scrollFrame = CGRect(origin: .zero, size: availableSize) + if self.scrollView.frame != scrollFrame { + self.scrollView.frame = scrollFrame + } if firstTime { self.scrollView.contentOffset = CGPoint(x: CGFloat(component.index) * availableSize.width, y: 0.0) } + let viewportCenter = self.scrollView.contentOffset.x + availableSize.width * 0.5 var i = 0 for item in component.items { + let itemFrame = CGRect(origin: CGPoint(x: availableSize.width * CGFloat(i), y: 0.0), size: availableSize) + let isDisplaying = itemFrame.intersects(self.scrollView.bounds) + + let centerDelta = itemFrame.midX - viewportCenter + let position = centerDelta / (availableSize.width * 0.75) + + i += 1 + + if abs(position) > 1.5 { + continue + } + validIds.append(item.content.id) let itemView: ComponentHostView @@ -363,11 +395,8 @@ private final class DemoPagerComponent: Component { self.itemViews[item.content.id] = itemView 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 environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: isDisplaying) + + let environment = DemoPageEnvironment(isDisplaying: isDisplaying, isCentral: abs(centerDelta) < CGFloat.ulpOfOne, position: position) let _ = itemView.update( transition: itemTransition, component: item.content.component, @@ -376,8 +405,6 @@ private final class DemoPagerComponent: Component { ) itemView.frame = itemFrame - - i += 1 } var removeIds: [AnyHashable] = [] @@ -393,6 +420,24 @@ private final class DemoPagerComponent: Component { self.component = component + if component.items.count > 1 { + let pageIndicatorComponent = PageIndicatorComponent( + pageCount: component.items.count, + position: self.scrollView.contentOffset.x / (self.scrollView.contentSize.width - availableSize.width), + inactiveColor: component.inactiveColor, + activeColor: component.activeColor + ) + let indicatorSize = self.pageIndicatorView.update( + transition: .immediate, + component: AnyComponent( + pageIndicatorComponent + ), + environment: {}, + containerSize: availableSize + ) + self.pageIndicatorView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - indicatorSize.width) / 2.0), y: availableSize.height - indicatorSize.height - 11.0), size: indicatorSize) + } + return availableSize } } @@ -402,10 +447,13 @@ private final class DemoPagerComponent: Component { } 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) + return view.update(component: self, availableSize: availableSize, transition: transition) } } +public final class DemoAnimateInTransition { +} + private final class DemoSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -440,56 +488,63 @@ private final class DemoSheetContent: CombinedComponent { private let context: AccountContext var cachedCloseImage: UIImage? + var isPremium: Bool? var reactions: [AvailableReactions.Reaction]? var stickers: [TelegramMediaFile]? - var reactionsDisposable: Disposable? - var stickersDisposable: Disposable? + var disposable: 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 [] + let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) + self.disposable = (combineLatest( + queue: Queue.mainQueue(), + self.context.engine.stickers.availableReactions(), + self.context.account.postbox.combinedView(keys: [stickersKey]) + |> map { views -> [OrderedItemListEntry]? in + if let view = views.views[stickersKey] as? OrderedItemListView, !view.items.isEmpty { + return view.items + } else { + return nil + } } - } - |> deliverOnMainQueue).start(next: { [weak self] reactions in + |> filter { items in + return items != nil + } + |> take(1), + self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + ) + |> map { reactions, items, accountPeer -> ([AvailableReactions.Reaction], [TelegramMediaFile], Bool?) in + if let reactions = reactions { + var result: [TelegramMediaFile] = [] + if let items = items { + for item in items { + if let mediaItem = item.contents.get(RecentMediaItem.self) { + result.append(mediaItem.media) + } + } + } + return (reactions.reactions.filter({ $0.isPremium }), result, accountPeer?.isPremium ?? false) + } else { + return ([], [], nil) + } + }).start(next: { [weak self] reactions, stickers, isPremium 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) + strongSelf.isPremium = isPremium + if !reactions.isEmpty && !stickers.isEmpty { + strongSelf.updated(transition: Transition(.immediate).withUserData(DemoAnimateInTransition())) + } }) } deinit { - self.reactionsDisposable?.dispose() - self.stickersDisposable?.dispose() + self.disposable?.dispose() } } @@ -502,7 +557,6 @@ private final class DemoSheetContent: CombinedComponent { 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 @@ -532,21 +586,28 @@ private final class DemoSheetContent: CombinedComponent { if let image = state.cachedCloseImage { closeImage = image } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: UIColor(rgb: 0xffffff))! + closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))! state.cachedCloseImage = closeImage } + var isStandalone = false + if case .other = component.source { + isStandalone = true + } + if let reactions = state.reactions, let stickers = state.stickers { let textColor = theme.actionSheet.primaryTextColor - let items: [DemoPagerComponent.Item] = [ + var items: [DemoPagerComponent.Item] = [ DemoPagerComponent.Item( AnyComponentWithIdentity( id: PremiumDemoScreen.Subject.moreUpload, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .bottom, + videoName: "4gb" )), title: strings.Premium_UploadSize, text: strings.Premium_UploadSizeInfo, @@ -560,8 +621,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.fasterDownload, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoName: "fastdownload" )), title: strings.Premium_FasterSpeed, text: strings.Premium_FasterSpeedInfo, @@ -575,8 +638,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.voiceToText, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoName: "voice" )), title: strings.Premium_VoiceToText, text: strings.Premium_VoiceToTextInfo, @@ -590,8 +655,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.noAds, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .bottom, + videoName: "noads" )), title: strings.Premium_NoAds, text: strings.Premium_NoAdsInfo, @@ -612,8 +679,8 @@ private final class DemoSheetContent: CombinedComponent { reactions: reactions ) ), - title: strings.Premium_Reactions, - text: strings.Premium_ReactionsInfo, + title: isStandalone ? strings.Premium_ReactionsStandalone : strings.Premium_Reactions, + text: isStandalone ? strings.Premium_ReactionsStandaloneInfo : strings.Premium_ReactionsInfo, textColor: textColor ) ) @@ -642,8 +709,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.advancedChatManagement, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoName: "fastdownload" )), title: strings.Premium_ChatManagement, text: strings.Premium_ChatManagementInfo, @@ -657,8 +726,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.profileBadge, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoName: "badge" )), title: strings.Premium_Badge, text: strings.Premium_BadgeInfo, @@ -672,8 +743,10 @@ private final class DemoSheetContent: CombinedComponent { id: PremiumDemoScreen.Subject.animatedUserpics, component: AnyComponent( PageComponent( - content: AnyComponent(DemoComponent( - context: component.context + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoName: "badge" )), title: strings.Premium_Avatar, text: strings.Premium_AvatarInfo, @@ -683,15 +756,26 @@ private final class DemoSheetContent: CombinedComponent { ) ) ] - let index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0 + let index: Int + switch component.source { + case .intro: + index = items.firstIndex(where: { (component.subject as AnyHashable) == $0.content.id }) ?? 0 + case .other: + items = items.filter { item in + return item.content.id == (component.subject as AnyHashable) + } + index = 0 + } let pager = pager.update( component: DemoPagerComponent( items: items, - index: index + index: index, + activeColor: UIColor(rgb: 0x7169ff), + inactiveColor: theme.list.disclosureArrowColor ), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.width + 154.0), - transition: .immediate + transition: context.transition ) context.add(pager .position(CGPoint(x: context.availableSize.width / 2.0, y: pager.size.height / 2.0)) @@ -700,7 +784,23 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: Button( - content: AnyComponent(Image(image: closeImage)), + content: AnyComponent(ZStack([ + AnyComponentWithIdentity( + id: "background", + component: AnyComponent( + BlurredRectangle( + color: UIColor(rgb: 0x888888, alpha: 0.1), + radius: 15.0 + ) + ) + ), + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent( + Image(image: closeImage) + ) + ), + ])), action: { [weak component] in component?.dismiss() } @@ -713,11 +813,15 @@ private final class DemoSheetContent: CombinedComponent { ) let buttonText: String - switch component.source { - case let .intro(price): - buttonText = strings.Premium_SubscribeFor(price ?? "–").string - case .other: - buttonText = strings.Premium_MoreAboutPremium + if state.isPremium == true { + buttonText = strings.Common_OK + } else { + switch component.source { + case let .intro(price): + buttonText = strings.Premium_SubscribeFor(price ?? "–").string + case .other: + buttonText = strings.Premium_MoreAboutPremium + } } let button = button.update( @@ -737,7 +841,7 @@ private final class DemoSheetContent: CombinedComponent { fontSize: 17.0, height: 50.0, cornerRadius: 10.0, - gloss: true, + gloss: state.isPremium != true, iconPosition: .right, action: { [weak component] in guard let component = component else { @@ -750,21 +854,17 @@ private final class DemoSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) + + var contentHeight: CGFloat = context.availableSize.width + 154.0 + if case .other = component.source { + contentHeight -= 40.0 + } - let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: context.availableSize.width + 154.0 + 20.0), size: button.size) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 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 @@ -875,9 +975,17 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { var disposed: () -> Void = {} + private var didSetReady = false + private let _ready = Promise() + public override var ready: Promise { + return self._ready + } + 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.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.navigationPresentation = .flatModal } @@ -894,5 +1002,18 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { self.view.disablesInteractiveModalDismiss = true } + + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + if !self.didSetReady { + self.didSetReady = true + if let view = self.node.hostView.findTaggedView(tag: PhoneDemoComponent.View.Tag()) as? PhoneDemoComponent.View { + self._ready.set(view.ready) + } else { + self._ready.set(.single(true) |> delay(0.1, queue: Queue.mainQueue())) + } + } + } } diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 1be2281a79..dbd9289734 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -3,6 +3,7 @@ import UIKit import Display import ComponentFlow import SwiftSignalKit +import Postbox import TelegramCore import TelegramPresentationData import PresentationDataUtils @@ -34,6 +35,7 @@ public enum PremiumSource: Equatable { case chatsPerFolder case accounts case deeplink(String?) + case profile(PeerId) var identifier: String { switch self { @@ -63,6 +65,8 @@ public enum PremiumSource: Equatable { return "double_limits__dialog_filters_chats" case .accounts: return "double_limits__accounts" + case .profile: + return "profile" case let .deeplink(reference): if let reference = reference { return "deeplink_\(reference)" @@ -727,15 +731,17 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let context: AccountContext let source: PremiumSource let isPremium: Bool? + let otherPeerName: String? let price: String? let present: (ViewController) -> Void let buy: () -> Void let updateIsFocused: (Bool) -> Void - init(context: AccountContext, source: PremiumSource, isPremium: Bool?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { + init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, price: String?, present: @escaping (ViewController) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { self.context = context self.source = source self.isPremium = isPremium + self.otherPeerName = otherPeerName self.price = price self.present = present self.buy = buy @@ -752,6 +758,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if lhs.isPremium != rhs.isPremium { return false } + if lhs.otherPeerName != rhs.otherPeerName { + return false + } if lhs.price != rhs.price { return false } @@ -874,13 +883,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) + let textString: String + if let _ = context.component.otherPeerName { + textString = strings.Premium_PersonalDescription + } else if context.component.isPremium == true { + textString = strings.Premium_SubscribedDescription + } else { + textString = strings.Premium_Description + } + 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: context.component.isPremium == true ? strings.Premium_SubscribedDescription : strings.Premium_Description, + text: textString, attributes: markdownAttributes ), horizontalAlignment: .center, @@ -917,6 +935,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let buy = context.component.buy let updateIsFocused = context.component.updateIsFocused + let isPremium = context.component.isPremium ?? false + var i = 0 for perk in state.configuration.perks { let iconBackgroundColors = gradientColors[i] @@ -942,6 +962,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var demoSubject: PremiumDemoScreen.Subject switch perk { case .doubleLimits: +// let controller = PremimLimitsListScreen(context: accountContext) +// present(controller) return case .moreUpload: demoSubject = .moreUpload @@ -970,7 +992,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { source: .intro(state?.price), action: { dismissImpl?() - buy() + if !isPremium { + buy() + } } ) controller.disposed = { @@ -1071,7 +1095,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let termsText = termsText.update( component: MultilineTextComponent( text: .markdown( - text: strings.Premium_Terms, + text: context.component.isPremium == true ? strings.Premium_ChargeInfo("$4.99", "–").string : strings.Premium_Terms, attributes: termsMarkdownAttributes ), horizontalAlignment: .natural, @@ -1088,24 +1112,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { tapAction: { attributes, _ in if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String, let controller = environment.controller() as? PremiumIntroScreen, let navigationController = controller.navigationController as? NavigationController { - let context = controller.context - let signal: Signal? - switch url { - case "terms": - signal = cachedTermsPage(context: context) - case "privacy": - signal = cachedPrivacyPage(context: context) - default: - signal = nil - } - if let signal = signal { - let _ = (signal - |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in - }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in - controller?.push(c) - }, dismissInput: {}, contentContext: nil) - }) + if url == "cancel" { + + } else { + let context = controller.context + let signal: Signal? + switch url { + case "terms": + signal = cachedTermsPage(context: context) + case "privacy": + signal = cachedPrivacyPage(context: context) + default: + signal = nil + } + if let signal = signal { + let _ = (signal + |> deliverOnMainQueue).start(next: { resolvedUrl in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak controller] c, arguments in + controller?.push(c) + }, dismissInput: {}, contentContext: nil) + }) + } } } } @@ -1126,17 +1154,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } } -private class BlurredRectangle: Component { +class BlurredRectangle: Component { let color: UIColor + let radius: CGFloat - init(color: UIColor) { + init(color: UIColor, radius: CGFloat = 0.0) { self.color = color + self.radius = radius } static func ==(lhs: BlurredRectangle, rhs: BlurredRectangle) -> Bool { if !lhs.color.isEqual(rhs.color) { return false } + if lhs.radius != rhs.radius { + return false + } return true } @@ -1158,7 +1191,7 @@ private class BlurredRectangle: Component { func update(component: BlurredRectangle, availableSize: CGSize, transition: Transition) -> CGSize { transition.setFrame(view: self.background.view, frame: CGRect(origin: CGPoint(), size: availableSize)) self.background.updateColor(color: component.color, transition: .immediate) - self.background.update(size: availableSize, cornerRadius: 0.0, transition: .immediate) + self.background.update(size: availableSize, cornerRadius: component.radius, transition: .immediate) return availableSize } @@ -1213,12 +1246,13 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var inProgress = false var premiumProduct: InAppPurchaseManager.Product? var isPremium: Bool? + var otherPeerName: String? private var disposable: Disposable? private var paymentDisposable = MetaDisposable() private var activationDisposable = MetaDisposable() - init(context: AccountContext, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) { + init(context: AccountContext, source: PremiumSource, updateInProgress: @escaping (Bool) -> Void, completion: @escaping () -> Void) { self.context = context self.updateInProgress = updateInProgress self.completion = completion @@ -1226,17 +1260,30 @@ private final class PremiumIntroScreenComponent: CombinedComponent { super.init() if let inAppPurchaseManager = context.sharedContext.inAppPurchaseManager { + let otherPeerName: Signal + if case let .profile(peerId) = source { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + otherPeerName = context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> map { peer -> String? in + return peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } + } else { + otherPeerName = .single(nil) + } + 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 + }, + otherPeerName + ).start(next: { [weak self] products, isPremium, otherPeerName in if let strongSelf = self { strongSelf.premiumProduct = products.first strongSelf.isPremium = isPremium + strongSelf.otherPeerName = otherPeerName strongSelf.updated(transition: .immediate) } }) @@ -1296,7 +1343,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, updateInProgress: self.updateInProgress, completion: self.completion) + return State(context: self.context, source: self.source, updateInProgress: self.updateInProgress, completion: self.completion) } static var body: Body { @@ -1306,6 +1353,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let topPanel = Child(BlurredRectangle.self) let topSeparator = Child(Rectangle.self) let title = Child(Text.self) + let secondaryTitle = Child(MultilineTextComponent.self) let bottomPanel = Child(BlurredRectangle.self) let bottomSeparator = Child(Rectangle.self) let button = Child(SolidRoundedButtonComponent.self) @@ -1343,9 +1391,16 @@ private final class PremiumIntroScreenComponent: CombinedComponent { transition: context.transition ) + let titleString: String + if state.isPremium == true { + titleString = environment.strings.Premium_SubscribedTitle + } else { + titleString = environment.strings.Premium_Title + } + let title = title.update( component: Text( - text: state.isPremium == true ? environment.strings.Premium_SubscribedTitle : environment.strings.Premium_Title, + text: titleString, font: Font.bold(28.0), color: environment.theme.rootController.navigationBar.primaryTextColor ), @@ -1353,55 +1408,38 @@ private final class PremiumIntroScreenComponent: CombinedComponent { transition: context.transition ) - let sideInset: CGFloat = 16.0 - let button = button.update( - component: SolidRoundedButtonComponent( - title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "—").string, - theme: SolidRoundedButtonComponent.Theme( - backgroundColor: UIColor(rgb: 0x8878ff), - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white - ), - height: 50.0, - cornerRadius: 10.0, - gloss: true, - isLoading: state.inProgress, - action: { - state.buy() - } + let textColor = environment.theme.list.itemPrimaryTextColor + let accentColor = UIColor(rgb: 0x597cf5) + + let textFont = Font.bold(18.0) + let boldTextFont = Font.bold(18.0) + + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { _ in + return nil + }) + let secondaryTitle = secondaryTitle.update( + component: MultilineTextComponent( + text: .markdown(text: state.otherPeerName.flatMap({ environment.strings.Premium_PersonalTitle($0).string }) ?? "", attributes: markdownAttributes), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 2, + lineSpacing: 0.0 ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), - transition: context.transition) + availableSize: context.availableSize, + transition: context.transition + ) let bottomPanelPadding: CGFloat = 12.0 let bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom + 5.0 : bottomPanelPadding - let bottomPanel = bottomPanel.update( - component: BlurredRectangle( - color: environment.theme.rootController.tabBar.backgroundColor - ), - availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset), - transition: context.transition - ) - - let bottomSeparator = bottomSeparator.update( - component: Rectangle( - color: environment.theme.rootController.tabBar.separatorColor - ), - availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), - transition: context.transition - ) - + let bottomPanelHeight: CGFloat = state.isPremium == true ? bottomInset : bottomPanelPadding + 50.0 + bottomInset + let scrollContent = scrollContent.update( component: ScrollComponent( content: AnyComponent(PremiumIntroScreenContentComponent( context: context.component.context, source: context.component.source, isPremium: state.isPremium, + otherPeerName: state.otherPeerName, price: state.premiumProduct?.price, present: context.component.present, buy: { [weak state] in @@ -1410,7 +1448,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { state?.updateIsFocused(isFocused) } )), - contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanel.size.height, right: 0.0), + contentInsets: UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: bottomPanelHeight, right: 0.0), contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in state?.topContentOffset = topContentOffset state?.bottomContentOffset = bottomContentOffset @@ -1445,17 +1483,25 @@ private final class PremiumIntroScreenComponent: CombinedComponent { let titleOffset: CGFloat let titleScale: CGFloat let titleOffsetDelta = (topInset + 160.0) - (environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0) - + let titleAlpha: CGFloat + if let topContentOffset = state.topContentOffset { topPanelAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0 let topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0 titleOffset = topContentOffset let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) titleScale = 1.0 - fraction * 0.36 + + if state.otherPeerName != nil { + titleAlpha = min(1.0, fraction * 1.5) + } else { + titleAlpha = 1.0 + } } else { topPanelAlpha = 0.0 titleScale = 1.0 titleOffset = 0.0 + titleAlpha = state.otherPeerName != nil ? 0.0 : 1.0 } context.add(star @@ -1475,27 +1521,79 @@ private final class PremiumIntroScreenComponent: CombinedComponent { context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0))) .scale(titleScale) + .opacity(titleAlpha) ) - let bottomPanelAlpha: CGFloat - if let bottomContentOffset = state.bottomContentOffset { - bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + context.add(secondaryTitle + .position(CGPoint(x: context.availableSize.width / 2.0, y: max(topInset + 160.0 - titleOffset, environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0))) + .scale(titleScale) + .opacity(1.0 - titleAlpha) + ) + + if state.isPremium == true { + } else { - bottomPanelAlpha = 1.0 + let sideInset: CGFloat = 16.0 + let button = button.update( + component: SolidRoundedButtonComponent( + title: environment.strings.Premium_SubscribeFor(state.premiumProduct?.price ?? "—").string, + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: UIColor(rgb: 0x8878ff), + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], + foregroundColor: .white + ), + height: 50.0, + cornerRadius: 10.0, + gloss: true, + isLoading: state.inProgress, + action: { + state.buy() + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - environment.safeInsets.left - environment.safeInsets.right, height: 50.0), + transition: context.transition) + + let bottomPanel = bottomPanel.update( + component: BlurredRectangle( + color: environment.theme.rootController.tabBar.backgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: bottomPanelPadding + button.size.height + bottomInset), + transition: context.transition + ) + + let bottomSeparator = bottomSeparator.update( + component: Rectangle( + color: environment.theme.rootController.tabBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let bottomPanelAlpha: CGFloat + if let bottomContentOffset = state.bottomContentOffset { + bottomPanelAlpha = min(16.0, bottomContentOffset) / 16.0 + } else { + bottomPanelAlpha = 1.0 + } + + context.add(bottomPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) + .opacity(bottomPanelAlpha) + ) + context.add(bottomSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) + .opacity(bottomPanelAlpha) + ) + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0)) + ) } - context.add(bottomPanel - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height / 2.0)) - .opacity(bottomPanelAlpha) - ) - context.add(bottomSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) - .opacity(bottomPanelAlpha) - ) - context.add(button - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height + bottomPanelPadding + button.size.height / 2.0)) - ) - return context.availableSize } } @@ -1510,6 +1608,10 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { return self._ready } + public weak var sourceView: UIView? + public weak var containerView: UIView? + public var animationColor: UIColor? + public init(context: AccountContext, modal: Bool = true, source: PremiumSource) { self.context = context @@ -1577,6 +1679,16 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { self.didSetReady = true self._ready.set(view.ready) + + if let sourceView = self.sourceView { + view.animateFrom = sourceView + view.containerView = self.containerView + view.animationColor = self.animationColor + + self.sourceView = nil + self.containerView = nil + self.animationColor = nil + } } } } diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index c660b7e8a9..64c9ea34d5 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -94,6 +94,7 @@ private class PremiumLimitAnimationComponent: Component { private let badgeMaskView: UIView private let badgeMaskBackgroundView: UIView private let badgeMaskArrowView: UIImageView + private let badgeMaskTailView: UIImageView private let badgeForeground: SimpleLayer private let badgeIcon: UIImageView private let badgeCountLabel: RollingLabel @@ -114,7 +115,6 @@ private class PremiumLimitAnimationComponent: Component { self.badgeView = UIView() self.badgeView.alpha = 0.0 - self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) self.badgeMaskBackgroundView = UIView() self.badgeMaskBackgroundView.backgroundColor = .white @@ -129,9 +129,23 @@ private class PremiumLimitAnimationComponent: Component { try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ") }) + self.badgeMaskTailView = UIImageView() + self.badgeMaskTailView.isHidden = true + + + let img = generateImage(CGSize(width: 44.0, height: 36.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 44.0, height: 24.0))) + context.translateBy(x: 22.0, y: 24.0) + try? drawSvgPath(context, path: "M0.0,0.0 H22.0 V4.75736 C22.0,7.43007 18.7686,8.76857 16.8787,6.87868 L11.7574,1.75736 C10.6321,0.632141 9.10602,0.0 7.51472,0.0 H0.0 Z ") + }) + self.badgeMaskTailView.image = img + self.badgeMaskView = UIView() self.badgeMaskView.addSubview(self.badgeMaskBackgroundView) self.badgeMaskView.addSubview(self.badgeMaskArrowView) + self.badgeMaskView.addSubview(self.badgeMaskTailView) self.badgeMaskView.layer.rasterizationScale = UIScreenScale self.badgeMaskView.layer.shouldRasterize = true self.badgeView.mask = self.badgeMaskView @@ -254,12 +268,22 @@ private class PremiumLimitAnimationComponent: Component { self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0)) 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.badgeMaskTailView.frame = CGRect(origin: CGPoint(x: badgeSize.width - 44.0, y: badgeSize.height - 36.0), size: CGSize(width: 44.0, height: 36.0)) + self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize) 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) + self.badgeView.layer.anchorPoint = CGPoint(x: 1.0, y: 1.0) + + self.badgeMaskTailView.isHidden = false + self.badgeMaskArrowView.isHidden = true + + self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition + 3.0, y: 82.0) } else { + self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) + + self.badgeMaskTailView.isHidden = true + self.badgeMaskArrowView.isHidden = false + self.badgeView.center = CGPoint(x: 3.0 + (availableSize.width - 6.0) * component.badgePosition, y: 82.0) if self.badgeView.frame.maxX > availableSize.width { @@ -575,6 +599,7 @@ private final class LimitSheetContent: CombinedComponent { var initialized = false var limits: EngineConfiguration.UserLimits var premiumLimits: EngineConfiguration.UserLimits + var isPremium = false var cachedCloseImage: (UIImage, PresentationTheme)? @@ -587,13 +612,15 @@ private final class LimitSheetContent: CombinedComponent { self.disposable = (context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), - TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true), + TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId) ) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { - let (limits, premiumLimits) = result + let (limits, premiumLimits, accountPeer) = result strongSelf.initialized = true strongSelf.limits = limits strongSelf.premiumLimits = premiumLimits + strongSelf.isPremium = accountPeer?.isPremium ?? false strongSelf.updated(transition: .immediate) } }) @@ -648,7 +675,7 @@ private final class LimitSheetContent: CombinedComponent { context.add(closeButton .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 @@ -664,16 +691,16 @@ private final class LimitSheetContent: CombinedComponent { badgeText = "\(component.count)" string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = "\(premiumLimit)" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) - case .chatsInFolder: + case .chatsPerFolder: let limit = state.limits.maxFolderChatsCount let premiumLimit = state.premiumLimits.maxFolderChatsCount iconName = "Premium/Chat" badgeText = "\(component.count)" string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = "\(premiumLimit)" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .pins: let limit = state.limits.maxPinnedChatCount @@ -682,7 +709,7 @@ private final class LimitSheetContent: CombinedComponent { badgeText = "\(component.count)" string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string defaultValue = component.count > limit ? "\(limit)" : "" - premiumValue = "\(premiumLimit)" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .files: let limit = Int64(state.limits.maxUploadFileParts) * 512 * 1024 + 1024 * 1024 * 100 @@ -690,10 +717,27 @@ private final class LimitSheetContent: CombinedComponent { 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 + defaultValue = component.count == 4 ? dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" + premiumValue = component.count != 4 ? dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) : "" + badgePosition = component.count == 4 ? 1.0 : 0.5 titleText = strings.Premium_FileTooLarge + case .accounts: + let limit = 3 + let premiumLimit = component.count + 1 + iconName = "Premium/Account" + badgeText = "\(component.count)" + string = strings.Premium_MaxAccountsText("\(component.count)").string + defaultValue = component.count > limit ? "\(limit)" : "" + premiumValue = component.count >= premiumLimit ? "" : "\(premiumLimit)" + if component.count == limit { + badgePosition = 0.5 + } else { + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) + } + } + var reachedMaximumLimit = badgePosition >= 1.0 + if case .folders = subject, !state.isPremium { + reachedMaximumLimit = false } let title = title.update( @@ -759,7 +803,7 @@ private final class LimitSheetContent: CombinedComponent { let button = button.update( component: SolidRoundedButtonComponent( - title: strings.Premium_IncreaseLimit, + title: !reachedMaximumLimit ? strings.Premium_IncreaseLimit : strings.Common_OK, theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, backgroundColors: [ @@ -774,8 +818,8 @@ private final class LimitSheetContent: CombinedComponent { fontSize: 17.0, height: 50.0, cornerRadius: 10.0, - gloss: true, - iconName: "Premium/X2", + gloss: !reachedMaximumLimit, + iconName: !reachedMaximumLimit ? "Premium/X2" : nil, iconPosition: .right, action: { [weak component] in guard let component = component else { @@ -890,9 +934,10 @@ private final class LimitSheetComponent: CombinedComponent { public class PremiumLimitScreen: ViewControllerComponentContainer { public enum Subject { case folders - case chatsInFolder + case chatsPerFolder case pins case files + case accounts } public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) { diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift new file mode 100644 index 0000000000..b93415a690 --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -0,0 +1,1111 @@ +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 MultilineTextComponent +import BundleIconComponent + +private final class PremimLimitsListScreenComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let expand: () -> Void + + init(context: AccountContext, expand: @escaping () -> Void) { + self.context = context + self.expand = expand + } + + static func ==(lhs: PremimLimitsListScreenComponent, rhs: PremimLimitsListScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + init(context: AccountContext) { + self.context = context + + super.init() + + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + return { context in +// let environment = context.environment[ViewControllerComponentContainer.Environment.self].value +// let state = context.state +// let theme = environment.theme +// let strings = environment.strings +// return CGSize(width: context.availableSize.width, height: environment.navigationHeight + image.size.height + environment.safeInsets.bottom) + return context.availableSize + } + } +} + +public class PremimLimitsListScreen: ViewController { + final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { + private var presentationData: PresentationData + private weak var controller: PremimLimitsListScreen? + + private let component: AnyComponent + private let theme: PresentationTheme? + + let dim: ASDisplayNode + let wrappingView: UIView + let containerView: UIView + let scrollView: UIScrollView + let hostView: ComponentHostView + + private(set) var isExpanded = false + private var panGestureRecognizer: UIPanGestureRecognizer? + private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? + + private var currentIsVisible: Bool = false + private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? + + fileprivate var temporaryDismiss = false + + init(context: AccountContext, controller: PremimLimitsListScreen, component: AnyComponent, theme: PresentationTheme?) { + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + self.controller = controller + + self.component = component + self.theme = theme + + self.dim = ASDisplayNode() + self.dim.alpha = 0.0 + self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) + + self.wrappingView = UIView() + self.containerView = UIView() + self.scrollView = UIScrollView() + self.hostView = ComponentHostView() + + super.init() + + self.scrollView.delegate = self + self.scrollView.showsVerticalScrollIndicator = false + + self.containerView.clipsToBounds = true + self.containerView.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.addSubnode(self.dim) + + self.view.addSubview(self.wrappingView) + self.wrappingView.addSubview(self.containerView) + self.containerView.addSubview(self.scrollView) + self.scrollView.addSubview(self.hostView) + } + + override func didLoad() { + super.didLoad() + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + panRecognizer.delegate = self + panRecognizer.delaysTouchesBegan = false + panRecognizer.cancelsTouchesInView = true + self.panGestureRecognizer = panRecognizer + self.wrappingView.addGestureRecognizer(panRecognizer) + + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + + self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.controller?.dismiss(animated: true) + } + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let (layout, _) = self.currentLayout { + if case .regular = layout.metrics.widthClass { + return false + } + } + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let contentOffset = self.scrollView.contentOffset.y + self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { + return true + } + return false + } + + private var isDismissing = false + func animateIn() { + ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) + + let targetPosition = self.containerView.center + let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) + + self.containerView.center = startPosition + let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) + transition.animateView(allowUserInteraction: true, { + self.containerView.center = targetPosition + }, completion: { _ in + }) + } + + func animateOut(completion: @escaping () -> Void = {}) { + self.isDismissing = true + + let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in + self?.controller?.dismiss(animated: false, completion: completion) + }) + let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) + alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) + + if !self.temporaryDismiss { + self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) + } + } + + func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + self.currentLayout = (layout, navigationHeight) + + if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { + self.containerView.addSubview(navigationBar.view) + } + + self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) + + var effectiveExpanded = self.isExpanded + if case .regular = layout.metrics.widthClass { + effectiveExpanded = true + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset + let topInset: CGFloat + if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { + if effectiveExpanded { + topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) + } else { + topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) + } + } else { + topInset = effectiveExpanded ? 0.0 : edgeTopInset + } + transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) + + let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) + + let clipFrame: CGRect + if layout.metrics.widthClass == .compact { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) + if isLandscape { + self.containerView.layer.cornerRadius = 0.0 + } else { + self.containerView.layer.cornerRadius = 10.0 + } + + if #available(iOS 11.0, *) { + if layout.safeInsets.bottom.isZero { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } else { + self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + } + } + + if isLandscape { + clipFrame = CGRect(origin: CGPoint(), size: layout.size) + } else { + let coveredByModalTransition: CGFloat = 0.0 + var containerTopInset: CGFloat = 10.0 + if let statusBarHeight = layout.statusBarHeight { + containerTopInset += statusBarHeight + } + + let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) + let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width + let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition + let maxScaledTopInset: CGFloat = containerTopInset - 10.0 + let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition + let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) + + clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) + } + } else { + self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) + self.containerView.layer.cornerRadius = 10.0 + + let verticalInset: CGFloat = 44.0 + + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + } + + transition.setFrame(view: self.containerView, frame: clipFrame) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) + + let environment = ViewControllerComponentContainer.Environment( + statusBarHeight: 0.0, + navigationHeight: navigationHeight, + safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), + isVisible: self.currentIsVisible, + theme: self.theme ?? self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + controller: { [weak self] in + return self?.controller + } + ) + var contentSize = self.hostView.update( + transition: transition, + component: self.component, + environment: { + environment + }, + forceUpdate: true, + containerSize: CGSize(width: clipFrame.size.width, height: 10000.0) + ) + contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) + transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) + + self.scrollView.contentSize = contentSize + } + + private var didPlayAppearAnimation = false + func updateIsVisible(isVisible: Bool) { + if self.currentIsVisible == isVisible { + return + } + self.currentIsVisible = isVisible + + guard let currentLayout = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + self.animateIn() + } + } + + private var defaultTopInset: CGFloat { + guard let (layout, _) = self.currentLayout else{ + return 210.0 + } + if case .compact = layout.metrics.widthClass { + var factor: CGFloat = 0.2488 + if layout.size.width <= 320.0 { + factor = 0.15 + } + return floor(max(layout.size.width, layout.size.height) * factor) + } else { + return 210.0 + } + } + + private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { + if let view = view { + if let view = view as? UIScrollView { + return (view, nil) + } + if let node = view.asyncdisplaykit_node as? ListView { + return (node.scroller, node) + } + return findScrollView(view: view.superview) + } else { + return nil + } + } + + @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + + let isLandscape = layout.orientation == .landscape + let edgeTopInset = isLandscape ? 0.0 : defaultTopInset + + switch recognizer.state { + case .began: + let point = recognizer.location(in: self.view) + let currentHitView = self.hitTest(point, with: nil) + + var scrollViewAndListNode = self.findScrollView(view: currentHitView) + if scrollViewAndListNode?.0.frame.height == self.frame.width { + scrollViewAndListNode = nil + } + let scrollView = scrollViewAndListNode?.0 + let listNode = scrollViewAndListNode?.1 + + let topInset: CGFloat + if self.isExpanded { + topInset = 0.0 + } else { + topInset = edgeTopInset + } + + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) + case .changed: + guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + var translation = recognizer.translation(in: self.view).y + + var currentOffset = topInset + translation + + let epsilon = 1.0 + if case let .known(value) = visibleContentOffset, value <= epsilon { + if let scrollView = scrollView { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) + } + } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { + scrollView.bounces = false + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } else if let scrollView = scrollView { + translation = panOffset + currentOffset = topInset + translation + if self.isExpanded { + recognizer.setTranslation(CGPoint(), in: self.view) + } else if currentOffset > 0.0 { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + } + + self.panGestureArguments = (topInset, translation, scrollView, listNode) + + if !self.isExpanded { + if currentOffset > 0.0, let scrollView = scrollView { + scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + self.bounds = bounds + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) + case .ended: + guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { + return + } + self.panGestureArguments = nil + + let visibleContentOffset = listNode?.visibleContentOffset() + let contentOffset = scrollView?.contentOffset.y ?? 0.0 + + let translation = recognizer.translation(in: self.view).y + var velocity = recognizer.velocity(in: self.view) + + if self.isExpanded { + if case let .known(value) = visibleContentOffset, value > 0.1 { + velocity = CGPoint() + } else if case .unknown = visibleContentOffset { + velocity = CGPoint() + } else if contentOffset > 0.1 { + velocity = CGPoint() + } + } + + var bounds = self.bounds + if self.isExpanded { + bounds.origin.y = -max(0.0, translation - edgeTopInset) + } else { + bounds.origin.y = -translation + } + bounds.origin.y = min(0.0, bounds.origin.y) + + scrollView?.bounces = true + + let offset = currentTopInset + panOffset + let topInset: CGFloat = edgeTopInset + + var dismissing = false + if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { + self.controller?.dismiss(animated: true, completion: nil) + dismissing = true + } else if self.isExpanded { + if velocity.y > 300.0 || offset > topInset / 2.0 { + self.isExpanded = false + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + let distance = topInset - offset + let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + } else if (velocity.y < -300.0 || offset < topInset / 2.0) { + if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { + DispatchQueue.main.async { + listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + } + + let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) + let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) + self.isExpanded = true + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } else { + if let listNode = listNode { + listNode.scroller.setContentOffset(CGPoint(), animated: false) + } else if let scrollView = scrollView { + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) + } + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + } + + if !dismissing { + var bounds = self.bounds + let previousBounds = bounds + bounds.origin.y = 0.0 + self.bounds = bounds + self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) + } + case .cancelled: + self.panGestureArguments = nil + + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) + default: + break + } + } + + func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { + guard isExpanded != self.isExpanded else { + return + } + self.isExpanded = isExpanded + + guard let (layout, navigationHeight) = self.currentLayout else { + return + } + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } + } + + var node: Node { + return self.displayNode as! Node + } + + private let context: AccountContext + private let theme: PresentationTheme? + private let component: AnyComponent + private var isInitiallyExpanded = false + + private var currentLayout: ContainerViewLayout? + + public convenience init(context: AccountContext) { + var expandImpl: (() -> Void)? + self.init(context: context, component: PremimLimitsListScreenComponent(context: context, expand: { + expandImpl?() + })) + + self.title = "Doubled Limits" + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) + + let rightBarButtonNode = ASImageNode() + rightBarButtonNode.image = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xededed), foregroundColor: UIColor(rgb: 0x7f8084)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: rightBarButtonNode) + + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + + expandImpl = { [weak self] in + self?.node.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) + if let currentLayout = self?.currentLayout { + self?.containerLayoutUpdated(currentLayout, transition: .animated(duration: 0.4, curve: .spring)) + } + } + } + + private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + self.context = context + self.component = AnyComponent(component) + self.theme = nil + + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func cancelPressed() { + self.dismiss(animated: true, completion: nil) + } + + override open func loadDisplayNode() { + self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) + if self.isInitiallyExpanded { + (self.displayNode as! Node).update(isExpanded: true, transition: .immediate) + } + self.displayNodeDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + self.view.endEditing(true) + if flag { + self.node.animateOut(completion: { + super.dismiss(animated: false, completion: {}) + completion?() + }) + } else { + super.dismiss(animated: false, completion: {}) + completion?() + } + } + + override open func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.node.updateIsVisible(isVisible: true) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.node.updateIsVisible(isVisible: false) + } + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var navigationLayout = self.navigationLayout(layout: layout) + var navigationFrame = navigationLayout.navigationFrame + + var layout = layout + if case .regular = layout.metrics.widthClass { + let verticalInset: CGFloat = 44.0 + let maxSide = max(layout.size.width, layout.size.height) + let minSide = min(layout.size.width, layout.size.height) + let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) + let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) + navigationFrame.size.width = clipFrame.width + layout.size = clipFrame.size + } + + navigationFrame.size.height = 56.0 + navigationLayout.navigationFrame = navigationFrame + navigationLayout.defaultContentHeight = 56.0 + + layout.statusBarHeight = nil + + self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, transition: transition) + } + + override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.currentLayout = layout + super.containerLayoutUpdated(layout, transition: transition) + + let navigationHeight: CGFloat = 56.0 + + self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) + } +} + + +//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 PremiumLimitsListContent: 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, action: @escaping () -> Void, dismiss: @escaping () -> Void) { +// self.context = context +// self.action = action +// self.dismiss = dismiss +// } +// +// static func ==(lhs: PremiumLimitsListContent, rhs: PremiumLimitsListContent) -> Bool { +// if lhs.context !== rhs.context { +// return false +// } +// return true +// } +// +// final class State: ComponentState { +// private let context: AccountContext +// var cachedCloseImage: UIImage? +// +// var limits: EngineConfiguration.UserLimits = .defaultValue +// var premiumLimits: EngineConfiguration.UserLimits = .defaultValue +// var disposable: Disposable? +// +// init(context: AccountContext) { +// self.context = context +// +// super.init() +// +// self.disposable = (self.context.engine.data.get( +// TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), +// TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) +// ) +// |> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in +// guard let strongSelf = self else { +// return +// } +// strongSelf.limits = limits +// strongSelf.premiumLimits = premiumLimits +// strongSelf.updated(transition: .immediate) +// }) +// } +// +// deinit { +// self.disposable?.dispose() +// } +// } +// +// func makeState() -> State { +// return State(context: self.context) +// } +// +// static var body: Body { +// let closeButton = Child(Button.self) +// let button = Child(SolidRoundedButtonComponent.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 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 +// } +// +// 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 contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) +// +// return contentSize +// } +// } +//} +// +// +//private final class PremiumLimitsListComponent: CombinedComponent { +// typealias EnvironmentType = ViewControllerComponentContainer.Environment +// +// let context: AccountContext +// let action: () -> Void +// +// init(context: AccountContext, action: @escaping () -> Void) { +// self.context = context +// self.action = action +// } +// +// static func ==(lhs: PremiumLimitsListComponent, rhs: PremiumLimitsListComponent) -> Bool { +// if lhs.context !== rhs.context { +// 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(PremiumLimitsListContent( +// context: context.component.context, +// 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 PremiumLimitsListScreen: ViewControllerComponentContainer { +// var disposed: () -> Void = {} +// +// public init(context: AccountContext, action: @escaping () -> Void) { +// super.init(context: context, component: PremiumLimitsListComponent(context: context, action: action), navigationBarAppearance: .none) +// +// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) +// +// 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 +// } +//} +// +// +// +// +// +//public final class ExpandingSheetEnvironment: Equatable { +// public let isDisplaying: Bool +// public let dismiss: () -> Void +// +// public init(isDisplaying: Bool, dismiss: @escaping () -> Void) { +// self.isDisplaying = isDisplaying +// self.dismiss = dismiss +// } +// +// public static func ==(lhs: ExpandingSheetEnvironment, rhs: ExpandingSheetEnvironment) -> Bool { +// if lhs.isDisplaying != rhs.isDisplaying { +// return false +// } +// return true +// } +//} +// +//public final class ExpandingSheetComponent: Component { +// public typealias EnvironmentType = (ChildEnvironmentType, SheetComponentEnvironment) +// +// public let content: AnyComponent +// public let backgroundColor: UIColor +// public let animateOut: ActionSlot> +// +// public init(content: AnyComponent, backgroundColor: UIColor, animateOut: ActionSlot>) { +// self.content = content +// self.backgroundColor = backgroundColor +// self.animateOut = animateOut +// } +// +// public static func ==(lhs: ExpandingSheetComponent, rhs: ExpandingSheetComponent) -> Bool { +// if lhs.content != rhs.content { +// return false +// } +// if lhs.backgroundColor != rhs.backgroundColor { +// return false +// } +// if lhs.animateOut != rhs.animateOut { +// return false +// } +// +// return true +// } +// +// public final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate { +// private let dimView: UIView +// private let wrappingView: UIView +// private let containerView: UIView +// private let scrollView: UIScrollView +// private let contentView: ComponentHostView +// +// private(set) var isExpanded = false +// private var panGestureRecognizer: UIPanGestureRecognizer? +// private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? +// +// private var previousIsDisplaying: Bool = false +// private var dismiss: (() -> Void)? +// +// override init(frame: CGRect) { +// self.dimView = UIView() +// self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.4) +// +// self.wrappingView = UIView() +// self.containerView = UIView() +// self.scrollView = UIScrollView() +// if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { +// self.scrollView.contentInsetAdjustmentBehavior = .never +// } +// +// self.contentView = ComponentHostView() +// +// super.init(frame: frame) +// +// self.addSubview(self.dimView) +// +// self.scrollView.delegate = self +// self.scrollView.showsVerticalScrollIndicator = false +// +// self.containerView.clipsToBounds = true +// +// self.addSubview(self.wrappingView) +// self.wrappingView.addSubview(self.containerView) +// self.containerView.addSubview(self.scrollView) +// self.scrollView.addSubview(self.contentView) +// +// self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimViewTapGesture(_:)))) +// +// let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) +// panRecognizer.delegate = self +// panRecognizer.delaysTouchesBegan = false +// panRecognizer.cancelsTouchesInView = true +// self.panGestureRecognizer = panRecognizer +// self.wrappingView.addGestureRecognizer(panRecognizer) +// } +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// @objc private func dimViewTapGesture(_ recognizer: UITapGestureRecognizer) { +// if case .ended = recognizer.state { +// self.dismiss?() +// } +// } +// +// override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { +// if let (layout, _) = self.currentLayout { +// if case .regular = layout.metrics.widthClass { +// return false +// } +// } +// return true +// } +// +// func scrollViewDidScroll(_ scrollView: UIScrollView) { +// let contentOffset = self.scrollView.contentOffset.y +// self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) +// } +// +// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { +// if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { +// return true +// } +// return false +// } +// +// +// +// override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { +// if !self.backgroundView.bounds.contains(self.convert(point, to: self.backgroundView)) { +// return self.dimView +// } +// +// return super.hitTest(point, with: event) +// } +// +// private func animateOut(completion: @escaping () -> Void) { +// self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) +// self.scrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: self.bounds.height - self.scrollView.contentInset.top), duration: 0.25, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false, additive: true, completion: { _ in +// completion() +// }) +// } +// +// func update(component: ExpandingSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { +// component.animateOut.connect { [weak self] completion in +// guard let strongSelf = self else { +// return +// } +// strongSelf.animateOut { +// completion(Void()) +// } +// } +// +// if self.backgroundView.backgroundColor != component.backgroundColor { +// self.backgroundView.backgroundColor = component.backgroundColor +// } +// +// transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) +// +// let contentSize = self.contentView.update( +// transition: transition, +// component: component.content, +// environment: { +// environment[ChildEnvironmentType.self] +// }, +// containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) +// ) +// +// transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) +// transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) +// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize), completion: nil) +// self.scrollView.contentSize = contentSize +// self.scrollView.contentInset = UIEdgeInsets(top: max(0.0, availableSize.height - contentSize.height), left: 0.0, bottom: 0.0, right: 0.0) +// +// if environment[SheetComponentEnvironment.self].value.isDisplaying, !self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateInTransition.self) { +// self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) +// self.scrollView.layer.animatePosition(from: CGPoint(x: 0.0, y: availableSize.height - self.scrollView.contentInset.top), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: nil) +// } else if !environment[SheetComponentEnvironment.self].value.isDisplaying, self.previousIsDisplaying, let _ = transition.userData(ViewControllerComponentContainer.AnimateOutTransition.self) { +// self.animateOut(completion: {}) +// } +// self.previousIsDisplaying = environment[SheetComponentEnvironment.self].value.isDisplaying +// +// self.dismiss = environment[SheetComponentEnvironment.self].value.dismiss +// +// 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) +// } +//} diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift index 89b6e16870..909e7cfd8d 100644 --- a/submodules/PremiumUI/Sources/PremiumStarComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -47,7 +47,7 @@ private func generateDiffuseTexture() -> UIImage { class PremiumStarComponent: Component { let isVisible: Bool let hasIdleAnimations: Bool - + init(isVisible: Bool, hasIdleAnimations: Bool) { self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations @@ -73,6 +73,10 @@ class PremiumStarComponent: Component { return self._ready.get() } + weak var animateFrom: UIView? + weak var containerView: UIView? + var animationColor: UIColor? + private let sceneView: SCNView private var previousInteractionTimestamp: Double = 0.0 @@ -80,7 +84,7 @@ class PremiumStarComponent: Component { private var hasIdleAnimations = false override init(frame: CGRect) { - self.sceneView = SCNView(frame: frame) + self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0))) self.sceneView.backgroundColor = .clear self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) self.sceneView.isUserInteractionEnabled = false @@ -194,7 +198,19 @@ class PremiumStarComponent: Component { case .changed: let translation = gesture.translation(in: gesture.view) let yawPan = deg2rad(Float(translation.x)) - let pitchPan = deg2rad(Float(translation.y)) + + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 60.0 + let coefficient: CGFloat = 0.4 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0) + if translation.y < 0.0 { + pitchTranslation *= -1.0 + } + let pitchPan = deg2rad(Float(pitchTranslation)) self.previousYaw = yawPan node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0) @@ -246,15 +262,68 @@ class PremiumStarComponent: Component { if !self.didSetReady { self.didSetReady = true - self._ready.set(.single(true)) - self.onReady() + Queue.mainQueue().justDispatch { + self._ready.set(.single(true)) + self.onReady() + } } } + private func maybeAnimateIn() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false), let animateFrom = self.animateFrom, let containerView = self.containerView else { + return + } + + if let animationColor = self.animationColor { + let newNode = node.clone() + newNode.geometry = node.geometry?.copy() as? SCNGeometry + + let colorMaterial = SCNMaterial() + colorMaterial.diffuse.contents = animationColor + colorMaterial.lightingModel = SCNMaterial.LightingModel.blinn + newNode.geometry?.materials = [colorMaterial] + node.addChildNode(newNode) + + newNode.scale = SCNVector3(1.03, 1.03, 1.03) + newNode.geometry?.materials.first?.diffuse.contents = animationColor + + let animation = CABasicAnimation(keyPath: "opacity") + animation.beginTime = CACurrentMediaTime() + 0.1 + animation.duration = 0.7 + animation.fromValue = 1.0 + animation.toValue = 0.0 + animation.fillMode = .forwards + animation.isRemovedOnCompletion = false + animation.completion = { [weak newNode] _ in + newNode?.removeFromParentNode() + } + newNode.addAnimation(animation, forKey: "opacity") + } + + let initialPosition = self.sceneView.center + let targetPosition = self.sceneView.superview!.convert(self.sceneView.center, to: containerView) + let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: -20.0) + + containerView.addSubview(self.sceneView) + self.sceneView.center = targetPosition + + animateFrom.alpha = 0.0 + self.sceneView.layer.animateScale(from: 0.05, to: 0.5, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring) + self.sceneView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + self.addSubview(self.sceneView) + self.sceneView.center = initialPosition + animateFrom.alpha = 1.0 + }) + + self.animateFrom = nil + self.containerView = nil + } + private func onReady() { self.setupGradientAnimation() self.setupShineAnimation() + self.maybeAnimateIn() self.playAppearanceAnimation(explode: true) self.previousInteractionTimestamp = CACurrentMediaTime() @@ -367,7 +436,9 @@ class PremiumStarComponent: Component { func update(component: PremiumStarComponent, availableSize: CGSize, transition: Transition) -> CGSize { 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) + if self.sceneView.superview == self { + self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0) + } self.hasIdleAnimations = component.hasIdleAnimations diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index 788a7d2f4f..ec1113f0ff 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import SwiftSignalKit import AsyncDisplayKit import ComponentFlow import TelegramCore @@ -66,7 +67,13 @@ final class ReactionsCarouselComponent: Component { } if isDisplaying && !self.isVisible { - self.node?.animateIn() + var fast = false + if let _ = transition.userData(DemoAnimateInTransition.self) { + fast = true + } + self.node?.setVisible(true, fast: fast) + } else if !isDisplaying && self.isVisible { + self.node?.setVisible(false) } self.isVisible = isDisplaying @@ -85,6 +92,9 @@ final class ReactionsCarouselComponent: Component { private let itemSize = CGSize(width: 110.0, height: 110.0) +//private let order = ["👌","😍","🤡","🕊","🥱","🥴"] +private let order = ["😍","👌","🥴","🐳","🥱","🕊","🤡"] + private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let theme: PresentationTheme @@ -105,10 +115,26 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let positionDelta: Double + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) { self.context = context self.theme = theme - self.reactions = Array(reactions.shuffled().prefix(6)) + + var reactionMap: [String: AvailableReactions.Reaction] = [:] + for reaction in reactions { + reactionMap[reaction.value] = reaction + } + + var sortedReactions: [AvailableReactions.Reaction] = [] + for emoji in order { + if let reaction = reactionMap[emoji] { + sortedReactions.append(reaction) + } + } + + self.reactions = sortedReactions self.scrollNode = ASScrollNode() self.tapNode = ASDisplayNode() @@ -123,6 +149,10 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.setup() } + deinit { + self.timer?.invalidate() + } + override func didLoad() { super.didLoad() @@ -134,7 +164,14 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } @objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) { - guard self.animator == nil, self.scrollStartPosition == nil else { + self.previousInteractionTimestamp = CACurrentMediaTime() + + if let animator = self.animator { + animator.invalidate() + self.animator = nil + } + + guard self.scrollStartPosition == nil else { return } @@ -143,11 +180,51 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { return } - self.scrollTo(index, playReaction: true, duration: 0.4) + self.scrollTo(index, playReaction: true, immediately: true, duration: 0.85) + self.hapticFeedback.impact(.light) } - func animateIn() { - self.scrollTo(1, playReaction: true, duration: 0.5, clockwise: true) + func setVisible(_ visible: Bool, fast: Bool = false) { + if visible { + self.animateIn(fast: fast) + } else { + self.animator?.invalidate() + self.animator = nil + + self.scrollTo(0, playReaction: false, immediately: false, duration: 0.0, clockwise: false) + self.timer?.invalidate() + self.timer = nil + + self.playingIndices.removeAll() + self.standaloneReactionAnimation?.removeFromSupernode() + } + } + + func animateIn(fast: Bool) { + let duration: Double = fast ? 1.4 : 2.2 + let delay: Double = fast ? 0.5 : 0.8 + self.scrollTo(1, playReaction: false, immediately: false, duration: duration, damping: 0.75, clockwise: true) + Queue.mainQueue().after(delay, { + self.playReaction(index: 1) + }) + + if self.timer == nil { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + if let strongSelf = self { + let currentTimestamp = CACurrentMediaTime() + if currentTimestamp > strongSelf.previousInteractionTimestamp + 4.0 { + var nextIndex = strongSelf.currentIndex - 1 + if nextIndex < 0 { + nextIndex = strongSelf.reactions.count + nextIndex + } + strongSelf.scrollTo(nextIndex, playReaction: true, immediately: true, duration: 0.85, clockwise: true) + strongSelf.previousInteractionTimestamp = currentTimestamp + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } } func animateOut() { @@ -156,7 +233,34 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } } - func scrollTo(_ index: Int, playReaction: Bool, duration: Double, clockwise: Bool? = nil) { + func springCurveFunc(_ t: Double, zeta: Double) -> Double { + let v0 = 0.0 + let omega = 20.285 + + let y: Double + if abs(zeta - 1.0) < 1e-8 { + let c1 = -1.0 + let c2 = v0 - omega + y = (c1 + c2 * t) * exp(-omega * t) + } else if zeta > 1 { + let s1 = omega * (-zeta + sqrt(zeta * zeta - 1)) + let s2 = omega * (-zeta - sqrt(zeta * zeta - 1)) + let c1 = (-s2 - v0) / (s2 - s1) + let c2 = (s1 + v0) / (s2 - s1) + y = c1 * exp(s1 * t) + c2 * exp(s2 * t) + } else { + let a = -omega * zeta + let b = omega * sqrt(1 - zeta * zeta) + let c2 = (v0 + a) / b + let theta = atan(c2) + // Alternatively y = (-cos(b * t) + c2 * sin(b * t)) * exp(a * t) + y = sqrt(1 + c2 * c2) * exp(a * t) * cos(b * t + theta + Double.pi) + } + + return y + 1 + } + + func scrollTo(_ index: Int, playReaction: Bool, immediately: Bool, duration: Double, damping: Double = 0.6, clockwise: Bool? = nil) { guard index >= 0 && index < self.itemNodes.count else { return } @@ -184,25 +288,41 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } } - 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 + if immediately { + self.playReaction(index: index) + } + + if duration.isZero { + self.currentPosition = newPosition + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) } - 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 playReaction { - self?.playReaction() - } - }) + } else { + self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in + var t = t + if duration <= 0.2 { + t = listViewAnimationCurveSystem(t) + } else { + t = self?.springCurveFunc(t, zeta: damping) ?? 0.0 + } + 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 playReaction && !immediately { + self?.playReaction(index: nil) + } + }) + } } func setup() { @@ -240,14 +360,19 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.ignoreContentOffsetChange = false } - func playReaction() { - let delta = self.positionDelta - let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) + func playReaction(index: Int?) { + let index = index ?? max(0, Int(round(self.currentPosition / self.positionDelta)) % self.itemNodes.count) guard !self.playingIndices.contains(index) else { return } + if let current = self.standaloneReactionAnimation, let dismiss = current.currentDismissAnimation { + dismiss() + current.currentDismissAnimation = nil + self.playingIndices.removeAll() + } + let reaction = self.reactions[index] let targetContainerNode = self.itemContainerNodes[index] let targetView = self.itemNodes[index].view @@ -284,10 +409,13 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { forceSmallEffectAnimation: true, targetView: targetView, addStandaloneReactionAnimation: nil, + currentItemNode: self.itemNodes[index], completion: { [weak standaloneReactionAnimation, weak self] in standaloneReactionAnimation?.removeFromSupernode() - self?.standaloneReactionAnimation = nil - self?.playingIndices.remove(index) + if self?.standaloneReactionAnimation === standaloneReactionAnimation { + self?.standaloneReactionAnimation = nil + self?.playingIndices.remove(index) + } } ) } @@ -301,6 +429,10 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let hapticFeedback = HapticFeedback() func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.isTracking { + self.previousInteractionTimestamp = CACurrentMediaTime() + } + guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { return } @@ -347,17 +479,21 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() let delta = self.positionDelta let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) - self.scrollTo(index, playReaction: true, duration: 0.2) + self.scrollTo(index, playReaction: true, immediately: true, duration: 0.2) } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() - self.playReaction() + self.playReaction(index: nil) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { @@ -372,7 +508,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { let delta = self.positionDelta - let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.45) + let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.44) for i in 0 ..< self.itemNodes.count { let itemNode = self.itemNodes[i] @@ -405,7 +541,7 @@ private 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 = CGPoint(x: itemFrame.midX, y: itemFrame.midY) - transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.55) + transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.65) 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/StickersCarouselComponent.swift b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift index 8285c81ad6..7092e5dd5c 100644 --- a/submodules/PremiumUI/Sources/StickersCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/StickersCarouselComponent.swift @@ -218,6 +218,9 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let positionDelta: Double + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + init(context: AccountContext, stickers: [TelegramMediaFile]) { self.context = context self.stickers = Array(stickers.shuffled().prefix(14)) @@ -249,6 +252,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { } @objc private func stickerTapped(_ gestureRecognizer: UITapGestureRecognizer) { + self.previousInteractionTimestamp = CACurrentMediaTime() + guard self.animator == nil, self.scrollStartPosition == nil else { return } @@ -263,6 +268,24 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { func animateIn() { self.scrollTo(1, playAnimation: true, duration: 0.5, clockwise: true) + + if self.timer == nil { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + if let strongSelf = self { + let currentTimestamp = CACurrentMediaTime() + if currentTimestamp > strongSelf.previousInteractionTimestamp + 4.0 { + var nextIndex = strongSelf.currentIndex - 1 + if nextIndex < 0 { + nextIndex = strongSelf.stickers.count + nextIndex + } + strongSelf.scrollTo(nextIndex, playAnimation: true, duration: 0.85, clockwise: true) + strongSelf.previousInteractionTimestamp = currentTimestamp + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } } func scrollTo(_ index: Int, playAnimation: Bool, duration: Double, clockwise: Bool? = nil) { @@ -360,6 +383,9 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { let containerNode = self.itemContainerNodes[i] let isCentral = i == index itemNode.setCentral(isCentral) + if !isCentral { + itemNode.setVisible(false) + } if isCentral { containerNode.view.superview?.bringSubviewToFront(containerNode.view) @@ -372,10 +398,18 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { if self.scrollStartPosition == nil { self.scrollStartPosition = (scrollView.contentOffset.y, self.currentPosition) } + + for itemNode in self.itemNodes { + itemNode.setCentral(false) + } } private let hapticFeedback = HapticFeedback() func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.isTracking { + self.previousInteractionTimestamp = CACurrentMediaTime() + } + guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { return } @@ -422,6 +456,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() let delta = self.positionDelta @@ -431,6 +467,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() self.playSelectedSticker() } @@ -482,8 +520,8 @@ private class StickersCarouselNode: ASDisplayNode, UIScrollViewDelegate { 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) + transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.75) + transition.updateAlpha(node: containerNode, alpha: 1.0 - distance * 0.6) let isVisible = self.visibility && itemFrame.intersects(bounds) itemNode.setVisible(isVisible) diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 941e967abf..0a6a09a083 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -936,7 +936,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.animateReactionSelection(context: context, theme: theme, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) } - func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { + public var currentDismissAnimation: (() -> Void)? + + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -955,12 +957,14 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode = ReactionNode(context: context, theme: theme, item: reaction) } self.itemNode = itemNode - - if let targetView = targetView as? ReactionIconView, !isLarge { - self.itemNodeIsEmbedded = true - targetView.addSubnode(itemNode) - } else { - self.addSubnode(itemNode) + + if !forceSmallEffectAnimation { + if let targetView = targetView as? ReactionIconView, !isLarge { + self.itemNodeIsEmbedded = true + targetView.addSubnode(itemNode) + } else { + self.addSubnode(itemNode) + } } itemNode.expandedAnimationDidBegin = { [weak self, weak targetView] in @@ -975,7 +979,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetView.isHidden = true } } - + itemNode.isExtracted = true let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) @@ -1077,7 +1081,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { completion() } } - + var didBeginDismissAnimation = false let beginDismissAnimation: () -> Void = { [weak self] in if !didBeginDismissAnimation { @@ -1089,62 +1093,91 @@ public final class StandaloneReactionAnimation: ASDisplayNode { return } - if isLarge { - strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { - if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { - let standaloneReactionAnimation = StandaloneReactionAnimation() + if forceSmallEffectAnimation { + additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in + additionalAnimationNode?.removeFromSupernode() + }) + + mainAnimationCompleted = true + intermediateCompletion() + } else { + if isLarge { + strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { + if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + addStandaloneReactionAnimation(standaloneReactionAnimation) + + standaloneReactionAnimation.animateReactionSelection( + context: itemNode.context, + theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, + reaction: itemNode.item, + avatarPeers: avatarPeers, + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: nil, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } - addStandaloneReactionAnimation(standaloneReactionAnimation) - - standaloneReactionAnimation.animateReactionSelection( - context: itemNode.context, - theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, - reaction: itemNode.item, - avatarPeers: avatarPeers, - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: nil, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) + mainAnimationCompleted = true + intermediateCompletion() + }) + } else { + if let targetView = strongSelf.targetView { + if let targetView = targetView as? ReactionIconView, !isLarge { + targetView.imageView.isHidden = false + } else { + targetView.alpha = 1.0 + targetView.isHidden = false + } + } + + if strongSelf.itemNodeIsEmbedded { + strongSelf.itemNode?.removeFromSupernode() } mainAnimationCompleted = true intermediateCompletion() - }) - } else { - if let targetView = strongSelf.targetView { - if let targetView = targetView as? ReactionIconView, !isLarge { - targetView.imageView.isHidden = false - } else { - targetView.alpha = 1.0 - targetView.isHidden = false - } } - - if strongSelf.itemNodeIsEmbedded { - strongSelf.itemNode?.removeFromSupernode() - } - - mainAnimationCompleted = true - intermediateCompletion() } } } + self.currentDismissAnimation = beginDismissAnimation + let maybeBeginDismissAnimation: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + beginDismissAnimation() + } + } + + if forceSmallEffectAnimation { + itemNode.mainAnimationCompletion = { + mainAnimationCompleted = true + maybeBeginDismissAnimation() + } + } + additionalAnimationNode.completed = { _ in additionalAnimationCompleted = true intermediateCompletion() - beginDismissAnimation() + if forceSmallEffectAnimation { + maybeBeginDismissAnimation() + } else { + beginDismissAnimation() + } } additionalAnimationNode.visibility = true - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - beginDismissAnimation() - }) + if !forceSmallEffectAnimation { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { + beginDismissAnimation() + }) + } } private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index e57be1642b..a9976646ee 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -45,6 +45,7 @@ protocol ReactionItemNode: ASDisplayNode { public final class ReactionNode: ASDisplayNode, ReactionItemNode { let context: AccountContext let item: ReactionItem + private let hasAppearAnimation: Bool private var animateInAnimationNode: AnimatedStickerNode? private let staticAnimationNode: AnimatedStickerNode @@ -67,6 +68,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, hasAppearAnimation: Bool = true) { self.context = context self.item = item + self.hasAppearAnimation = hasAppearAnimation self.staticAnimationNode = AnimatedStickerNode() @@ -113,6 +115,8 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } } + public var mainAnimationCompletion: (() -> Void)? + public func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { let intrinsicSize = size @@ -130,7 +134,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let expandedAnimationFrame = animationFrame - if isExpanded, self.animationNode == nil { + if isExpanded && !self.hasAppearAnimation { + self.staticAnimationNode.play(fromIndex: 0) + } else if isExpanded, self.animationNode == nil { let animationNode = AnimatedStickerNode() animationNode.automaticallyLoadFirstFrame = true self.animationNode = animationNode @@ -143,6 +149,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self?.expandedAnimationDidBegin?() } } + animationNode.completed = { [weak self] _ in + self?.mainAnimationCompletion?() + } if largeExpanded { animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) @@ -274,7 +283,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { if self.animationNode == nil { self.didSetupStillAnimation = true - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + if !self.hasAppearAnimation { + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) + } else { + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + } self.staticAnimationNode.position = animationFrame.center self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.staticAnimationNode.updateLayout(size: animationFrame.size) diff --git a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift index 0ade6498f7..e2e0befb1e 100644 --- a/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift +++ b/submodules/SolidRoundedButtonNode/Sources/SolidRoundedButtonNode.swift @@ -523,6 +523,14 @@ public final class SolidRoundedButtonView: UIView { } } + public var gloss: Bool { + didSet { + if self.gloss != oldValue { + self.setupGloss() + } + } + } + public var progressType: SolidRoundedButtonProgressType = .fullSize public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) { @@ -532,6 +540,7 @@ public final class SolidRoundedButtonView: UIView { self.buttonHeight = height self.buttonCornerRadius = cornerRadius self.title = title + self.gloss = gloss self.buttonBackgroundNode = UIImageView() self.buttonBackgroundNode.clipsToBounds = true @@ -604,33 +613,55 @@ public final class SolidRoundedButtonView: UIView { } if gloss { - let shimmerView = ShimmerEffectForegroundView() - self.shimmerView = shimmerView - - if #available(iOS 13.0, *) { - shimmerView.layer.cornerCurve = .continuous - shimmerView.layer.cornerRadius = self.buttonCornerRadius + self.setupGloss() + } + } + + private func setupGloss() { + if self.gloss { + if self.shimmerView == nil { + let shimmerView = ShimmerEffectForegroundView() + self.shimmerView = shimmerView + + if #available(iOS 13.0, *) { + shimmerView.layer.cornerCurve = .continuous + shimmerView.layer.cornerRadius = self.buttonCornerRadius + } + + let borderView = UIView() + borderView.isUserInteractionEnabled = false + self.borderView = borderView + + let borderMaskView = UIView() + borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel + borderMaskView.layer.borderColor = UIColor.white.cgColor + borderMaskView.layer.cornerRadius = self.buttonCornerRadius + borderView.mask = borderMaskView + self.borderMaskView = borderMaskView + + let borderShimmerView = ShimmerEffectForegroundView() + self.borderShimmerView = borderShimmerView + borderView.addSubview(borderShimmerView) + + self.insertSubview(shimmerView, belowSubview: self.buttonNode) + self.insertSubview(borderView, belowSubview: self.buttonNode) + + self.updateShimmerParameters() + + if let width = self.validLayout { + _ = self.updateLayout(width: width, transition: .immediate) + } } + } else if self.shimmerView != nil { + self.shimmerView?.removeFromSuperview() + self.borderView?.removeFromSuperview() + self.borderMaskView?.removeFromSuperview() + self.borderShimmerView?.removeFromSuperview() - let borderView = UIView() - borderView.isUserInteractionEnabled = false - self.borderView = borderView - - let borderMaskView = UIView() - borderMaskView.layer.borderWidth = 1.0 + UIScreenPixel - borderMaskView.layer.borderColor = UIColor.white.cgColor - borderMaskView.layer.cornerRadius = self.buttonCornerRadius - borderView.mask = borderMaskView - self.borderMaskView = borderMaskView - - let borderShimmerView = ShimmerEffectForegroundView() - self.borderShimmerView = borderShimmerView - borderView.addSubview(borderShimmerView) - - self.insertSubview(shimmerView, belowSubview: self.buttonNode) - self.insertSubview(borderView, belowSubview: self.buttonNode) - - self.updateShimmerParameters() + self.shimmerView = nil + self.borderView = nil + self.borderMaskView = nil + self.borderShimmerView = nil } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index ebeb836cb3..0d85d179d3 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -67,7 +67,9 @@ final class StickerPackPreviewGridItemNode: GridItemNode { private var animationNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode - private var lockBackground: UIImageView? + private var lockBackground: UIVisualEffectView? + private var lockTintView: UIView? + private var lockIconNode: ASImageNode? private var theme: PresentationTheme? @@ -167,21 +169,43 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.isLocked = isLocked if isLocked { - let lockBackground: UIImageView - if let currentBackground = self.lockBackground { + let lockBackground: UIVisualEffectView + let lockIconNode: ASImageNode + if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode { lockBackground = currentBackground + lockIconNode = currentIcon } else { - lockBackground = UIImageView() + let effect: UIBlurEffect + if #available(iOS 10.0, *) { + effect = UIBlurEffect(style: .regular) + } else { + effect = UIBlurEffect(style: .light) + } + lockBackground = UIVisualEffectView(effect: effect) lockBackground.clipsToBounds = true lockBackground.isUserInteractionEnabled = false - lockBackground.image = PresentationResourcesChat.chatInputMediaStickerGridPremiumIcon(theme) + lockIconNode = ASImageNode() + lockIconNode.displaysAsynchronously = false + lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/Lock"), color: .white) + + let lockTintView = UIView() + lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) + lockBackground.contentView.addSubview(lockTintView) + self.lockBackground = lockBackground + self.lockTintView = lockTintView + self.lockIconNode = lockIconNode self.view.addSubview(lockBackground) + self.addSubnode(lockIconNode) } - } else if let lockBackground = self.lockBackground { + } else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { self.lockBackground = nil + self.lockTintView = nil + self.lockIconNode = nil lockBackground.removeFromSuperview() + lockTintView.removeFromSuperview() + lockIconNode.removeFromSupernode() } if let stickerItem = stickerItem { @@ -290,10 +314,18 @@ final class StickerPackPreviewGridItemNode: GridItemNode { self.placeholderNode.update(backgroundColor: theme.list.itemBlocksBackgroundColor, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), data: item.file.immediateThumbnailData, size: placeholderFrame.size) } - if let lockBackground = self.lockBackground { - let lockSize = CGSize(width: 32.0, height: 32.0) + if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { + let lockSize = CGSize(width: 30.0, height: 30.0) let lockBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - lockSize.width) / 2.0), y: bounds.height - lockSize.height - 6.0), size: lockSize) lockBackground.frame = lockBackgroundFrame + lockBackground.layer.cornerRadius = lockSize.width / 2.0 + if #available(iOS 13.0, *) { + lockBackground.layer.cornerCurve = .circular + } + lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size) + if let icon = lockIconNode.image { + lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - icon.size.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - icon.size.height) / 2.0)), size: icon.size) + } } } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 4532df2191..0a061cc7ba 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -181,7 +181,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC } if let dimensitons = self.item.file.dimensions { - let textSpacing: CGFloat = 10.0 + let textSpacing: CGFloat = 50.0 let textSize = self.textNode.measure(CGSize(width: 100.0, height: 100.0)) let imageSize = dimensitons.cgSize.aspectFitted(boundingSize) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 231647f4df..ccb31262bc 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1169,6 +1169,7 @@ public class Account { self.managedOperationsDisposable.add(managedAnimatedEmojiAnimationsUpdates(postbox: self.postbox, network: self.network).start()) } self.managedOperationsDisposable.add(managedGreetingStickers(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedPremiumStickers(postbox: self.postbox, network: self.network).start()) if !supplementary { let mediaBox = postbox.mediaBox diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 8ac1e27de4..db128a12b5 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -154,3 +154,27 @@ func managedGreetingStickers(postbox: Postbox, network: Network) -> Signal then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } + +func managedPremiumStickers(postbox: Postbox, network: Network) -> Signal { + let poll = managedRecentMedia(postbox: postbox, network: network, collectionId: Namespaces.OrderedItemList.CloudPremiumStickers, reverseHashOrder: false, forceFetch: false, fetch: { hash in + return network.request(Api.functions.messages.getStickers(emoticon: "⭐️⭐️", hash: 0)) + |> retryRequest + |> mapToSignal { result -> Signal<[OrderedItemListEntry]?, NoError> in + switch result { + case .stickersNotModified: + return .single(nil) + case let .stickers(_, stickers): + var items: [OrderedItemListEntry] = [] + for sticker in stickers { + if let file = telegramMediaFileFromApiDocument(sticker), let id = file.id { + if let entry = CodableEntry(RecentMediaItem(file)) { + items.append(OrderedItemListEntry(id: RecentMediaItemId(id).rawValue, contents: entry)) + } + } + } + return .single(items) + } + } + }) + return (poll |> then(.complete() |> suspendAwareDelay(3.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index a4e459fc4c..eecd7f3430 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -62,6 +62,7 @@ public struct Namespaces { public static let CloudGreetingStickers: Int32 = 10 public static let RecentDownloads: Int32 = 11 public static let PremiumStickers: Int32 = 12 + public static let CloudPremiumStickers: Int32 = 13 } public struct CachedItemCollection { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 49cfc98ef9..0a11af13f9 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -38,6 +38,7 @@ public enum PresentationResourceKey: Int32 { case itemListDownArrow case itemListDisclosureArrow + case itemListDisclosureLocked case itemListCheckIcon case itemListSecondaryCheckIcon case itemListPlusIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift index edbd9ee2aa..78bdbc02cf 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesItemList.swift @@ -33,6 +33,12 @@ public struct PresentationResourcesItemList { }) } + public static func disclosureLockedImage(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.itemListDisclosureLocked.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.list.disclosureArrowColor) + }) + } + public static func checkIconImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.itemListCheckIcon.rawValue, { theme in return generateItemListCheckIcon(color: theme.list.itemAccentColor) diff --git a/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json deleted file mode 100644 index 718c1456b3..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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 deleted file mode 100644 index 57fbe59d71..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Components/Dots.imageset/dots@3x.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/Contents.json new file mode 100644 index 0000000000..7a89bd061f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "phone.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/phone.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/phone.pdf new file mode 100644 index 0000000000..1b32cad7a3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Phone.imageset/phone.pdf @@ -0,0 +1,176 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E1 << /ca 0.500000 >> >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 0.000000 -0.003418 cm +0.000000 0.000000 0.000000 scn +1001.992859 2652.003418 m +1040.278442 2652.004395 1072.592285 2652.005127 1099.064819 2649.842285 c +1126.787598 2647.577148 1153.344360 2642.644287 1178.614136 2629.768555 c +1216.999146 2610.210449 1248.207153 2579.002441 1267.765259 2540.617432 c +1280.640869 2515.347656 1285.573730 2488.791016 1287.838745 2461.068115 c +1290.001709 2434.595947 1290.000854 2402.282471 1290.000000 2363.997559 c +1290.000000 288.009521 l +1290.000854 249.724609 1290.001709 217.410889 1287.838745 190.938965 c +1285.573730 163.216064 1280.640869 136.659180 1267.765259 111.389648 c +1248.207153 73.004395 1216.999146 41.796631 1178.614136 22.238281 c +1153.344360 9.362793 1126.787598 4.429932 1099.064819 2.164795 c +1072.592529 0.001709 1040.279175 0.002686 1001.994263 0.003662 c +288.005798 0.003662 l +249.720932 0.002686 217.407471 0.001709 190.935211 2.164795 c +163.212448 4.429932 136.655716 9.362793 111.386002 22.238281 c +73.000938 41.796631 41.792908 73.004395 22.234743 111.389648 c +9.359177 136.659180 4.426258 163.216064 2.161221 190.938965 c +-0.001672 217.411621 -0.000894 249.725342 0.000039 288.010742 c +0.000039 2363.996338 l +-0.000894 2402.281738 -0.001672 2434.595703 2.161221 2461.068115 c +4.426258 2488.791016 9.359177 2515.347656 22.234743 2540.617432 c +41.792908 2579.002441 73.000938 2610.210449 111.386002 2629.768555 c +136.655716 2642.644287 163.212463 2647.577148 190.935226 2649.842285 c +217.407806 2652.005127 249.721725 2652.004395 288.007172 2652.003418 c +1001.992859 2652.003418 l +h +75.695122 2513.377930 m +60.000065 2482.574707 60.000069 2442.250977 60.000069 2361.603516 c +60.000069 290.403564 l +60.000069 209.756104 60.000065 169.432129 75.695122 138.628906 c +89.500893 111.533691 111.530090 89.504395 138.625427 75.698730 c +169.428711 60.003662 209.752472 60.003662 290.399994 60.003662 c +999.600037 60.003662 l +1080.247559 60.003662 1120.571289 60.003662 1151.374634 75.698730 c +1178.469971 89.504395 1200.499146 111.533691 1214.304932 138.628906 c +1230.000000 169.432129 1230.000000 209.756104 1230.000000 290.403564 c +1230.000000 2361.603271 l +1230.000000 2442.250977 1230.000000 2482.574707 1214.304932 2513.377930 c +1200.499146 2540.473389 1178.469971 2562.502441 1151.374634 2576.308350 c +1120.571289 2592.003418 1080.247559 2592.003418 999.600037 2592.003418 c +290.400055 2592.003418 l +209.752487 2592.003418 169.428711 2592.003418 138.625427 2576.308350 c +111.530090 2562.502441 89.500893 2540.473389 75.695122 2513.377930 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 18.000000 17.998047 cm +0.000000 0.000000 0.000000 scn +1079.599121 2613.900635 m +1053.868774 2616.002686 1022.190063 2616.002441 983.365845 2616.001953 c +270.634308 2616.001953 l +231.810059 2616.002441 200.131302 2616.002686 174.400970 2613.900635 c +147.791351 2611.726562 123.867096 2607.096191 101.557800 2595.729248 c +66.559647 2577.896729 38.105267 2549.442383 20.272820 2514.444092 c +8.905663 2492.134766 4.275493 2468.210693 2.101401 2441.601074 c +-0.000850 2415.870605 -0.000461 2384.191895 0.000014 2345.367676 c +0.000014 270.636230 l +-0.000461 231.812012 -0.000850 200.133301 2.101401 174.402832 c +4.275493 147.793457 8.905663 123.868896 20.272820 101.559814 c +38.105267 66.561523 66.559647 38.107178 101.557800 20.274658 c +123.867096 8.907715 147.791351 4.277344 174.400955 2.103271 c +200.132080 0.000977 231.812027 0.001465 270.637878 0.001953 c +983.362183 0.001953 l +1022.187927 0.001465 1053.867920 0.000977 1079.599121 2.103271 c +1106.208740 4.277344 1130.132935 8.907715 1152.442139 20.274658 c +1187.440308 38.107178 1215.894775 66.561523 1233.727173 101.559814 c +1245.094360 123.868896 1249.724609 147.793457 1251.898560 174.402832 c +1254.000854 200.131836 1254.000488 231.808838 1254.000000 270.630859 c +1254.000000 2345.373291 l +1254.000488 2384.194824 1254.000854 2415.871826 1251.898560 2441.601074 c +1249.724609 2468.210693 1245.094360 2492.134766 1233.727173 2514.444092 c +1215.894775 2549.442383 1187.440308 2577.896729 1152.442139 2595.729248 c +1130.132935 2607.096191 1106.208740 2611.726562 1079.599121 2613.900635 c +h +42.000034 2343.602051 m +42.000034 2424.249512 42.000031 2464.573242 57.695091 2495.376465 c +71.500854 2522.471924 93.530060 2544.500977 120.625397 2558.306885 c +151.428680 2574.001953 191.752472 2574.001953 272.400024 2574.001953 c +359.579956 2574.001953 l +360.458221 2573.991699 l +366.948761 2573.875977 371.125000 2572.780518 374.811859 2570.816895 c +378.715668 2568.737793 381.783417 2565.685303 383.882019 2561.791992 c +385.609863 2558.586670 386.679810 2555.006104 387.023712 2549.834473 c +387.280701 2523.251221 390.322266 2513.048340 395.821350 2502.766113 c +401.573456 2492.010498 410.014465 2483.569336 420.770050 2477.817383 c +431.525635 2472.065186 442.194092 2469.001953 471.614197 2469.001953 c +782.397766 2469.001953 l +811.817871 2469.001953 822.486328 2472.065186 833.241821 2477.817383 c +843.997375 2483.569336 852.438477 2492.010498 858.190613 2502.766113 c +863.689697 2513.048340 866.731323 2523.251221 866.988037 2549.840820 c +867.332153 2555.006104 868.402039 2558.586670 870.129883 2561.791992 c +872.228577 2565.685303 875.296265 2568.737793 879.200073 2570.816895 c +882.886963 2572.780518 887.063110 2573.875977 893.553711 2573.991699 c +894.431946 2574.001953 l +981.600037 2574.001953 l +1062.247559 2574.001953 1102.571289 2574.001953 1133.374634 2558.306885 c +1160.469971 2544.500977 1182.499146 2522.471924 1196.304932 2495.376465 c +1212.000000 2464.573242 1212.000000 2424.249512 1212.000000 2343.602051 c +1212.000000 272.401855 l +1212.000000 191.754395 1212.000000 151.430664 1196.304932 120.627197 c +1182.499146 93.531982 1160.469971 71.502930 1133.374634 57.697021 c +1102.571289 42.001953 1062.247559 42.001953 981.600037 42.001953 c +272.399963 42.001953 l +191.752441 42.001953 151.428680 42.001953 120.625397 57.697021 c +93.530060 71.502930 71.500854 93.531982 57.695091 120.627197 c +42.000031 151.430664 42.000034 191.754395 42.000034 272.401855 c +42.000034 2343.602051 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 6076 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 1290.000000 2652.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000074 00000 n +0000006206 00000 n +0000006229 00000 n +0000006406 00000 n +0000006480 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +6539 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 8ed3cbc9e6..4387dd42b3 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1043,11 +1043,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !reaction.isEnabled { continue } - if reaction.isPremium && !hasPremium { - hasPremiumPlaceholder = true - continue - } - + switch allowedReactions { case let .set(set): if !set.contains(reaction.value) { @@ -1056,6 +1052,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .all: break } + + if reaction.isPremium && !hasPremium { + hasPremiumPlaceholder = true + continue + } + actions.reactionItems.append(.reaction(ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation, @@ -4707,6 +4709,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G themeEmoticon = nil } } + if strongSelf.chatLocation.peerId == strongSelf.context.account.peerId { + themeEmoticon = nil + } var presentationData = presentationData var useDarkAppearance = presentationData.theme.overallDarkAppearance @@ -11548,12 +11553,14 @@ 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: 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)) + let controller = PremiumLimitScreen(context: strongSelf.context, subject: .files, count: 4, action: { + }) + strongSelf.push(controller) return } else if item.fileSize > Int64(limits.maxUploadFileParts) * 512 * 1024 && !isPremium { let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .files, count: 0, action: { + let controller = PremiumLimitScreen(context: context, subject: .files, count: 2, action: { replaceImpl?(PremiumIntroScreen(context: context, source: .upload)) }) replaceImpl = { [weak controller] c in diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index cbede1a041..98984a227f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -178,7 +178,9 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { private(set) var animationNode: AnimatedStickerNode? private(set) var placeholderNode: StickerShimmerEffectNode? - private var lockBackground: UIImageView? + private var lockBackground: UIVisualEffectView? + private var lockTintView: UIView? + private var lockIconNode: ASImageNode? var isLocked: Bool? private var didSetUpAnimationNode = false @@ -313,21 +315,43 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { self.isLocked = item.isLocked if item.isLocked { - let lockBackground: UIImageView - if let currentBackground = self.lockBackground { + let lockBackground: UIVisualEffectView + let lockIconNode: ASImageNode + if let currentBackground = self.lockBackground, let currentIcon = self.lockIconNode { lockBackground = currentBackground + lockIconNode = currentIcon } else { - lockBackground = UIImageView() + let effect: UIBlurEffect + if #available(iOS 10.0, *) { + effect = UIBlurEffect(style: .regular) + } else { + effect = UIBlurEffect(style: .light) + } + lockBackground = UIVisualEffectView(effect: effect) lockBackground.clipsToBounds = true lockBackground.isUserInteractionEnabled = false - lockBackground.image = PresentationResourcesChat.chatInputMediaStickerGridPremiumIcon(item.theme) + lockIconNode = ASImageNode() + lockIconNode.displaysAsynchronously = false + lockIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white) + + let lockTintView = UIView() + lockTintView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.15) + lockBackground.contentView.addSubview(lockTintView) + self.lockBackground = lockBackground + self.lockTintView = lockTintView + self.lockIconNode = lockIconNode self.view.addSubview(lockBackground) + self.addSubnode(lockIconNode) } - } else if let lockBackground = self.lockBackground { + } else if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { self.lockBackground = nil + self.lockTintView = nil + self.lockIconNode = nil lockBackground.removeFromSuperview() + lockTintView.removeFromSuperview() + lockIconNode.removeFromSupernode() } } @@ -360,10 +384,18 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size) } - if let lockBackground = self.lockBackground { - let lockSize = CGSize(width: 26.0, height: 26.0) + if let lockBackground = self.lockBackground, let lockTintView = self.lockTintView, let lockIconNode = self.lockIconNode { + let lockSize = CGSize(width: 24.0, height: 24.0) let lockBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - lockSize.width) / 2.0), y: size.height - lockSize.height - 2.0), size: lockSize) lockBackground.frame = lockBackgroundFrame + lockBackground.layer.cornerRadius = lockSize.width / 2.0 + if #available(iOS 13.0, *) { + lockBackground.layer.cornerCurve = .circular + } + lockTintView.frame = CGRect(origin: CGPoint(), size: lockBackgroundFrame.size) + if let icon = lockIconNode.image { + lockIconNode.frame = CGRect(origin: CGPoint(x: lockBackgroundFrame.minX + floorToScreenPixels((lockBackgroundFrame.width - icon.size.width) / 2.0), y: lockBackgroundFrame.minY + floorToScreenPixels((lockBackgroundFrame.height - icon.size.height) / 2.0)), size: icon.size) + } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index 70e07a36ba..4e29814952 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2016,6 +2016,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { var displayAvatarContextMenu: ((ASDisplayNode, ContextGesture?) -> Void)? var displayCopyContextMenu: ((ASDisplayNode, Bool, Bool) -> Void)? + var displayPremiumIntro: ((UIView, Bool) -> Void)? + var navigationTransition: PeerInfoHeaderNavigationTransition? var backgroundAlpha: CGFloat = 1.0 @@ -2180,6 +2182,12 @@ final class PeerInfoHeaderNode: ASDisplayNode { let phoneGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePhoneLongPress(_:))) self.subtitleNodeRawContainer.view.addGestureRecognizer(phoneGestureRecognizer) + + let premiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) + self.titleCredibilityIconNode.view.addGestureRecognizer(premiumGestureRecognizer) + + let expandedPremiumGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleStarTap(_:))) + self.titleExpandedCredibilityIconNode.view.addGestureRecognizer(expandedPremiumGestureRecognizer) } @objc private func handleUsernameLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { @@ -2194,6 +2202,13 @@ final class PeerInfoHeaderNode: ASDisplayNode { } } + @objc private func handleStarTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard let view = gestureRecognizer.view, self.currentCredibilityIcon == .premium else { + return + } + self.displayPremiumIntro?(view, view == self.titleExpandedCredibilityIconNode.view) + } + func initiateAvatarExpansion(gallery: Bool, first: Bool) { if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery { self.requestOpenAvatarForEditing?(false) @@ -3073,6 +3088,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { if !self.backgroundNode.frame.contains(point) { return nil } + if self.currentCredibilityIcon == .premium { + let iconFrame = self.titleCredibilityIconNode.view.convert(self.titleCredibilityIconNode.bounds, to: self.view) + let expandedIconFrame = self.titleExpandedCredibilityIconNode.view.convert(self.titleExpandedCredibilityIconNode.bounds, to: self.view) + if expandedIconFrame.contains(point) && self.isAvatarExpanded { + return self.titleExpandedCredibilityIconNode.view + } else if iconFrame.contains(point) { + return self.titleCredibilityIconNode.view + } + } + if result == self.view || result == self.regularContentNode.view || result == self.editingContentNode.view { return nil } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index b373e12de7..07a5bccf7d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -651,11 +651,10 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p interaction.accountContextMenu(peerAccountContext.account.id, node, gesture) })) } - if settings.accountsAndPeers.count + 1 < maximumNumberOfAccounts { - items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: { - interaction.openSettings(.addAccount) - })) - } + + items[.accounts]!.append(PeerInfoScreenActionItem(id: 100, text: presentationData.strings.Settings_AddAccount, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), action: { + interaction.openSettings(.addAccount) + })) } if !settings.proxySettings.servers.isEmpty { @@ -824,12 +823,10 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat interaction.openSettings(.username) })) - if let settings = data.globalSettings, settings.accountsAndPeers.count + 1 < maximumNumberOfAccounts { - items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { - interaction.openSettings(.addAccount) - })) - items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: presentationData.strings.Settings_AddAnotherAccount_Help)) - } + items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { + interaction.openSettings(.addAccount) + })) + items[.account]!.append(PeerInfoScreenCommentItem(id: ItemAddAccountHelp, text: presentationData.strings.Settings_AddAnotherAccount_Help)) items[.logout]!.append(PeerInfoScreenActionItem(id: ItemLogout, text: presentationData.strings.Settings_Logout, color: .destructive, alignment: .center, action: { interaction.openSettings(.logout) @@ -2938,6 +2935,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: self.hasPassport.get()) + self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in guard let strongSelf = self, let data = strongSelf.data, let user = data.peer as? TelegramUser else { return @@ -2976,7 +2974,19 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } } else { screenData = peerInfoScreenData(context: context, peerId: peerId, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, isSettings: self.isSettings, hintGroupInCommon: hintGroupInCommon, existingRequestsContext: requestsContext) - + + self.headerNode.displayPremiumIntro = { [weak self] sourceView, white in + guard let strongSelf = self else { + return + } + + let controller = PremiumIntroScreen(context: strongSelf.context, source: .profile(strongSelf.peerId)) + controller.sourceView = sourceView + controller.containerView = strongSelf.controller?.navigationController?.view + controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor + strongSelf.controller?.push(controller) + } + self.headerNode.displayAvatarContextMenu = { [weak self] node, gesture in guard let strongSelf = self, let peer = strongSelf.data?.peer else { return @@ -6192,7 +6202,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate case .language: push(LocalizationListController(context: self.context)) case .premium: - self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) + self.controller?.push(PremiumIntroScreen(context: self.context, modal: false, source: .settings)) case .stickers: if let settings = self.data?.globalSettings { push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in @@ -6233,11 +6243,37 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate case .username: push(usernameSetupController(context: self.context)) case .addAccount: - self.context.sharedContext.beginNewAuth(testingEnvironment: self.context.account.testingEnvironment) + var maximumAvailableAccounts: Int = 3 + if self.data?.peer?.isPremium == true { + maximumAvailableAccounts = 4 + } + var count: Int = 1 + if let settings = self.data?.globalSettings { + for (_, peer, _) in settings.accountsAndPeers { + if peer.isPremium { + maximumAvailableAccounts = 4 + } + } + count += settings.accountsAndPeers.count + } + if count >= maximumAvailableAccounts { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .accounts, count: Int32(count), action: { + let controller = PremiumIntroScreen(context: context, source: .accounts) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.controller?.push(controller) + } else { + self.context.sharedContext.beginNewAuth(testingEnvironment: self.context.account.testingEnvironment) + } case .logout: - if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone, let accounts = self.data?.globalSettings?.accountsAndPeers { + if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone { if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController { - self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: accounts.count + 1 < maximumNumberOfAccounts, phoneNumber: phoneNumber)) + self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber)) } } case .rememberPassword: @@ -7946,7 +7982,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { let presentationDataSignal: Signal if let updatedPresentationData = updatedPresentationData { presentationDataSignal = updatedPresentationData.signal - } else { + } else if self.peerId != self.context.account.peerId { let themeEmoticon: Signal = self.cachedDataPromise.get() |> map { cachedData -> String? in if let cachedData = cachedData as? CachedUserData { @@ -7972,6 +8008,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { } return presentationData } + } else { + presentationDataSignal = context.sharedContext.presentationData } self.presentationDataDisposable = (presentationDataSignal @@ -8129,17 +8167,16 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen { let strings = self.presentationData.strings var items: [ContextMenuItem] = [] - if other.count + 1 < maximumNumberOfAccounts { - items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - guard let strongSelf = self else { - return - } - strongSelf.controllerNode.openSettings(section: .addAccount) - f(.dismissWithoutContent) - }))) - } + items.append(.action(ContextMenuActionItem(text: strings.Settings_AddAccount, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + guard let strongSelf = self else { + return + } + strongSelf.controllerNode.openSettings(section: .addAccount) + f(.dismissWithoutContent) + }))) + let avatarSize = CGSize(width: 28.0, height: 28.0)