diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 307b835168..f7b9e72e03 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7582,6 +7582,9 @@ Sorry for the inconvenience."; "Premium.FasterSpeed" = "Faster Download Speed"; "Premium.FasterSpeedInfo" = "No more limits on the speed with which media and documents are downloaded."; +"Premium.VoiceToText" = "Voice-to-Text Conversion"; +"Premium.VoiceToTextInfo" = "Ability to read the transcript of any incoming voice message."; + "Premium.NoAds" = "No Ads"; "Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads."; @@ -7591,16 +7594,24 @@ Sorry for the inconvenience."; "Premium.Stickers" = "Premium Stickers"; "Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly."; +"Premium.ChatManagement" = "Advanced Chat Management"; +"Premium.ChatManagementInfo" = "Tools to set default folder, auto-archive and hide new chats."; + "Premium.Badge" = "Profile Badge"; "Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram."; "Premium.Avatar" = "Animated Profile Pictures"; "Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression."; -"Premium.SubscribeFor" = "Subscribe for %@ per month"; +"Premium.SubscribeFor" = "Subscribe for %@ / month"; "Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM"; "Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; +"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy)."; + "Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted"; "Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted"; + +"Settings.Terms_URL" = "https://telegram.org/tos"; +"Settings.PrivacyPolicy_URL" = "https://telegram.org/privacy"; diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 5864f4c561..04ef073a42 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -380,6 +380,8 @@ public class AttachmentController: ViewController { override func didLoad() { super.didLoad() + self.view.disablesInteractiveModalDismiss = true + self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) if let controller = self.controller { diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 23bc17ed13..3de098e3ac 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -165,13 +165,15 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch let isContact = transaction.isPeerContact(peerId: peerId) if case let .chatList(currentFilter) = source { - if let currentFilter = currentFilter { + if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter { items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters for i in 0 ..< filters.count { if filters[i].id == currentFilter.id { - let _ = filters[i].data.addExcludePeer(peerId: peer.id) + var updatedData = data + let _ = updatedData.addExcludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) break } } @@ -179,7 +181,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } |> deliverOnMainQueue).start(completed: { c.dismiss(completion: { - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: currentFilter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false }), in: .current) }) @@ -188,13 +190,13 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } else { var hasFolders = false - for filter in filters { - let predicate = chatListFilterPredicate(filter: filter.data) + for case let .filter(_, _, _, data) in filters { + let predicate = chatListFilterPredicate(filter: data) if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { continue } - var data = filter.data + var data = data if data.addIncludePeer(peerId: peer.id) { hasFolders = true break @@ -206,56 +208,62 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch var updatedItems: [ContextMenuItem] = [] for filter in filters { - let predicate = chatListFilterPredicate(filter: filter.data) - if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { - continue - } - - var data = filter.data - if !data.addIncludePeer(peerId: peer.id) { - continue - } - - let filterType = chatListFilterType(filter) - updatedItems.append(.action(ContextMenuActionItem(text: filter.title, icon: { theme in - let imageName: String - switch filterType { - case .generic: - imageName = "Chat/Context Menu/List" - case .unmuted: - imageName = "Chat/Context Menu/Unmute" - case .unread: - imageName = "Chat/Context Menu/MarkAsUnread" - case .channels: - imageName = "Chat/Context Menu/Channels" - case .groups: - imageName = "Chat/Context Menu/Groups" - case .bots: - imageName = "Chat/Context Menu/Bots" - case .contacts: - imageName = "Chat/Context Menu/User" - case .nonContacts: - imageName = "Chat/Context Menu/UnknownUser" + if case let .filter(_, title, _, data) = filter { + let predicate = chatListFilterPredicate(filter: data) + if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) { + continue } - return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) - }, action: { c, f in - c.dismiss(completion: { - let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in - var filters = filters - for i in 0 ..< filters.count { - if filters[i].id == filter.id { - let _ = filters[i].data.addIncludePeer(peerId: peer.id) - break - } - } - return filters - }).start() - chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: filter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in - return false - }), in: .current) - }) - }))) + var data = data + if !data.addIncludePeer(peerId: peer.id) { + continue + } + + let filterType = chatListFilterType(data) + updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in + let imageName: String + switch filterType { + case .generic: + imageName = "Chat/Context Menu/List" + case .unmuted: + imageName = "Chat/Context Menu/Unmute" + case .unread: + imageName = "Chat/Context Menu/MarkAsUnread" + case .channels: + imageName = "Chat/Context Menu/Channels" + case .groups: + imageName = "Chat/Context Menu/Groups" + case .bots: + imageName = "Chat/Context Menu/Bots" + case .contacts: + imageName = "Chat/Context Menu/User" + case .nonContacts: + imageName = "Chat/Context Menu/UnknownUser" + } + return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor) + }, action: { c, f in + c.dismiss(completion: { + let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in + var filters = filters + for i in 0 ..< filters.count { + if filters[i].id == filter.id { + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + let _ = updatedData.addIncludePeer(peerId: peer.id) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } + break + } + } + return filters + }).start() + + chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in + return false + }), in: .current) + }) + }))) + } } updatedItems.append(.separator) @@ -318,11 +326,11 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch switch result { case .done: f(.default) - case .limitExceeded: + case let .limitExceeded(count, _): f(.default) var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, action: { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { let premiumScreen = PremiumIntroScreen(context: context) replaceImpl?(premiumScreen) }) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 2fcd7b798f..c2e351b421 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -160,6 +160,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var activeDownloadsDisposable: Disposable? private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer? + private var isPremium: Bool = false + private var didSetupTabs = false public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { @@ -191,8 +193,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style let title: String - if let filter = self.filter { - title = filter.title + if let filter = self.filter, case let .filter(_, filterTitle, _, _) = filter { + title = filterTitle } else if self.groupId == .root { title = self.presentationData.strings.DialogList_Title } else { @@ -762,7 +764,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.tabContainerNode.cancelAnimations() strongSelf.chatListDisplayNode.inlineTabContainerNode.cancelAnimations() } - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: strongSelf.isPremium, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: tabContainerData.0, selectedFilter: filter, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: fraction, presentationData: strongSelf.presentationData, transition: transition) } self.reloadFilters() @@ -833,7 +835,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) if let layout = self.validLayout { - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) self.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .immediate) } @@ -1204,8 +1206,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var archiveEnabled = options.delete var displayArchive = true - if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter { - if !filter.data.excludeArchived { + if let filter = strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { + if !data.excludeArchived { displayArchive = false } } @@ -1265,8 +1267,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - let _ = (strongSelf.context.engine.peers.currentChatListFilters() - |> deliverOnMainQueue).start(next: { [weak self] filters in + let _ = combineLatest( + queue: Queue.mainQueue(), + strongSelf.context.engine.peers.currentChatListFilters(), + 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) + ) + ).start(next: { [weak self] filters, result in guard let strongSelf = self else { return } @@ -1299,7 +1308,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }) }))) - if let filter = filters.first(where: { $0.id == id }), filter.data.includePeers.peers.count < 100 { + + let (_, _, premiumLimits) = result + let premiumLimit = premiumLimits.maxFolderChatsCount + + if let filter = filters.first(where: { $0.id == id }), case let .filter(_, _, _, data) = filter, data.includePeers.peers.count < premiumLimit { 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 @@ -1322,20 +1335,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } var found = false for filter in presetList { - if filter.id == id { + if filter.id == id, case let .filter(_, _, _, data) = filter { let (accountPeer, limits, premiumLimits) = result let limit = limits.maxFolderChatsCount let premiumLimit = premiumLimits.maxFolderChatsCount if let accountPeer = accountPeer, accountPeer.isPremium { - if filter.data.includePeers.peers.count >= premiumLimit { + if data.includePeers.peers.count >= premiumLimit { //printPremiumError return } } else { - if filter.data.includePeers.peers.count >= limit { + if data.includePeers.peers.count >= limit { var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, action: { + let controller = PremiumLimitScreen(context: context, subject: .chatsInFolder, count: Int32(data.includePeers.peers.count), action: { let controller = PremiumIntroScreen(context: context) replaceImpl?(controller) }) @@ -1792,7 +1805,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let navigationBarHeight = self.navigationBar?.frame.maxY ?? 0.0 transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0))) - self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.containerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, transitionFraction: self.chatListDisplayNode.containerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) if let tabContainerData = self.tabContainerData { self.chatListDisplayNode.inlineTabContainerNode.isHidden = !tabContainerData.1 || tabContainerData.0.count <= 1 } else { @@ -1858,7 +1871,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let defaultFilterIds = defaultFilters.0.compactMap { entry -> Int32? in switch entry { case .all: - return nil + return 0 case let .filter(id, _, _): return id } @@ -1908,6 +1921,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + private var initializedFilters = false private func reloadFilters(firstUpdate: (() -> Void)? = nil) { let preferencesKey: PostboxViewKey = .preferences(keys: Set([ ApplicationSpecificPreferencesKeys.chatListFilterSettings @@ -1923,21 +1937,31 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let filterItems = chatListFilterItems(context: self.context) var notifiedFirstUpdate = false self.filterDisposable.set((combineLatest(queue: .mainQueue(), - context.account.postbox.combinedView(keys: [ + self.context.account.postbox.combinedView(keys: [ preferencesKey ]), filterItems, - displayTabsAtBottom + displayTabsAtBottom, + self.context.account.postbox.peerView(id: self.context.account.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom in + |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom, peerView in guard let strongSelf = self else { return } + + let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false + strongSelf.isPremium = isPremium + let (_, items) = countAndFilterItems var filterItems: [ChatListFilterTabEntry] = [] - filterItems.append(.all(unreadCount: 0)) + for (filter, unreadCount, hasUnmutedUnread) in items { - filterItems.append(.filter(id: filter.id, text: filter.title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread))) + switch filter { + case .allChats: + filterItems.append(.all(unreadCount: 0)) + case let .filter(id, title, _, _): + filterItems.append(.filter(id: id, text: title, unread: ChatListFilterTabEntryUnreadCount(value: unreadCount, hasUnmuted: hasUnmutedUnread))) + } } var resolvedItems = filterItems @@ -1951,7 +1975,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { wasEmpty = true } - var selectedEntryId = strongSelf.chatListDisplayNode.containerNode.currentItemFilter + + let firstItem = countAndFilterItems.1.first?.0 ?? .allChats + let firstItemEntryId: ChatListFilterTabEntryId + switch firstItem { + case .allChats: + firstItemEntryId = .all + case let .filter(id, _, _, _): + firstItemEntryId = .filter(id) + } + + var selectedEntryId = !strongSelf.initializedFilters ? firstItemEntryId : strongSelf.chatListDisplayNode.containerNode.currentItemFilter var resetCurrentEntry = false if !resolvedItems.contains(where: { $0.id == selectedEntryId }) { resetCurrentEntry = true @@ -1975,12 +2009,26 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } strongSelf.tabContainerData = (resolvedItems, displayTabsAtBottom) var availableFilters: [ChatListContainerNodeFilter] = [] - availableFilters.append(.all) + var hasAllChats = false for item in items { - availableFilters.append(.filter(item.0)) + switch item.0 { + case .allChats: + hasAllChats = true + availableFilters.append(.all) + case .filter: + availableFilters.append(.filter(item.0)) + } + } + if !hasAllChats { + availableFilters.insert(.all, at: 0) } strongSelf.chatListDisplayNode.containerNode.updateAvailableFilters(availableFilters) + if !strongSelf.initializedFilters && selectedEntryId != strongSelf.chatListDisplayNode.containerNode.currentItemFilter { + strongSelf.chatListDisplayNode.containerNode.switchToFilter(id: selectedEntryId, animated: false, completion: nil) + } + strongSelf.initializedFilters = true + let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom let animated = strongSelf.didSetupTabs @@ -1999,7 +2047,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.containerLayoutUpdated(layout, transition: transition) (strongSelf.parent as? TabBarController)?.updateLayout(transition: transition) } else { - strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) + strongSelf.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing, canReorderAllChats: isPremium, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.chatListDisplayNode.inlineTabContainerNode.update(size: CGSize(width: layout.size.width, height: 40.0), sideInset: layout.safeInsets.left, filters: resolvedItems, selectedFilter: selectedEntryId, isReordering: strongSelf.chatListDisplayNode.isReorderingFilters || (strongSelf.chatListDisplayNode.containerNode.currentItemNode.currentState.editing && !strongSelf.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: false, transitionFraction: strongSelf.chatListDisplayNode.containerNode.transitionFraction, presentationData: strongSelf.presentationData, transition: .animated(duration: 0.4, curve: .spring)) } } @@ -2335,7 +2383,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } else { let groupId = self.groupId - let filterPredicate = (self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.data).flatMap(chatListFilterPredicate) + let filterPredicate: ChatListFilterPredicate? + if let filter = self.chatListDisplayNode.containerNode.currentItemNode.chatListFilter, case let .filter(_, _, _, data) = filter { + filterPredicate = chatListFilterPredicate(filter: data) + } else { + filterPredicate = nil + } signal = self.context.account.postbox.transaction { transaction -> Void in markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: groupId, filterPredicate: filterPredicate) if let filterPredicate = filterPredicate { @@ -3185,15 +3238,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if !presetList.isEmpty { items.append(.separator) - for preset in presetList { - let filterType = chatListFilterType(preset) + for case let .filter(id, title, _, data) in presetList { + let filterType = chatListFilterType(data) var badge: ContextMenuActionBadge? for item in filterItems { - if item.0.id == preset.id && item.1 != 0 { + if item.0.id == id && item.1 != 0 { badge = ContextMenuActionBadge(value: "\(item.1)", color: item.2 ? .accent : .inactive) } } - items.append(.action(ContextMenuActionItem(text: preset.title, badge: badge, icon: { theme in + items.append(.action(ContextMenuActionItem(text: title, badge: badge, icon: { theme in let imageName: String switch filterType { case .generic: @@ -3219,7 +3272,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let strongSelf = self else { return } - strongSelf.selectTab(id: .filter(preset.id)) + strongSelf.selectTab(id: .filter(id)) }))) } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 39ba025625..3326c7b8a9 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -448,6 +448,11 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { return _ready.get() } + private let _validLayoutReady = Promise() + var validLayoutReady: Signal { + return _validLayoutReady.get() + } + private var currentItemNodeValue: ChatListContainerItemNode? var currentItemNode: ChatListNode { return self.currentItemNodeValue!.listNode @@ -833,13 +838,13 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } } - func switchToFilter(id: ChatListFilterTabEntryId, completion: (() -> Void)? = nil) { - guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else { - return - } + func switchToFilter(id: ChatListFilterTabEntryId, animated: Bool = true, completion: (() -> Void)? = nil) { self.onFilterSwitch?() if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) { if let itemNode = self.itemNodes[id] { + guard let (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) = self.validLayout else { + return + } self.selectedId = id if let currentItemNode = self.currentItemNodeValue { itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden) @@ -859,8 +864,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { let disposable = MetaDisposable() self.pendingItemNode = (id, itemNode, disposable) - disposable.set((itemNode.listNode.ready - |> filter { $0 } + disposable.set((combineLatest(itemNode.listNode.ready, self.validLayoutReady) + |> filter { $0 && $1 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak itemNode] _ in guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else { @@ -871,7 +876,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { } strongSelf.pendingItemNode = nil - let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) { let previousId = strongSelf.selectedId @@ -937,6 +942,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, isReorderingFilters: Bool, isEditing: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, isReorderingFilters, isEditing) + self._validLayoutReady.set(.single(true)) + var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index a59f088772..b460350302 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -550,6 +550,10 @@ func chatListFilterAddChatsController(context: AccountContext, filter: ChatListF } private func internalChatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController { + guard case let .filter(_, _, _, filterData) = filter else { + return ViewController(navigationBarPresentationData: nil) + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let additionalCategories: [ChatListNodeAdditionalCategory] = [ ChatListNodeAdditionalCategory( @@ -587,12 +591,12 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f .bots: .bots ] for (category, id) in categoryMapping { - if filter.data.categories.contains(category) { + if filterData.categories.contains(category) { selectedCategories.insert(id.rawValue) } } - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filter.data.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100)) + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100)) controller.navigationPresentation = .modal let _ = (controller.result |> take(1) @@ -627,9 +631,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f var filters = filters for i in 0 ..< filters.count { if filters[i].id == filter.id { - filters[i].data.categories = categories - filters[i].data.includePeers.setPeers(includePeers) - filters[i].data.excludePeers = filters[i].data.excludePeers.filter { !filters[i].data.includePeers.peers.contains($0) } + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + updatedData.categories = categories + updatedData.includePeers.setPeers(includePeers) + updatedData.excludePeers = updatedData.excludePeers.filter { !updatedData.includePeers.peers.contains($0) } + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } } } return filters @@ -639,9 +647,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f }) } else { var filter = filter - filter.data.categories = categories - filter.data.includePeers.setPeers(includePeers) - filter.data.excludePeers = filter.data.excludePeers.filter { !filter.data.includePeers.peers.contains($0) } + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + updatedData.categories = categories + updatedData.includePeers.setPeers(includePeers) + updatedData.excludePeers = updatedData.excludePeers.filter { !updatedData.includePeers.peers.contains($0) } + filter = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } updated(filter) controller?.dismiss() } @@ -650,6 +662,9 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f } private func internalChatListFilterExcludeChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController { + guard case let .filter(_, _, _, filterData) = filter else { + return ViewController(navigationBarPresentationData: nil) + } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let additionalCategories: [ChatListNodeAdditionalCategory] = [ ChatListNodeAdditionalCategory( @@ -669,17 +684,17 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex ), ] var selectedCategories = Set() - if filter.data.excludeMuted { + if filterData.excludeMuted { selectedCategories.insert(AdditionalExcludeCategoryId.muted.rawValue) } - if filter.data.excludeRead { + if filterData.excludeRead { selectedCategories.insert(AdditionalExcludeCategoryId.read.rawValue) } - if filter.data.excludeArchived { + if filterData.excludeArchived { selectedCategories.insert(AdditionalExcludeCategoryId.archived.rawValue) } - let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filter.data.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100)) + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filterData.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100)) controller.navigationPresentation = .modal let _ = (controller.result |> take(1) @@ -705,11 +720,15 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex var filters = filters for i in 0 ..< filters.count { if filters[i].id == filter.id { - filters[i].data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) - filters[i].data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) - filters[i].data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) - filters[i].data.excludePeers = excludePeers - filters[i].data.includePeers.setPeers(filters[i].data.includePeers.peers.filter { !filters[i].data.excludePeers.contains($0) }) + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + updatedData.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) + updatedData.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) + updatedData.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) + updatedData.excludePeers = excludePeers + updatedData.includePeers.setPeers(updatedData.includePeers.peers.filter { !updatedData.excludePeers.contains($0) }) + filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } } } return filters @@ -719,11 +738,15 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex }) } else { var filter = filter - filter.data.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) - filter.data.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) - filter.data.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) - filter.data.excludePeers = excludePeers - filter.data.includePeers.setPeers(filter.data.includePeers.peers.filter { !filter.data.excludePeers.contains($0) }) + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + updatedData.excludeMuted = additionalCategoryIds.contains(AdditionalExcludeCategoryId.muted.rawValue) + updatedData.excludeRead = additionalCategoryIds.contains(AdditionalExcludeCategoryId.read.rawValue) + updatedData.excludeArchived = additionalCategoryIds.contains(AdditionalExcludeCategoryId.archived.rawValue) + updatedData.excludePeers = excludePeers + updatedData.includePeers.setPeers(updatedData.includePeers.peers.filter { !updatedData.excludePeers.contains($0) }) + filter = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) + } updated(filter) controller?.dismiss() } @@ -742,27 +765,27 @@ enum ChatListFilterType { case nonContacts } -func chatListFilterType(_ filter: ChatListFilter) -> ChatListFilterType { +func chatListFilterType(_ data: ChatListFilterData) -> ChatListFilterType { let filterType: ChatListFilterType - if filter.data.categories == .all { - if filter.data.excludeRead { + if data.categories == .all { + if data.excludeRead { filterType = .unread - } else if filter.data.excludeMuted { + } else if data.excludeMuted { filterType = .unmuted } else { filterType = .generic } } else { - if filter.data.categories == .channels { + if data.categories == .channels { filterType = .channels - } else if filter.data.categories == .groups { + } else if data.categories == .groups { filterType = .groups - } else if filter.data.categories == .bots { + } else if data.categories == .bots { filterType = .bots - } else if filter.data.categories == .contacts { + } else if data.categories == .contacts { filterType = .contacts - } else if filter.data.categories == .nonContacts { + } else if data.categories == .nonContacts { filterType = .nonContacts } else { filterType = .generic @@ -772,6 +795,32 @@ func chatListFilterType(_ filter: ChatListFilter) -> ChatListFilterType { return filterType } +private extension ChatListFilter { + var title: String { + if case let .filter(_, title, _, _) = self { + return title + } else { + return "" + } + } + + var emoticon: String? { + if case let .filter(_, _, emoticon, _) = self { + return emoticon + } else { + return nil + } + } + + var data: ChatListFilterData? { + if case let .filter(_, _, _, data) = self { + return data + } else { + return nil + } + } +} + func chatListFilterPresetController(context: AccountContext, currentPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { let initialName: String if let currentPreset = currentPreset { @@ -779,7 +828,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } else { initialName = "" } - let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: currentPreset != nil, includeCategories: currentPreset?.data.categories ?? [], excludeMuted: currentPreset?.data.excludeMuted ?? false, excludeRead: currentPreset?.data.excludeRead ?? false, excludeArchived: currentPreset?.data.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data.includePeers.peers ?? [], additionallyExcludePeers: currentPreset?.data.excludePeers ?? [], expandedSections: []) + let initialState = ChatListFilterPresetControllerState(name: initialName, changedName: currentPreset != nil, includeCategories: currentPreset?.data?.categories ?? [], excludeMuted: currentPreset?.data?.excludeMuted ?? false, excludeRead: currentPreset?.data?.excludeRead ?? false, excludeArchived: currentPreset?.data?.excludeArchived ?? false, additionallyIncludePeers: currentPreset?.data?.includePeers.peers ?? [], additionallyExcludePeers: currentPreset?.data?.excludePeers ?? [], expandedSections: []) let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void = { f in @@ -789,24 +838,26 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let presentationData = context.sharedContext.currentPresentationData.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - switch chatListFilterType(filter) { - case .generic: - state.name = initialName - case .unmuted: - state.name = presentationData.strings.ChatListFolder_NameNonMuted - case .unread: - state.name = presentationData.strings.ChatListFolder_NameUnread - case .channels: - state.name = presentationData.strings.ChatListFolder_NameChannels - case .groups: - state.name = presentationData.strings.ChatListFolder_NameGroups - case .bots: - state.name = presentationData.strings.ChatListFolder_NameBots - case .contacts: - state.name = presentationData.strings.ChatListFolder_NameContacts - case .nonContacts: - state.name = presentationData.strings.ChatListFolder_NameNonContacts + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + if let data = filter.data { + switch chatListFilterType(data) { + case .generic: + state.name = initialName + case .unmuted: + state.name = presentationData.strings.ChatListFolder_NameNonMuted + case .unread: + state.name = presentationData.strings.ChatListFolder_NameUnread + case .channels: + state.name = presentationData.strings.ChatListFolder_NameChannels + case .groups: + state.name = presentationData.strings.ChatListFolder_NameGroups + case .bots: + state.name = presentationData.strings.ChatListFolder_NameBots + case .contacts: + state.name = presentationData.strings.ChatListFolder_NameContacts + case .nonContacts: + state.name = presentationData.strings.ChatListFolder_NameNonContacts + } } } return state @@ -832,7 +883,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -840,9 +891,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat skipStateAnimation = true updateState { state in var state = state - state.additionallyIncludePeers = filter.data.includePeers.peers - state.additionallyExcludePeers = filter.data.excludePeers - state.includeCategories = filter.data.categories + state.additionallyIncludePeers = filter.data?.includePeers.peers ?? [] + state.additionallyExcludePeers = filter.data?.excludePeers ?? [] + state.includeCategories = filter.data?.categories ?? [] return state } }) @@ -853,7 +904,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let state = stateValue.with { $0 } var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + let filter: ChatListFilter = .filter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) let _ = (context.engine.peers.currentChatListFilters() |> deliverOnMainQueue).start(next: { filters in @@ -861,12 +912,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat skipStateAnimation = true updateState { state in var state = state - state.additionallyIncludePeers = filter.data.includePeers.peers - state.additionallyExcludePeers = filter.data.excludePeers - state.includeCategories = filter.data.categories - state.excludeRead = filter.data.excludeRead - state.excludeMuted = filter.data.excludeMuted - state.excludeArchived = filter.data.excludeArchived + state.additionallyIncludePeers = filter.data?.includePeers.peers ?? [] + state.additionallyExcludePeers = filter.data?.excludePeers ?? [] + state.includeCategories = filter.data?.categories ?? [] + state.excludeRead = filter.data?.excludeRead ?? false + state.excludeMuted = filter.data?.excludeMuted ?? false + state.excludeArchived = filter.data?.excludeArchived ?? false return state } }) @@ -997,18 +1048,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - var updatedFilter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + + var filterId = currentPreset?.id ?? -1 if currentPreset == nil { - updatedFilter.id = context.engine.peers.generateNewChatListFilterId(filters: filters) + filterId = context.engine.peers.generateNewChatListFilterId(filters: filters) } + var updatedFilter: ChatListFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + var filters = filters if let _ = currentPreset { var found = false for i in 0 ..< filters.count { - if filters[i].id == updatedFilter.id { - var includePeers = filters[i].data.includePeers + if filters[i].id == updatedFilter.id, case let .filter(_, _, _, data) = filters[i] { + var updatedData = data + var includePeers = updatedData.includePeers includePeers.setPeers(state.additionallyIncludePeers) - updatedFilter.data.includePeers = includePeers + updatedData.includePeers = includePeers + updatedFilter = .filter(id: filterId, title: state.name, emoticon: currentPreset?.emoticon, data: updatedData) filters[i] = updatedFilter found = true } @@ -1100,16 +1156,19 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat } attemptNavigationImpl = { let state = stateValue.with { $0 } - if let currentPreset = currentPreset { - var currentPresetWithoutPinnerPeers = currentPreset + if let currentPreset = currentPreset, case let .filter(currentId, currentTitle, currentEmoticon, currentData) = currentPreset { + var currentPresetWithoutPinnedPeers = currentPreset + var currentIncludePeers = ChatListFilterIncludePeers() - currentIncludePeers.setPeers(currentPresetWithoutPinnerPeers.data.includePeers.peers) - currentPresetWithoutPinnerPeers.data.includePeers = currentIncludePeers + currentIncludePeers.setPeers(currentData.includePeers.peers) + var currentPresetWithoutPinnedPeersData = currentData + currentPresetWithoutPinnedPeersData.includePeers = currentIncludePeers + currentPresetWithoutPinnedPeers = .filter(id: currentId, title: currentTitle, emoticon: currentEmoticon, data: currentPresetWithoutPinnedPeersData) var includePeers = ChatListFilterIncludePeers() includePeers.setPeers(state.additionallyIncludePeers) - let filter = ChatListFilter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) - if currentPresetWithoutPinnerPeers != filter { + let filter: ChatListFilter = .filter(id: currentPreset.id, title: state.name, emoticon: currentPreset.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers)) + if currentPresetWithoutPinnedPeers != filter { displaySaveAlert() return false } diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 0ae2c4f341..2c2a3f0fe9 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -74,7 +74,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case suggestedPreset(index: PresetIndex, title: String, label: String, preset: ChatListFilterData) case suggestedAddCustom(String) case listHeader(String) - case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool) + case preset(index: PresetIndex, title: String, label: String, preset: ChatListFilter, canBeReordered: Bool, canBeDeleted: Bool, isEditing: Bool, isAllChats: Bool) case addItem(text: String, isEditing: Bool) case listFooter(String) @@ -95,7 +95,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return 0 case .listHeader: return 100 - case let .preset(index, _, _, _, _, _, _): + case let .preset(index, _, _, _, _, _, _, _): return 101 + index.value case .addItem: return 1000 @@ -122,7 +122,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { return .suggestedAddCustom case .listHeader: return .listHeader - case let .preset(_, _, _, preset, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _): return .preset(preset.id) case .addItem: return .addItem @@ -152,8 +152,8 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { }) case let .listHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) - case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing): - return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, sectionId: self.section, action: { + case let .preset(_, title, label, preset, canBeReordered, canBeDeleted, isEditing, isAllChats): + return ChatListFilterPresetListItem(presentationData: presentationData, preset: preset, title: title, label: label, editing: ChatListFilterPresetListItemEditing(editable: true, editing: isEditing, revealed: false), canBeReordered: canBeReordered, canBeDeleted: canBeDeleted, isAllChats: isAllChats, sectionId: self.section, action: { arguments.openPreset(preset) }, setItemWithRevealedOptions: { lhs, rhs in arguments.setItemWithRevealedOptions(lhs, rhs) @@ -191,15 +191,17 @@ 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) -> [ChatListFilterPresetListEntry] { +private func chatListFilterPresetListControllerEntries(presentationData: PresentationData, state: ChatListFilterPresetListControllerState, filters: [(ChatListFilter, Int)], updatedFilterOrder: [Int32]?, suggestedFilters: [ChatListFeaturedFilter], settings: ChatListFilterSettings, isPremium: Bool) -> [ChatListFilterPresetListEntry] { var entries: [ChatListFilterPresetListEntry] = [] entries.append(.screenHeader(presentationData.strings.ChatListFolderSettings_Info)) let filteredSuggestedFilters = suggestedFilters.filter { suggestedFilter in for (filter, _) in filters { - if filter.data == suggestedFilter.data { - return false + if case let .filter(_, _, _, data) = filter { + if data == suggestedFilter.data { + return false + } } } return true @@ -209,7 +211,12 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection)) for (filter, chatCount) in filtersWithAppliedOrder(filters: filters, order: updatedFilterOrder) { - entries.append(.preset(index: PresetIndex(value: entries.count), title: filter.title, label: chatCount == 0 ? "" : "\(chatCount)", preset: filter, canBeReordered: filters.count > 1, canBeDeleted: true, isEditing: state.isEditing)) + 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)) + } + 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)) + } } if filters.count < 10 { entries.append(.addItem(text: presentationData.strings.ChatListFolderSettings_NewFolder, isEditing: state.isEditing)) @@ -263,7 +270,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in var filters = filters let id = context.engine.peers.generateNewChatListFilterId(filters: filters) - filters.insert(ChatListFilter(id: id, title: title, emoticon: nil, data: data), at: 0) + filters.insert(.filter(id: id, title: title, emoticon: nil, data: data), at: 0) return filters } |> deliverOnMainQueue).start(next: { _ in @@ -291,7 +298,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } else { if filters.count >= limit { var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { let controller = PremiumIntroScreen(context: context) replaceImpl?(controller) }) @@ -360,9 +367,12 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch filtersWithCounts.get(), preferences, updatedFilterOrder.get(), - featuredFilters + featuredFilters, + context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) ) - |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters -> (ItemListControllerState, (ItemListNodeState, Any)) in + |> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, result -> (ItemListControllerState, (ItemListNodeState, Any)) in + let isPremium = result?.isPremium ?? false + let filterSettings = preferences.values[ApplicationSpecificPreferencesKeys.chatListFilterSettings]?.get(ChatListFilterSettings.self) ?? ChatListFilterSettings.default let leftNavigationButton: ItemListNavigationButton? switch mode { @@ -431,7 +441,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), 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), style: .blocks, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -461,7 +471,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [ChatListFilterPresetListEntry]) -> Signal in let fromEntry = entries[fromIndex] - guard case let .preset(_, _, _, fromPreset, _, _, _) = fromEntry else { + guard case let .preset(_, _, _, fromPreset, _, _, _, _) = fromEntry else { return .single(false) } var referenceFilter: ChatListFilter? @@ -469,7 +479,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch var afterAll = false if toIndex < entries.count { switch entries[toIndex] { - case let .preset(_, _, _, preset, _, _, _): + case let .preset(_, _, _, preset, _, _, _, _): referenceFilter = preset default: if entries[toIndex] < fromEntry { diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift index ec45b3a08a..9e8ba9da0c 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListItem.swift @@ -23,6 +23,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { let editing: ChatListFilterPresetListItemEditing let canBeReordered: Bool let canBeDeleted: Bool + let isAllChats: Bool let sectionId: ItemListSectionId let action: () -> Void let setItemWithRevealedOptions: (Int32?, Int32?) -> Void @@ -36,6 +37,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { editing: ChatListFilterPresetListItemEditing, canBeReordered: Bool, canBeDeleted: Bool, + isAllChats: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, setItemWithRevealedOptions: @escaping (Int32?, Int32?) -> Void, @@ -48,6 +50,7 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { self.editing = editing self.canBeReordered = canBeReordered self.canBeDeleted = canBeDeleted + self.isAllChats = isAllChats self.sectionId = sectionId self.action = action self.setItemWithRevealedOptions = setItemWithRevealedOptions @@ -92,7 +95,9 @@ final class ChatListFilterPresetListItem: ListViewItem, ItemListItem { } } - var selectable: Bool = true + var selectable: Bool { + return !self.isAllChats + } func selected(listView: ListView){ listView.clearHighlightAnimated(true) @@ -205,7 +210,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN } let titleAttributedString = NSMutableAttributedString() - titleAttributedString.append(NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) + titleAttributedString.append(NSAttributedString(string: item.isAllChats ? item.presentationData.strings.ChatList_FolderAllChats : item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)) var editableControlSizeAndApply: (CGFloat, (CGFloat) -> ItemListEditableControlNode)? var reorderControlSizeAndApply: (CGFloat, (CGFloat, Bool, ContainedViewLayoutTransition) -> ItemListEditableReorderControlNode)? @@ -293,6 +298,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN editableControlNode?.removeFromSupernode() }) } + strongSelf.editableControlNode?.isHidden = !item.canBeDeleted if let reorderControlSizeAndApply = reorderControlSizeAndApply { if strongSelf.reorderControlNode == nil { @@ -374,6 +380,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN if let arrowImage = strongSelf.arrowNode.image { strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width + revealOffset, y: floorToScreenPixels((layout.contentSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) } + strongSelf.arrowNode.isHidden = item.isAllChats strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 0.0), size: CGSize(width: params.width - params.rightInset - 56.0 - (leftInset + revealOffset + editingOffset), height: layout.contentSize.height)) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index f742a7985e..dc844aacc6 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -192,7 +192,7 @@ private final class ItemNode: ASDisplayNode { self.pressed() } - func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isAllChats: Bool, isReordering: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { + func updateText(strings: PresentationStrings, title: String, shortTitle: String, unreadCount: Int, unreadHasUnmuted: Bool, isNoFilter: Bool, selectionFraction: CGFloat, isEditing: Bool, isAllChats: Bool, isReordering: Bool, canReorderAllChats: Bool, presentationData: PresentationData, transition: ContainedViewLayoutTransition) { self.isEditing = isEditing if self.theme !== presentationData.theme { @@ -215,7 +215,7 @@ private final class ItemNode: ASDisplayNode { self.selectionFraction = selectionFraction self.unreadCount = unreadCount - transition.updateAlpha(node: self.containerNode, alpha: isEditing || (isReordering && isAllChats) ? 0.5 : 1.0) + transition.updateAlpha(node: self.containerNode, alpha: isEditing || (isReordering && isAllChats && !canReorderAllChats) ? 0.5 : 1.0) if isReordering && !isAllChats { if self.deleteButtonNode == nil { @@ -265,7 +265,7 @@ private final class ItemNode: ASDisplayNode { if self.isReordering != isReordering { self.isReordering = isReordering - if self.isReordering && !isAllChats { + if self.isReordering && (!isAllChats || canReorderAllChats) { self.startShaking() } else { self.layer.removeAnimation(forKey: "shaking_position") @@ -474,14 +474,14 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { private var reorderedItemIds: [ChatListFilterTabEntryId]? private lazy var hapticFeedback = { HapticFeedback() }() - private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData)? + private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, transitionFraction: CGFloat, presentationData: PresentationData)? var reorderedFilterIds: [Int32]? { return self.reorderedItemIds.flatMap { $0.compactMap { switch $0 { case .all: - return nil + return 0 case let .filter(id): return id } @@ -516,7 +516,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } for (id, itemNode) in strongSelf.itemNodes { if itemNode.view.convert(itemNode.bounds, to: strongSelf.view).contains(point) { - if case .all = id { + if case .all = id, !(strongSelf.currentParams?.canReorderAllChats ?? false) { return false } return true @@ -553,8 +553,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { strongSelf.addSubnode(itemNode) strongSelf.reorderingItemPosition = (itemNode.frame.minX, 0.0) - if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) } return } @@ -573,13 +573,15 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { strongSelf.reorderingItemPosition = nil strongSelf.reorderingAutoScrollAnimator?.invalidate() strongSelf.reorderingAutoScrollAnimator = nil - if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) } }, moved: { [weak self] offset in guard let strongSelf = self, let reorderingItem = strongSelf.reorderingItem else { return } + + let minIndex = (strongSelf.currentParams?.canReorderAllChats ?? false) ? 0 : 1 if let reorderingItemNode = strongSelf.itemNodes[reorderingItem], let (initial, _) = strongSelf.reorderingItemPosition, let reorderedItemIds = strongSelf.reorderedItemIds, let currentItemIndex = reorderedItemIds.firstIndex(of: reorderingItem) { for (id, itemNode) in strongSelf.itemNodes { @@ -591,9 +593,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { if reorderingItemNode.frame.intersects(itemFrame) { let targetIndex: Int if reorderingItemNode.frame.midX < itemFrame.midX { - targetIndex = max(1, itemIndex - 1) + targetIndex = max(minIndex, itemIndex - 1) } else { - targetIndex = max(1, min(reorderedItemIds.count - 1, itemIndex)) + targetIndex = max(minIndex, min(reorderedItemIds.count - 1, itemIndex)) } if targetIndex != currentItemIndex { strongSelf.hapticFeedback.tap() @@ -607,8 +609,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { updatedReorderedItemIds.insert(reorderingItem, at: targetIndex) } strongSelf.reorderedItemIds = updatedReorderedItemIds - if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .animated(duration: 0.25, curve: .easeInOut)) } } break @@ -618,8 +620,8 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { strongSelf.reorderingItemPosition = (initial, offset) } - if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, transitionFraction, presentationData) = strongSelf.currentParams { - strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate) + if let (size, sideInset, filters, selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData) = strongSelf.currentParams { + strongSelf.update(size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering: isReordering, isEditing: isEditing, canReorderAllChats: canReorderAllChats, transitionFraction: transitionFraction, presentationData: presentationData, transition: .immediate) } }) self.reorderingGesture = reorderingGesture @@ -635,7 +637,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.scrollNode.layer.removeAllAnimations() } - func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) { + func update(size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, transitionFraction: CGFloat, presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) { let isFirstTime = self.currentParams == nil let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition @@ -680,7 +682,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { self.reorderedItemIds = nil } - self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, transitionFraction, presentationData: presentationData) + self.currentParams = (size: size, sideInset: sideInset, filters: filters, selectedFilter: selectedFilter, isReordering, isEditing, canReorderAllChats, transitionFraction, presentationData: presentationData) self.reorderingGesture?.isEnabled = isReordering @@ -762,7 +764,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { selectionFraction = 0.0 } - itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isAllChats: isNoFilter, isReordering: isReordering, presentationData: presentationData, transition: itemNodeTransition) + itemNode.updateText(strings: presentationData.strings, title: filter.title(strings: presentationData.strings), shortTitle: filter.shortTitle(strings: presentationData.strings), unreadCount: unreadCount, unreadHasUnmuted: unreadHasUnmuted, isNoFilter: isNoFilter, selectionFraction: selectionFraction, isEditing: isEditing, isAllChats: isNoFilter, isReordering: isReordering, canReorderAllChats: canReorderAllChats, presentationData: presentationData, transition: itemNodeTransition) } var removeKeys: [ChatListFilterTabEntryId] = [] for (id, _) in self.itemNodes { @@ -893,7 +895,13 @@ final class ChatListFilterTabContainerNode: ASDisplayNode { } else { transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) } - transition.updateAlpha(node: self.selectedLineNode, alpha: isReordering && selectedFilter == .all ? 0.5 : 1.0) + let lineAlpha: CGFloat + if isReordering && canReorderAllChats { + lineAlpha = 0.0 + } else { + lineAlpha = isReordering && selectedFilter == .all ? 0.5 : 1.0 + } + transition.updateAlpha(node: self.selectedLineNode, alpha: lineAlpha) if let previousSelectedFrame = self.previousSelectedFrame { let previousContentOffsetX = max(0.0, min(previousContentWidth - previousScrollBounds.width, floor(previousSelectedFrame.midX - previousScrollBounds.width / 2.0))) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabInlineContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabInlineContainerNode.swift index e6674f6282..c001b7d82b 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabInlineContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabInlineContainerNode.swift @@ -390,7 +390,7 @@ final class ChatListFilterTabInlineContainerNode: ASDisplayNode { $0.compactMap { switch $0 { case .all: - return nil + return 0 case let .filter(id): return id } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index e77cf7a025..c57f9f9272 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -842,9 +842,9 @@ public final class ChatListNode: ListView { switch result { case .done: break - case .limitExceeded: + case let .limitExceeded(count, _): var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, action: { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { let premiumScreen = PremiumIntroScreen(context: context) replaceImpl?(premiumScreen) }) @@ -1175,8 +1175,12 @@ public final class ChatListNode: ListView { updatedScrollPosition = nil } - let filterData = filter.flatMap { filter -> ChatListItemFilterData in - return ChatListItemFilterData(excludesArchived: filter.data.excludeArchived) + let filterData = filter.flatMap { filter -> ChatListItemFilterData? in + if case let .filter(_, _, _, data) = filter { + return ChatListItemFilterData(excludesArchived: data.excludeArchived) + } else { + return nil + } } return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode) @@ -2265,13 +2269,13 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres if let chatListFilters = chatListFilters { var result = "" - for filter in chatListFilters { - let predicate = chatListFilterPredicate(filter: filter.data) + for case let .filter(_, title, _, data) in chatListFilters { + let predicate = chatListFilterPredicate(filter: data) if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) { if !result.isEmpty { result.append(", ") } - result.append(filter.title) + result.append(title) } } diff --git a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift index 122970ea2e..bb89d776e9 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNodeLocation.swift @@ -110,7 +110,12 @@ public func chatListFilterPredicate(filter: ChatListFilterData) -> ChatListFilte } func chatListViewForLocation(groupId: PeerGroupId, location: ChatListNodeLocation, account: Account) -> Signal { - let filterPredicate: ChatListFilterPredicate? = (location.filter?.data).flatMap(chatListFilterPredicate) + let filterPredicate: ChatListFilterPredicate? + if let filter = location.filter, case let .filter(_, _, _, data) = filter { + filterPredicate = chatListFilterPredicate(filter: data) + } else { + filterPredicate = nil + } switch location { case let .initial(count, _): diff --git a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift index 68ed9415f5..bc9501cdf8 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListViewTransition.swift @@ -188,9 +188,7 @@ func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toV } } else if fromView.filteredEntries.isEmpty || fromView.filter != toView.filter { var updateEmpty = true - if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, fromFilter.data.includePeers.pinnedPeers != toFilter.data.includePeers.pinnedPeers { - var fromData = fromFilter.data - let toData = toFilter.data + if !fromView.filteredEntries.isEmpty, let fromFilter = fromView.filter, let toFilter = toView.filter, case var .filter(_, _, _, fromData) = fromFilter, case let .filter(_, _, _, toData) = toFilter, fromData.includePeers.pinnedPeers != toData.includePeers.pinnedPeers { fromData.includePeers = toData.includePeers if fromData == toData { options.insert(.AnimateInsertion) diff --git a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift index 448b94ec99..f25162654c 100644 --- a/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift +++ b/submodules/ChatListUI/Sources/TabBarChatListFilterController.swift @@ -17,10 +17,10 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt unreadCountItems.append(.totalInGroup(.root)) var additionalPeerIds = Set() var additionalGroupIds = Set() - for filter in filters { - additionalPeerIds.formUnion(filter.data.includePeers.peers) - additionalPeerIds.formUnion(filter.data.excludePeers) - if !filter.data.excludeArchived { + for case let .filter(_, _, _, data) in filters { + additionalPeerIds.formUnion(data.includePeers.peers) + additionalPeerIds.formUnion(data.excludePeers) + if !data.excludeArchived { additionalGroupIds.insert(Namespaces.PeerGroup.archive) } } @@ -79,50 +79,29 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt let totalBadge = 0 for filter in filters { - var tags: [PeerSummaryCounterTags] = [] - if filter.data.categories.contains(.contacts) { - tags.append(.contact) - } - if filter.data.categories.contains(.nonContacts) { - tags.append(.nonContact) - } - if filter.data.categories.contains(.groups) { - tags.append(.group) - } - if filter.data.categories.contains(.bots) { - tags.append(.bot) - } - if filter.data.categories.contains(.channels) { - tags.append(.channel) - } - var count = 0 var unmutedUnreadCount = 0 - if let totalState = totalStates[.root] { - for tag in tags { - if filter.data.excludeMuted { - if let value = totalState.filteredCounters[tag] { - if value.chatCount != 0 { - count += Int(value.chatCount) - unmutedUnreadCount += Int(value.chatCount) - } - } - } else { - if let value = totalState.absoluteCounters[tag] { - count += Int(value.chatCount) - } - if let value = totalState.filteredCounters[tag] { - if value.chatCount != 0 { - unmutedUnreadCount += Int(value.chatCount) - } - } - } + if case let .filter(_, _, _, data) = filter { + var tags: [PeerSummaryCounterTags] = [] + if data.categories.contains(.contacts) { + tags.append(.contact) } - } - if !filter.data.excludeArchived { - if let totalState = totalStates[Namespaces.PeerGroup.archive] { + if data.categories.contains(.nonContacts) { + tags.append(.nonContact) + } + if data.categories.contains(.groups) { + tags.append(.group) + } + if data.categories.contains(.bots) { + tags.append(.bot) + } + if data.categories.contains(.channels) { + tags.append(.channel) + } + + if let totalState = totalStates[.root] { for tag in tags { - if filter.data.excludeMuted { + if data.excludeMuted { if let value = totalState.filteredCounters[tag] { if value.chatCount != 0 { count += Int(value.chatCount) @@ -141,62 +120,85 @@ func chatListFilterItems(context: AccountContext) -> Signal<(Int, [(ChatListFilt } } } - } - for peerId in filter.data.includePeers.peers { - if let (tag, peerCount, hasUnmuted, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue { - var matches = true - if tags.contains(tag) { - if isMuted && filter.data.excludeMuted { - } else { - matches = false - } - } - if matches { - let matchesGroup: Bool - switch groupId { - case .root: - matchesGroup = true - case .group: - if groupId == Namespaces.PeerGroup.archive { - matchesGroup = !filter.data.excludeArchived + if !data.excludeArchived { + if let totalState = totalStates[Namespaces.PeerGroup.archive] { + for tag in tags { + if data.excludeMuted { + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + count += Int(value.chatCount) + unmutedUnreadCount += Int(value.chatCount) + } + } } else { - matchesGroup = false - } - } - if matchesGroup && peerCount != 0 { - count += 1 - if hasUnmuted { - unmutedUnreadCount += 1 + if let value = totalState.absoluteCounters[tag] { + count += Int(value.chatCount) + } + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + unmutedUnreadCount += Int(value.chatCount) + } + } } } } } - } - for peerId in filter.data.excludePeers { - if let (tag, peerCount, _, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue { - var matches = true - if tags.contains(tag) { - if isMuted && filter.data.excludeMuted { - matches = false - } - } - - if matches { - let matchesGroup: Bool - switch groupId { - case .root: - matchesGroup = true - case .group: - if groupId == Namespaces.PeerGroup.archive { - matchesGroup = !filter.data.excludeArchived + for peerId in data.includePeers.peers { + if let (tag, peerCount, hasUnmuted, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue { + var matches = true + if tags.contains(tag) { + if isMuted && data.excludeMuted { } else { - matchesGroup = false + matches = false } } - if matchesGroup && peerCount != 0 { - count -= 1 - if !isMuted { - unmutedUnreadCount -= 1 + if matches { + let matchesGroup: Bool + switch groupId { + case .root: + matchesGroup = true + case .group: + if groupId == Namespaces.PeerGroup.archive { + matchesGroup = !data.excludeArchived + } else { + matchesGroup = false + } + } + if matchesGroup && peerCount != 0 { + count += 1 + if hasUnmuted { + unmutedUnreadCount += 1 + } + } + } + } + } + for peerId in data.excludePeers { + if let (tag, peerCount, _, groupIdValue, isMuted) = peerTagAndCount[peerId], peerCount != 0, let groupId = groupIdValue { + var matches = true + if tags.contains(tag) { + if isMuted && data.excludeMuted { + matches = false + } + } + + if matches { + let matchesGroup: Bool + switch groupId { + case .root: + matchesGroup = true + case .group: + if groupId == Namespaces.PeerGroup.archive { + matchesGroup = !data.excludeArchived + } else { + matchesGroup = false + } + } + if matchesGroup && peerCount != 0 { + count -= 1 + if !isMuted { + unmutedUnreadCount -= 1 + } } } } diff --git a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift index 2aa41374a6..42ec14d704 100644 --- a/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift +++ b/submodules/Components/MultilineTextComponent/Sources/MultilineTextComponent.swift @@ -20,6 +20,10 @@ public final class MultilineTextComponent: Component { public let insets: UIEdgeInsets public let textShadowColor: UIColor? public let textStroke: (UIColor, CGFloat)? + public let highlightColor: UIColor? + public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? + public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? public init( text: TextContent, @@ -31,7 +35,11 @@ public final class MultilineTextComponent: Component { cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), textShadowColor: UIColor? = nil, - textStroke: (UIColor, CGFloat)? = nil + textStroke: (UIColor, CGFloat)? = nil, + highlightColor: UIColor? = nil, + highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, + tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, + longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil ) { self.text = text self.horizontalAlignment = horizontalAlignment @@ -43,6 +51,10 @@ public final class MultilineTextComponent: Component { self.insets = insets self.textShadowColor = textShadowColor self.textStroke = textStroke + self.highlightColor = highlightColor + self.highlightAction = highlightAction + self.tapAction = tapAction + self.longTapAction = longTapAction } public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool { @@ -90,10 +102,18 @@ public final class MultilineTextComponent: Component { return false } + if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor { + if !lhsHighlightColor.isEqual(rhsHighlightColor) { + return false + } + } else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) { + return false + } + return true } - public final class View: TextView { + public final class View: ImmediateTextView { public func update(component: MultilineTextComponent, availableSize: CGSize) -> CGSize { let attributedString: NSAttributedString switch component.text { @@ -102,26 +122,25 @@ public final class MultilineTextComponent: Component { case let .markdown(text, attributes): attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes) } + + self.attributedText = attributedString + self.maximumNumberOfLines = component.maximumNumberOfLines + self.truncationType = component.truncationType + self.textAlignment = component.horizontalAlignment + self.verticalAlignment = component.verticalAlignment + self.lineSpacing = component.lineSpacing + self.cutout = component.cutout + self.insets = component.insets + self.textShadowColor = component.textShadowColor + self.textStroke = component.textStroke + self.linkHighlightColor = component.highlightColor + self.highlightAttributeAction = component.highlightAction + self.tapAttributeAction = component.tapAction + self.longTapAttributeAction = component.longTapAction - let makeLayout = TextView.asyncLayout(self) - let (layout, apply) = makeLayout(TextNodeLayoutArguments( - attributedString: attributedString, - backgroundColor: nil, - maximumNumberOfLines: component.maximumNumberOfLines, - truncationType: component.truncationType, - constrainedSize: availableSize, - alignment: component.horizontalAlignment, - verticalAlignment: component.verticalAlignment, - lineSpacing: component.lineSpacing, - cutout: component.cutout, - insets: component.insets, - textShadowColor: component.textShadowColor, - textStroke: component.textStroke, - displaySpoilers: false - )) - let _ = apply() - - return layout.size + let size = self.updateLayout(availableSize) + + return size } } diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 9d322809da..ec7dc58860 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -59,6 +59,12 @@ open class ViewControllerComponentContainer: ViewController { case `default` } + public enum StatusBarStyle { + case none + case ignore + case `default` + } + public final class Environment: Equatable { public let statusBarHeight: CGFloat public let navigationHeight: CGFloat @@ -127,11 +133,11 @@ open class ViewControllerComponentContainer: ViewController { } public final class Node: ViewControllerTracingNode { - private var presentationData: PresentationData + fileprivate var presentationData: PresentationData private weak var controller: ViewControllerComponentContainer? private var component: AnyComponent - private let theme: PresentationTheme? + var theme: PresentationTheme? public let hostView: ComponentHostView private var currentIsVisible: Bool = false @@ -204,30 +210,68 @@ open class ViewControllerComponentContainer: ViewController { } private let context: AccountContext - private let theme: PresentationTheme? + private var theme: PresentationTheme? private let component: AnyComponent - public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + private var presentationDataDisposable: Disposable? + private var validLayout: ContainerViewLayout? + + public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { self.context = context self.component = AnyComponent(component) self.theme = theme + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let navigationBarPresentationData: NavigationBarPresentationData? switch navigationBarAppearance { case .none: navigationBarPresentationData = nil case .transparent: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 }, hideBackground: true, hideBadge: false, hideSeparator: true) + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData, hideBackground: true, hideBadge: false, hideSeparator: true) case .default: - navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 }) + navigationBarPresentationData = NavigationBarPresentationData(presentationData: presentationData) } super.init(navigationBarPresentationData: navigationBarPresentationData) + + self.presentationDataDisposable = (self.context.sharedContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + strongSelf.node.presentationData = presentationData + + switch statusBarStyle { + case .none: + strongSelf.statusBar.statusBarStyle = .Hide + case .ignore: + strongSelf.statusBar.statusBarStyle = .Ignore + case .default: + strongSelf.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style + } + + if let layout = strongSelf.validLayout { + strongSelf.containerLayoutUpdated(layout, transition: .immediate) + } + } + }) + + switch statusBarStyle { + case .none: + self.statusBar.statusBarStyle = .Hide + case .ignore: + self.statusBar.statusBarStyle = .Ignore + case .default: + self.statusBar.statusBarStyle = presentationData.theme.rootController.statusBarStyle.style + } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.presentationDataDisposable?.dispose() + } + override open func loadDisplayNode() { self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) @@ -255,6 +299,7 @@ open class ViewControllerComponentContainer: ViewController { let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY + self.validLayout = layout self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } diff --git a/submodules/ContextUI/Sources/PeekControllerContent.swift b/submodules/ContextUI/Sources/PeekControllerContent.swift index d770e1ad29..ba9b23d524 100644 --- a/submodules/ContextUI/Sources/PeekControllerContent.swift +++ b/submodules/ContextUI/Sources/PeekControllerContent.swift @@ -30,5 +30,7 @@ public protocol PeekControllerContentNode { } public protocol PeekControllerAccessoryNode { + var dismiss: () -> Void { get set } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) } diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index 8893b785da..1895dd2955 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -115,6 +115,9 @@ final class PeekControllerNode: ViewControllerTracingNode { self.addSubnode(self.actionsContainerNode) if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { + self.fullScreenAccessoryNode?.dismiss = { [weak self] in + self?.requestDismiss() + } self.addSubnode(fullScreenAccessoryNode) } @@ -194,6 +197,7 @@ final class PeekControllerNode: ViewControllerTracingNode { if let fullScreenAccessoryNode = self.fullScreenAccessoryNode { fullScreenAccessoryNode.updateLayout(size: layout.size, transition: transition) + transition.updateFrame(node: fullScreenAccessoryNode, frame: CGRect(origin: .zero, size: layout.size)) } self.contentNodeHasValidLayout = true diff --git a/submodules/Display/Source/ImmediateTextNode.swift b/submodules/Display/Source/ImmediateTextNode.swift index 612fb81758..b7dae91235 100644 --- a/submodules/Display/Source/ImmediateTextNode.swift +++ b/submodules/Display/Source/ImmediateTextNode.swift @@ -226,7 +226,7 @@ public class ASTextNode: ImmediateTextNode { } } -public class ImmediateTextView: TextView { +open class ImmediateTextView: TextView { public var attributedText: NSAttributedString? public var textAlignment: NSTextAlignment = .natural public var verticalAlignment: TextVerticalAlignment = .top diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 5ce6f8cadf..99bfdc5ac8 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -4,7 +4,7 @@ import SwiftSignalKit private var backArrowImageCache: [Int32: UIImage] = [:] -public final class SparseNode: ASDisplayNode { +open class SparseNode: ASDisplayNode { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero { return nil diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index fed1774a06..a5b9e8c6bc 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1621,7 +1621,6 @@ open class TextView: UIView { private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout { if let attributedString = attributedString { - let stringLength = attributedString.length let font: CTFont diff --git a/submodules/InstantPageCache/BUILD b/submodules/InstantPageCache/BUILD index 101fd0f887..0b2afced14 100644 --- a/submodules/InstantPageCache/BUILD +++ b/submodules/InstantPageCache/BUILD @@ -15,6 +15,8 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/PersistentStringHash:PersistentStringHash", + "//submodules/AccountContext:AccountContext", + "//submodules/UrlHandling:UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift b/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift new file mode 100644 index 0000000000..07d32a5e33 --- /dev/null +++ b/submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift @@ -0,0 +1,88 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext +//import InstantPageUI +import UrlHandling + +public func extractAnchor(string: String) -> (String, String?) { + var anchorValue: String? + if let anchorRange = string.range(of: "#") { + let anchor = string[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } + } + var trimmedUrl = string + if let anchor = anchorValue, let anchorRange = string.range(of: "#\(anchor)") { + let url = string[.. Signal { + var faqUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL + if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { + faqUrl = "https://telegram.org/faq#general-questions" + } + return cachedInternalInstantPage(context: context, url: faqUrl) +} + +public func cachedTermsPage(context: AccountContext) -> Signal { + var termsUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_Terms_URL + if termsUrl == "Settings.Terms_URL" || termsUrl.isEmpty { + termsUrl = "https://telegram.org/tos" + } + return cachedInternalInstantPage(context: context, url: termsUrl) +} + +public func cachedPrivacyPage(context: AccountContext) -> Signal { + var privacyUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_PrivacyPolicy_URL + if privacyUrl == "Settings.PrivacyPolicy_URL" || privacyUrl.isEmpty { + privacyUrl = "https://telegram.org/privacy" + } + return cachedInternalInstantPage(context: context, url: privacyUrl) +} + +private func cachedInternalInstantPage(context: AccountContext, url: String) -> Signal { + let (cachedUrl, anchor) = extractAnchor(string: url) + return cachedInstantPage(postbox: context.account.postbox, url: cachedUrl) + |> mapToSignal { cachedInstantPage -> Signal in + let updated = resolveInstantViewUrl(account: context.account, url: url) + |> afterNext { result in + if case let .instantView(webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage { + if instantPage.isComplete { + let _ = updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage).start() + } else { + let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage) + |> mapToSignal { webPage -> Signal in + if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + return updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage) + } else { + return .complete() + } + }).start() + } + } + } + + let now = Int32(CFAbsoluteTimeGetCurrent()) + if let cachedInstantPage = cachedInstantPage, case let .Loaded(content) = cachedInstantPage.webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + let current: Signal = .single(.instantView(cachedInstantPage.webPage, anchor)) + if now > cachedInstantPage.timestamp + refreshTimeout { + return current + |> then(updated) + } else { + return current + } + } else { + return updated + } + } +} diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 7e34f7d15f..0f311fd5f3 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -603,14 +603,16 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo var updatedLabelBadgeImage: UIImage? var currentCredibilityIconImage: UIImage? - if item.peer.isScam { - currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - } else if item.peer.isFake { - currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) - } else if item.peer.isVerified { - currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) - } else if item.peer.isPremium { - currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + if item.peer.id != item.context.account.peerId { + if item.peer.isScam { + currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if item.peer.isFake { + currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(item.presentationData.theme, strings: item.presentationData.strings, type: .regular) + } else if item.peer.isVerified { + currentCredibilityIconImage = PresentationResourcesChatList.verifiedIcon(item.presentationData.theme) + } else if item.peer.isPremium { + currentCredibilityIconImage = PresentationResourcesChatList.premiumIcon(item.presentationData.theme) + } } var titleIconsWidth: CGFloat = 0.0 diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 3610c24fe3..f239cf8fa2 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -241,6 +241,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { self.installationActionBackgroundNode.displayWithoutProcessing = true self.installationActionBackgroundNode.isLayerBacked = true self.installationActionNode = HighlightableButtonNode() + self.installationActionNode.hitTestSlop = UIEdgeInsets(top: -16.0, left: -16.0, bottom: -16.0, right: -16.0) self.installTextNode = TextNode() self.installTextNode.isUserInteractionEnabled = false diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift index 0e7cf8b938..721af4b8b4 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift @@ -153,12 +153,14 @@ class IncreaseLimitHeaderItemNode: ListViewItemNode { UIColor(rgb: 0xe46ace) ], inactiveTitle: item.strings.Premium_Free, + inactiveValue: "", inactiveTitleColor: .black, activeTitle: item.strings.Premium_Premium, activeValue: "\(item.premiumCount)", activeTitleColor: .white, badgeIconName: badgeIconName, - badgeText: "\(item.count)" + badgeText: "\(item.count)", + badgePosition: CGFloat(item.count) / CGFloat(item.premiumCount) )), environment: {}, containerSize: CGSize(width: layout.size.width - params.leftInset - params.rightInset, height: 200.0) diff --git a/submodules/Postbox/Package.swift b/submodules/Postbox/Package.swift index 5b5c81e392..6cbe03959d 100644 --- a/submodules/Postbox/Package.swift +++ b/submodules/Postbox/Package.swift @@ -20,6 +20,7 @@ let package = Package( .package(name: "sqlcipher", path: "../sqlcipher"), .package(name: "StringTransliteration", path: "../StringTransliteration"), .package(name: "ManagedFile", path: "../ManagedFile"), + .package(name: "RangeSet", path: "../Utils/RangeSet"), .package(name: "SSignalKit", path: "../SSignalKit"), ], targets: [ @@ -30,6 +31,7 @@ let package = Package( dependencies: [.product(name: "MurMurHash32", package: "MurMurHash32", condition: nil), .product(name: "SwiftSignalKit", package: "SSignalKit", condition: nil), .product(name: "ManagedFile", package: "ManagedFile", condition: nil), + .product(name: "RangeSet", package: "RangeSet", condition: nil), .product(name: "sqlcipher", package: "sqlcipher", condition: nil), .product(name: "StringTransliteration", package: "StringTransliteration", condition: nil), .product(name: "Crc32", package: "Crc32", condition: nil)], diff --git a/submodules/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 3fe8dd5760..3bfca16d19 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -42,6 +42,9 @@ swift_library( "//submodules/Components/Forms/PrefixSectionGroupComponent:PrefixSectionGroupComponent", "//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/ConfettiEffect:ConfettiEffect", + "//submodules/TextFormat:TextFormat", + "//submodules/GZip:GZip", + "//submodules/InstantPageCache:InstantPageCache", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star new file mode 100644 index 0000000000..16390db8f1 Binary files /dev/null and b/submodules/PremiumUI/Resources/star differ diff --git a/submodules/PremiumUI/Resources/star.scn b/submodules/PremiumUI/Resources/star.scn deleted file mode 100644 index 1a06ef01a4..0000000000 Binary files a/submodules/PremiumUI/Resources/star.scn and /dev/null differ diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 7aa52a9771..e7cc10b0ef 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -12,320 +12,10 @@ import PrefixSectionGroupComponent import BundleIconComponent import SolidRoundedButtonComponent import Markdown -import SceneKit import InAppPurchaseManager import ConfettiEffect - -private func deg2rad(_ number: Float) -> Float { - return number * .pi / 180 -} - -private func rad2deg(_ number: Float) -> Float { - return number * 180.0 / .pi -} - -private class StarComponent: Component { - let isVisible: Bool - - init(isVisible: Bool) { - self.isVisible = isVisible - } - - static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool { - return lhs.isVisible == rhs.isVisible - } - - final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { - final class Tag { - } - - func matches(tag: Any) -> Bool { - if let _ = tag as? Tag { - return true - } - return false - } - - private var _ready = Promise() - var ready: Signal { - return self._ready.get() - } - - private let sceneView: SCNView - - private var previousInteractionTimestamp: Double = 0.0 - private var timer: SwiftSignalKit.Timer? - - override init(frame: CGRect) { - self.sceneView = SCNView(frame: frame) - self.sceneView.backgroundColor = .clear - self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) - self.sceneView.isUserInteractionEnabled = false - - super.init(frame: frame) - - self.addSubview(self.sceneView) - - self.setup() - - let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) - self.addGestureRecognizer(panGestureRecoginzer) - - let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) - self.addGestureRecognizer(tapGestureRecoginzer) - - self.disablesInteractiveModalDismiss = true - self.disablesInteractiveTransitionGestureRecognizer = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.timer?.invalidate() - } - - @objc private func handleTap(_ gesture: UITapGestureRecognizer) { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { - return - } - - self.previousInteractionTimestamp = CACurrentMediaTime() - - var left = true - if let view = gesture.view { - let point = gesture.location(in: view) - let distanceFromCenter = abs(point.x - view.frame.size.width / 2.0) - if distanceFromCenter > 60.0 { - return - } - if point.x > view.frame.width / 2.0 { - left = false - } - } - - if node.animationKeys.contains("tapRotate") { - self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true) - return - } - - let initial = node.rotation - let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6) - - let animation = CABasicAnimation(keyPath: "rotation") - animation.fromValue = NSValue(scnVector4: initial) - animation.toValue = NSValue(scnVector4: target) - animation.duration = 0.25 - animation.timingFunction = CAMediaTimingFunction(name: .easeOut) - animation.fillMode = .forwards - node.addAnimation(animation, forKey: "tapRotate") - - node.rotation = target - - Queue.mainQueue().after(0.25) { - node.rotation = initial - let springAnimation = CASpringAnimation(keyPath: "rotation") - springAnimation.fromValue = NSValue(scnVector4: target) - springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)) - springAnimation.mass = 1.0 - springAnimation.stiffness = 21.0 - springAnimation.damping = 5.8 - springAnimation.duration = springAnimation.settlingDuration * 0.8 - node.addAnimation(springAnimation, forKey: "tapRotate") - } - } - - private var previousAngle: Float = 0.0 - @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { - return - } - - self.previousInteractionTimestamp = CACurrentMediaTime() - - if #available(iOS 11.0, *) { - node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1) - node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1) - } else { - node.removeAllAnimations() - } - - switch gesture.state { - case .began: - self.previousAngle = 0.0 - case .changed: - let translation = gesture.translation(in: gesture.view) - let anglePan = deg2rad(Float(translation.x)) - - self.previousAngle = anglePan - node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: self.previousAngle) - case .ended: - let velocity = gesture.velocity(in: gesture.view) - - var smallAngle = false - if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 { - smallAngle = true - } - - self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) - node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0) - default: - break - } - } - - private func setup() { - guard let scene = SCNScene(named: "star.scn") else { - return - } - self.sceneView.scene = scene - self.sceneView.delegate = self - - let _ = self.sceneView.snapshot() - } - - private var didSetReady = false - func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { - if !self.didSetReady { - self.didSetReady = true - - self._ready.set(.single(true)) - self.onReady() - } - } - - private func onReady() { - self.setupGradientAnimation() - self.setupShineAnimation() - - self.playAppearanceAnimation(explode: true) - - 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 + 5.0 { - strongSelf.playAppearanceAnimation() - } - } - }, queue: Queue.mainQueue()) - self.timer?.start() - } - - private func setupGradientAnimation() { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { - return - } - guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { - return - } - - let animation = CABasicAnimation(keyPath: "contentsTransform") - animation.duration = 4.5 - animation.fromValue = NSValue(scnMatrix4: initial) - animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) - animation.timingFunction = CAMediaTimingFunction(name: .linear) - animation.autoreverses = true - animation.repeatCount = .infinity - - node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") - } - - private func setupShineAnimation() { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { - return - } - guard let initial = node.geometry?.materials.first?.emission.contentsTransform else { - return - } - - let animation = CABasicAnimation(keyPath: "contentsTransform") - animation.fillMode = .forwards - animation.fromValue = NSValue(scnMatrix4: initial) - animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0)) - animation.timingFunction = CAMediaTimingFunction(name: .easeOut) - animation.beginTime = 0.6 - animation.duration = 0.9 - - let group = CAAnimationGroup() - group.animations = [animation] - group.beginTime = 1.0 - group.duration = 3.0 - group.repeatCount = .infinity - - node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") - } - - private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { - guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { - return - } - - self.previousInteractionTimestamp = CACurrentMediaTime() - - if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { - let particleSystem = particles.particleSystems?.first - particleSystem?.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3) - particleSystem?.particleVelocity = 2.2 - particleSystem?.birthRate = 4.5 - particleSystem?.particleLifeSpan = 2.0 - - node.physicsField?.isActive = true - Queue.mainQueue().after(1.0) { - node.physicsField?.isActive = false - particles.particleSystems?.first?.birthRate = 1.2 - particleSystem?.particleVelocity = 1.0 - particleSystem?.particleLifeSpan = 4.0 - } - } - - let from = node.presentation.rotation - node.removeAnimation(forKey: "tapRotate") - - var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 - if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { - toValue *= -1 - } - if mirror { - toValue *= -1 - } - let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: toValue) - let distance = rad2deg(to.w - from.w) - - guard !distance.isZero else { - return - } - - let springAnimation = CASpringAnimation(keyPath: "rotation") - springAnimation.fromValue = NSValue(scnVector4: from) - springAnimation.toValue = NSValue(scnVector4: to) - springAnimation.mass = 1.0 - springAnimation.stiffness = 21.0 - springAnimation.damping = 5.8 - springAnimation.duration = springAnimation.settlingDuration * 0.75 - springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 - - node.addAnimation(springAnimation, forKey: "rotate") - } - - func update(component: StarComponent, 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) - - return availableSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, transition: transition) - } -} - +import TextFormat +import InstantPageCache private final class SectionGroupComponent: Component { public final class Item: Equatable { @@ -813,6 +503,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let infoBackground = Child(RoundedRectangle.self) let infoTitle = Child(MultilineTextComponent.self) let infoText = Child(MultilineTextComponent.self) + let termsText = Child(MultilineTextComponent.self) return { context in let sideInset: CGFloat = 16.0 @@ -954,6 +645,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } ), + SectionGroupComponent.Item( + AnyComponentWithIdentity( + id: "voice", + component: AnyComponent( + PerkComponent( + iconName: "Premium/Perk/Voice", + iconBackgroundColors: [ + UIColor(rgb: 0xDE4768), + UIColor(rgb: 0xD54D82) + ], + title: strings.Premium_VoiceToText, + titleColor: titleColor, + subtitle: strings.Premium_VoiceToTextInfo, + subtitleColor: subtitleColor, + arrowColor: arrowColor + ) + ) + ), + action: { + + } + ), SectionGroupComponent.Item( AnyComponentWithIdentity( id: "noAds", @@ -1020,6 +733,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { } ), + SectionGroupComponent.Item( + AnyComponentWithIdentity( + id: "chat", + component: AnyComponent( + PerkComponent( + iconName: "Premium/Perk/Chat", + iconBackgroundColors: [ + UIColor(rgb: 0x9674FF), + UIColor(rgb: 0x8C7DFF) + ], + title: strings.Premium_ChatManagement, + titleColor: titleColor, + subtitle: strings.Premium_ChatManagementInfo, + subtitleColor: subtitleColor, + arrowColor: arrowColor + ) + ) + ), + action: { + + } + ), SectionGroupComponent.Item( AnyComponentWithIdentity( id: "badge", @@ -1134,7 +869,65 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + infoText.size.width / 2.0, y: size.height + textPadding + infoText.size.height / 2.0)) ) size.height += infoBackground.size.height - size.height += 3.0 + size.height += 6.0 + + let termsFont = Font.regular(13.0) + let termsTextColor = environment.theme.list.freeTextColor + let termsMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), bold: MarkdownAttributeSet(font: termsFont, textColor: termsTextColor), link: MarkdownAttributeSet(font: termsFont, textColor: environment.theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let termsText = termsText.update( + component: MultilineTextComponent( + text: .markdown( + text: strings.Premium_Terms, + attributes: termsMarkdownAttributes + ), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.0, + highlightColor: environment.theme.list.itemAccentColor.withAlphaComponent(0.3), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + 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) + }) + } + } + } + ), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets - textSideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(termsText + .position(CGPoint(x: sideInset + environment.safeInsets.left + textSideInset + termsText.size.width / 2.0, y: size.height + termsText.size.height / 2.0)) + ) + size.height += termsText.size.height + size.height += 10.0 size.height += scrollEnvironment.insets.bottom return size @@ -1286,7 +1079,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { static var body: Body { let background = Child(Rectangle.self) let scrollContent = Child(ScrollComponent.self) - let star = Child(StarComponent.self) + let star = Child(PremiumStarComponent.self) let topPanel = Child(BlurredRectangle.self) let topSeparator = Child(Rectangle.self) let title = Child(Text.self) @@ -1306,7 +1099,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } let star = star.update( - component: StarComponent(isVisible: starIsVisible), + component: PremiumStarComponent(isVisible: starIsVisible), availableSize: CGSize(width: min(390.0, context.availableSize.width), height: 220.0), transition: context.transition ) @@ -1462,7 +1255,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { .opacity(bottomPanelAlpha) ) context.add(bottomSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height - bottomSeparator.size.height)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - bottomPanel.size.height)) .opacity(bottomPanelAlpha) ) context.add(button @@ -1475,7 +1268,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { } public final class PremiumIntroScreen: ViewControllerComponentContainer { - private let context: AccountContext + fileprivate let context: AccountContext private var didSetReady = false private let _ready = Promise() @@ -1538,7 +1331,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { super.containerLayoutUpdated(layout, transition: transition) if !self.didSetReady { - if let view = self.node.hostView.findTaggedView(tag: StarComponent.View.Tag()) as? StarComponent.View { + if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { self.didSetReady = true self._ready.set(view.ready) } diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 81030a3482..93aacedcd6 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -22,19 +22,22 @@ private class PremiumLimitAnimationComponent: Component { private let activeColors: [UIColor] private let textColor: UIColor private let badgeText: String? + private let badgePosition: CGFloat init( iconName: String, inactiveColor: UIColor, activeColors: [UIColor], textColor: UIColor, - badgeText: String? + badgeText: String?, + badgePosition: CGFloat ) { self.iconName = iconName self.inactiveColor = inactiveColor self.activeColors = activeColors self.textColor = textColor self.badgeText = badgeText + self.badgePosition = badgePosition } static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool { @@ -53,6 +56,9 @@ private class PremiumLimitAnimationComponent: Component { if lhs.badgeText != rhs.badgeText { return false } + if lhs.badgePosition != rhs.badgePosition { + return false + } return true } @@ -144,9 +150,8 @@ private class PremiumLimitAnimationComponent: Component { let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil) let positionAnimation = CABasicAnimation(keyPath: "position.x") - positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0)) - positionAnimation.toValue = NSValue(cgPoint: CGPoint()) - positionAnimation.isAdditive = true + positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: 0.0, y: 0.0)) + positionAnimation.toValue = NSValue(cgPoint: self.badgeView.center) positionAnimation.duration = 0.5 positionAnimation.fillMode = .forwards positionAnimation.beginTime = now @@ -225,7 +230,7 @@ private class PremiumLimitAnimationComponent: Component { self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0)) self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize) - self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0) + self.badgeView.center = CGPoint(x: availableSize.width * component.badgePosition, y: 82.0) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0) @@ -318,33 +323,39 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { public let inactiveColor: UIColor public let activeColors: [UIColor] public let inactiveTitle: String + public let inactiveValue: String public let inactiveTitleColor: UIColor public let activeTitle: String public let activeValue: String public let activeTitleColor: UIColor public let badgeIconName: String public let badgeText: String? + public let badgePosition: CGFloat public init( inactiveColor: UIColor, activeColors: [UIColor], inactiveTitle: String, + inactiveValue: String, inactiveTitleColor: UIColor, activeTitle: String, activeValue: String, activeTitleColor: UIColor, badgeIconName: String, - badgeText: String? + badgeText: String?, + badgePosition: CGFloat ) { self.inactiveColor = inactiveColor self.activeColors = activeColors self.inactiveTitle = inactiveTitle + self.inactiveValue = inactiveValue self.inactiveTitleColor = inactiveTitleColor self.activeTitle = activeTitle self.activeValue = activeValue self.activeTitleColor = activeTitleColor self.badgeIconName = badgeIconName self.badgeText = badgeText + self.badgePosition = badgePosition } public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool { @@ -357,6 +368,9 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { if lhs.inactiveTitle != rhs.inactiveTitle { return false } + if lhs.inactiveValue != rhs.inactiveValue { + return false + } if lhs.inactiveTitleColor != rhs.inactiveTitleColor { return false } @@ -375,11 +389,15 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { if lhs.badgeText != rhs.badgeText { return false } + if lhs.badgePosition != rhs.badgePosition { + return false + } return true } public static var body: Body { let inactiveTitle = Child(MultilineTextComponent.self) + let inactiveValue = Child(MultilineTextComponent.self) let activeTitle = Child(MultilineTextComponent.self) let activeValue = Child(MultilineTextComponent.self) let animation = Child(PremiumLimitAnimationComponent.self) @@ -404,6 +422,20 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { transition: context.transition ) + let inactiveValue = inactiveValue.update( + component: MultilineTextComponent( + text: .plain( + NSAttributedString( + string: component.inactiveValue, + font: Font.semibold(15.0), + textColor: component.inactiveTitleColor + ) + ) + ), + availableSize: context.availableSize, + transition: context.transition + ) + let activeTitle = activeTitle.update( component: MultilineTextComponent( text: .plain( @@ -438,7 +470,8 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { inactiveColor: component.inactiveColor, activeColors: component.activeColors, textColor: component.activeTitleColor, - badgeText: component.badgeText + badgeText: component.badgeText, + badgePosition: component.badgePosition ), availableSize: CGSize(width: context.availableSize.width, height: height), transition: context.transition @@ -452,8 +485,12 @@ public final class PremiumLimitDisplayComponent: CombinedComponent { .position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) ) + context.add(inactiveValue + .position(CGPoint(x: context.availableSize.width / 2.0 - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0)) + ) + context.add(activeTitle - .position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) ) context.add(activeValue @@ -470,12 +507,14 @@ private final class LimitSheetContent: CombinedComponent { let context: AccountContext let subject: PremiumLimitScreen.Subject + let count: Int32 let action: () -> Void let dismiss: () -> Void - init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void, dismiss: @escaping () -> Void) { + init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void, dismiss: @escaping () -> Void) { self.context = context self.subject = subject + self.count = count self.action = action self.dismiss = dismiss } @@ -487,6 +526,9 @@ private final class LimitSheetContent: CombinedComponent { if lhs.subject != rhs.subject { return false } + if lhs.count != rhs.count { + return false + } return true } @@ -549,36 +591,46 @@ private final class LimitSheetContent: CombinedComponent { let iconName: String let badgeText: String let string: String + let defaultValue: String let premiumValue: String + let badgePosition: CGFloat switch subject { case .folders: let limit = state.limits.maxFoldersCount let premiumLimit = state.premiumLimits.maxFoldersCount iconName = "Premium/Folder" - badgeText = "\(limit)" + badgeText = "\(component.count)" string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .chatsInFolder: let limit = state.limits.maxFolderChatsCount let premiumLimit = state.premiumLimits.maxFolderChatsCount iconName = "Premium/Chat" - badgeText = "\(limit)" + badgeText = "\(component.count)" string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .pins: let limit = state.limits.maxPinnedChatCount let premiumLimit = state.premiumLimits.maxPinnedChatCount iconName = "Premium/Pin" - badgeText = "\(limit)" + badgeText = "\(component.count)" string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string + defaultValue = component.count > limit ? "\(limit)" : "" premiumValue = "\(premiumLimit)" + badgePosition = CGFloat(component.count) / CGFloat(premiumLimit) case .files: let limit: Int64 = 2048 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts) let premiumLimit: Int64 = 4096 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts) 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 = CGFloat(component.count) / CGFloat(premiumLimit) } let title = title.update( @@ -625,12 +677,14 @@ private final class LimitSheetContent: CombinedComponent { UIColor(rgb: 0xe46ace) ], inactiveTitle: strings.Premium_Free, + inactiveValue: defaultValue, inactiveTitleColor: .black, activeTitle: strings.Premium_Premium, activeValue: premiumValue, activeTitleColor: .white, badgeIconName: iconName, - badgeText: badgeText + badgeText: badgeText, + badgePosition: badgePosition ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: .immediate @@ -696,11 +750,13 @@ private final class LimitSheetComponent: CombinedComponent { let context: AccountContext let subject: PremiumLimitScreen.Subject + let count: Int32 let action: () -> Void - init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) { + init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) { self.context = context self.subject = subject + self.count = count self.action = action } @@ -729,6 +785,7 @@ private final class LimitSheetComponent: CombinedComponent { content: AnyComponent(LimitSheetContent( context: context.component.context, subject: context.component.subject, + count: context.component.count, action: context.component.action, dismiss: { animateOut.invoke(Action { _ in @@ -775,8 +832,8 @@ public class PremiumLimitScreen: ViewControllerComponentContainer { case files } - public init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) { - super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, action: action), navigationBarAppearance: .none) + public init(context: AccountContext, subject: PremiumLimitScreen.Subject, count: Int32, action: @escaping () -> Void) { + super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, count: count, action: action), navigationBarAppearance: .none) self.navigationPresentation = .flatModal } diff --git a/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift b/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift index 863e08a51d..6fc1794441 100644 --- a/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumReactionsScreen.swift @@ -11,7 +11,7 @@ import PresentationDataUtils import SolidRoundedButtonNode import AppBundle -public final class PremiumStickersScreen: ViewController { +public final class PremiumReactionsScreen: ViewController { private let context: AccountContext private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -21,7 +21,7 @@ public final class PremiumStickersScreen: ViewController { public var proceed: (() -> Void)? private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { - private weak var controller: PremiumStickersScreen? + private weak var controller: PremiumReactionsScreen? private var presentationData: PresentationData private let blurView: UIVisualEffectView @@ -38,7 +38,7 @@ public final class PremiumStickersScreen: ViewController { private var validLayout: ContainerViewLayout? - init(controller: PremiumStickersScreen) { + init(controller: PremiumReactionsScreen) { self.controller = controller self.presentationData = controller.presentationData @@ -70,7 +70,14 @@ public final class PremiumStickersScreen: ViewController { self.overlayTextNode.maximumNumberOfLines = 0 self.overlayTextNode.lineSpacing = 0.1 - self.proceedButton = SolidRoundedButtonNode(title: self.presentationData.strings.Premium_Reactions_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 11.0, gloss: true) + self.proceedButton = SolidRoundedButtonNode(title: self.presentationData.strings.Premium_Reactions_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme( + backgroundColor: .white, + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) self.cancelButton = HighlightableButtonNode() self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) @@ -78,7 +85,7 @@ public final class PremiumStickersScreen: ViewController { self.carouselNode = ReactionCarouselNode(context: controller.context, theme: controller.presentationData.theme, reactions: controller.reactions) super.init() - + self.addSubnode(self.dimNode) self.addSubnode(self.darkDimNode) self.addSubnode(self.containerNode) @@ -99,7 +106,10 @@ public final class PremiumStickersScreen: ViewController { self.overlayTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.Premium_Reactions_Description, font: Font.regular(17.0), textColor: textColor) self.proceedButton.pressed = { [weak self] in - self?.animateOut() + if let strongSelf = self, let controller = strongSelf.controller, let navigationController = controller.navigationController { + strongSelf.animateOut() + navigationController.pushViewController(PremiumIntroScreen(context: controller.context), animated: true) + } } self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) @@ -209,6 +219,8 @@ public final class PremiumStickersScreen: ViewController { super.init(navigationBarPresentationData: nil) + self.navigationPresentation = .flatModal + self.statusBar.statusBarStyle = .Ignore self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) diff --git a/submodules/PremiumUI/Sources/PremiumStarComponent.swift b/submodules/PremiumUI/Sources/PremiumStarComponent.swift new file mode 100644 index 0000000000..ef55e2e26c --- /dev/null +++ b/submodules/PremiumUI/Sources/PremiumStarComponent.swift @@ -0,0 +1,373 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import SceneKit +import GZip +import AppBundle + +private let sceneVersion: Int = 1 + +private func deg2rad(_ number: Float) -> Float { + return number * .pi / 180 +} + +private func rad2deg(_ number: Float) -> Float { + return number * 180.0 / .pi +} + +private func generateParticlesTexture() -> UIImage { + return UIImage() +} + +private func generateFlecksTexture() -> UIImage { + return UIImage() +} + +private func generateShineTexture() -> UIImage { + return UIImage() +} + +private func generateDiffuseTexture() -> UIImage { + return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in + let colorsArray: [CGColor] = [ + UIColor(rgb: 0x0079ff).cgColor, + UIColor(rgb: 0x6a93ff).cgColor, + UIColor(rgb: 0x9172fe).cgColor, + UIColor(rgb: 0xe46acd).cgColor, + ] + var locations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + })! +} + +class PremiumStarComponent: Component { + let isVisible: Bool + + init(isVisible: Bool) { + self.isVisible = isVisible + } + + static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool { + return lhs.isVisible == rhs.isVisible + } + + final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { + final class Tag { + } + + func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private var _ready = Promise() + var ready: Signal { + return self._ready.get() + } + + private let sceneView: SCNView + + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + + override init(frame: CGRect) { + self.sceneView = SCNView(frame: frame) + self.sceneView.backgroundColor = .clear + self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5) + self.sceneView.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.addSubview(self.sceneView) + + self.setup() + + let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) + self.addGestureRecognizer(panGestureRecoginzer) + + let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) + self.addGestureRecognizer(tapGestureRecoginzer) + + self.disablesInteractiveModalDismiss = true + self.disablesInteractiveTransitionGestureRecognizer = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.timer?.invalidate() + } + + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + self.previousInteractionTimestamp = CACurrentMediaTime() + + var left: Bool? + var top: Bool? + if let view = gesture.view { + let point = gesture.location(in: view) + let horizontalDistanceFromCenter = abs(point.x - view.frame.size.width / 2.0) + if horizontalDistanceFromCenter > 60.0 { + return + } + let verticalDistanceFromCenter = abs(point.y - view.frame.size.height / 2.0) + if horizontalDistanceFromCenter > 20.0 { + left = point.x < view.frame.width / 2.0 + } + if verticalDistanceFromCenter > 20.0 { + top = point.y < view.frame.height / 2.0 + } + } + + if node.animationKeys.contains("tapRotate"), let left = left { + self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true) + return + } + + let initial = node.eulerAngles + var yaw: CGFloat = 0.0 + var pitch: CGFloat = 0.0 + if let left = left { + yaw = left ? -0.6 : 0.6 + } + if let top = top { + pitch = top ? -0.3 : 0.3 + } + let target = SCNVector3(pitch, yaw, 0.0) + + let animation = CABasicAnimation(keyPath: "eulerAngles") + animation.fromValue = NSValue(scnVector3: initial) + animation.toValue = NSValue(scnVector3: target) + animation.duration = 0.25 + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.fillMode = .forwards + node.addAnimation(animation, forKey: "tapRotate") + + node.eulerAngles = target + + Queue.mainQueue().after(0.25) { + node.eulerAngles = initial + let springAnimation = CASpringAnimation(keyPath: "eulerAngles") + springAnimation.fromValue = NSValue(scnVector3: target) + springAnimation.toValue = NSValue(scnVector3: SCNVector3(x: 0.0, y: 0.0, z: 0.0)) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.8 + node.addAnimation(springAnimation, forKey: "tapRotate") + } + } + + private var previousYaw: Float = 0.0 + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + self.previousInteractionTimestamp = CACurrentMediaTime() + + if #available(iOS 11.0, *) { + node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1) + node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1) + } else { + node.removeAllAnimations() + } + + switch gesture.state { + case .began: + self.previousYaw = 0.0 + case .changed: + let translation = gesture.translation(in: gesture.view) + let yawPan = deg2rad(Float(translation.x)) + let pitchPan = deg2rad(Float(translation.y)) + + self.previousYaw = yawPan + node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0) + case .ended: + let velocity = gesture.velocity(in: gesture.view) + + var smallAngle = false + if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 { + smallAngle = true + } + + self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600) + node.eulerAngles = SCNVector3(0.0, 0.0, 0.0) + default: + break + } + } + + private func setup() { + guard let url = getAppBundle().url(forResource: "star", withExtension: ""), + let compressedData = try? Data(contentsOf: url), + let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else { + return + } + let fileName = "star_\(sceneVersion).scn" + let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory() + fileName) + if !FileManager.default.fileExists(atPath: tmpURL.path) { + try? decompressedData.write(to: tmpURL) + } + + guard let scene = try? SCNScene(url: tmpURL, options: nil) else { + return + } + + self.sceneView.scene = scene + self.sceneView.delegate = self + + let _ = self.sceneView.snapshot() + } + + private var didSetReady = false + func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) { + if !self.didSetReady { + self.didSetReady = true + + self._ready.set(.single(true)) + self.onReady() + } + } + + private func onReady() { + self.setupGradientAnimation() + self.setupShineAnimation() + + self.playAppearanceAnimation(explode: true) + + 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 + 5.0 { + strongSelf.playAppearanceAnimation() + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } + + private func setupGradientAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.duration = 4.5 + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0)) + animation.timingFunction = CAMediaTimingFunction(name: .linear) + animation.autoreverses = true + animation.repeatCount = .infinity + + node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient") + } + + private func setupShineAnimation() { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + guard let initial = node.geometry?.materials.first?.emission.contentsTransform else { + return + } + + let animation = CABasicAnimation(keyPath: "contentsTransform") + animation.fillMode = .forwards + animation.fromValue = NSValue(scnMatrix4: initial) + animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0)) + animation.timingFunction = CAMediaTimingFunction(name: .easeOut) + animation.beginTime = 0.6 + animation.duration = 0.9 + + let group = CAAnimationGroup() + group.animations = [animation] + group.beginTime = 1.0 + group.duration = 3.0 + group.repeatCount = .infinity + + node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer") + } + + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { + guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else { + return + } + + self.previousInteractionTimestamp = CACurrentMediaTime() + + if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) { + let particleSystem = particles.particleSystems?.first + particleSystem?.particleColorVariation = SCNVector4(0.15, 0.2, 0.35, 0.3) + particleSystem?.particleVelocity = 2.2 + particleSystem?.birthRate = 4.5 + particleSystem?.particleLifeSpan = 2.0 + + node.physicsField?.isActive = true + Queue.mainQueue().after(1.0) { + node.physicsField?.isActive = false + particles.particleSystems?.first?.birthRate = 1.2 + particleSystem?.particleVelocity = 1.0 + particleSystem?.particleLifeSpan = 4.0 + } + } + + let from = node.presentation.eulerAngles + node.removeAnimation(forKey: "tapRotate") + + var toValue: Float = smallAngle ? 0.0 : .pi * 2.0 + if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 { + toValue *= -1 + } + if mirror { + toValue *= -1 + } + let to = SCNVector3(x: 0.0, y: toValue, z: 0.0) + let distance = rad2deg(to.y - from.y) + + guard !distance.isZero else { + return + } + + let springAnimation = CASpringAnimation(keyPath: "eulerAngles") + springAnimation.fromValue = NSValue(scnVector3: from) + springAnimation.toValue = NSValue(scnVector3: to) + springAnimation.mass = 1.0 + springAnimation.stiffness = 21.0 + springAnimation.damping = 5.8 + springAnimation.duration = springAnimation.settlingDuration * 0.75 + springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7 + + node.addAnimation(springAnimation, forKey: "rotate") + } + + 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) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + 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/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index cd108a673f..93671bb8e0 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -7,69 +7,6 @@ import InstantPageUI import InstantPageCache import UrlHandling -private func extractAnchor(string: String) -> (String, String?) { - var anchorValue: String? - if let anchorRange = string.range(of: "#") { - let anchor = string[anchorRange.upperBound...] - if !anchor.isEmpty { - anchorValue = String(anchor) - } - } - var trimmedUrl = string - if let anchor = anchorValue, let anchorRange = string.range(of: "#\(anchor)") { - let url = string[.. Signal { - var faqUrl = context.sharedContext.currentPresentationData.with { $0 }.strings.Settings_FAQ_URL - if faqUrl == "Settings.FAQ_URL" || faqUrl.isEmpty { - faqUrl = "https://telegram.org/faq#general-questions" - } - - let (cachedUrl, anchor) = extractAnchor(string: faqUrl) - - return cachedInstantPage(postbox: context.account.postbox, url: cachedUrl) - |> mapToSignal { cachedInstantPage -> Signal in - let updated = resolveInstantViewUrl(account: context.account, url: faqUrl) - |> afterNext { result in - if case let .instantView(webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage { - if instantPage.isComplete { - let _ = updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage).start() - } else { - let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage) - |> mapToSignal { webPage -> Signal in - if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { - return updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage) - } else { - return .complete() - } - }).start() - } - } - } - - let now = Int32(CFAbsoluteTimeGetCurrent()) - if let cachedInstantPage = cachedInstantPage, case let .Loaded(content) = cachedInstantPage.webPage.content, let instantPage = content.instantPage, instantPage.isComplete { - let current: Signal = .single(.instantView(cachedInstantPage.webPage, anchor)) - if now > cachedInstantPage.timestamp + refreshTimeout { - return current - |> then(updated) - } else { - return current - } - } else { - return updated - } - } -} - func faqSearchableItems(context: AccountContext, resolvedUrl: Signal, suggestAccountDeletion: Bool) -> Signal<[SettingsSearchableItem], NoError> { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings return resolvedUrl diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index 6d91abfc47..ebff87f703 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -236,7 +236,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol } }))) } - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index fa46e2a395..dd860b9ee5 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -369,7 +369,9 @@ private final class StickerPackContainer: ASDisplayNode { } }))) } - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { + + })) } else { return nil } @@ -1319,6 +1321,7 @@ public enum StickerPackScreenPerformedAction { } public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController { + let stickerPacks = [mainStickerPack] let controller = StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: actionPerformed) controller.dismissed = dismissed return controller diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 99c1c30edd..022e2a5bd6 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -35,8 +35,9 @@ public final class StickerPreviewPeekContent: PeekControllerContent { public let item: StickerPreviewPeekItem let isLocked: Bool let menu: [ContextMenuItem] + let openPremiumIntro: () -> Void - public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, menu: [ContextMenuItem]) { + public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, item: StickerPreviewPeekItem, isLocked: Bool = false, menu: [ContextMenuItem], openPremiumIntro: @escaping () -> Void) { self.account = account self.theme = theme self.strings = strings @@ -47,6 +48,7 @@ public final class StickerPreviewPeekContent: PeekControllerContent { } else { self.menu = menu } + self.openPremiumIntro = openPremiumIntro } public func presentation() -> PeekControllerContentPresentation { @@ -71,7 +73,7 @@ public final class StickerPreviewPeekContent: PeekControllerContent { public func fullScreenAccessoryNode(blurView: UIVisualEffectView) -> (PeekControllerAccessoryNode & ASDisplayNode)? { if self.isLocked { - return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings) + return PremiumStickerPackAccessoryNode(theme: self.theme, strings: self.strings, proceed: self.openPremiumIntro) } else { return nil } @@ -211,12 +213,17 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC } } -final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccessoryNode { +final class PremiumStickerPackAccessoryNode: SparseNode, PeekControllerAccessoryNode { + var dismiss: () -> Void = {} + let proceed: () -> Void + let textNode: ImmediateTextNode let proceedButton: SolidRoundedButtonNode let cancelButton: HighlightableButtonNode - init(theme: PresentationTheme, strings: PresentationStrings) { + init(theme: PresentationTheme, strings: PresentationStrings, proceed: @escaping () -> Void) { + self.proceed = proceed + self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.textAlignment = .center @@ -224,7 +231,14 @@ final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccess self.textNode.attributedText = NSAttributedString(string: strings.Premium_Stickers_Description, font: Font.regular(17.0), textColor: theme.actionSheet.secondaryTextColor) self.textNode.lineSpacing = 0.1 - self.proceedButton = SolidRoundedButtonNode(title: strings.Premium_Stickers_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme(theme: theme), height: 50.0, cornerRadius: 11.0, gloss: true) + self.proceedButton = SolidRoundedButtonNode(title: strings.Premium_Stickers_Proceed, icon: UIImage(bundleImageName: "Premium/ButtonIcon"), theme: SolidRoundedButtonTheme( + backgroundColor: .white, + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], foregroundColor: .white), height: 50.0, cornerRadius: 11.0, gloss: true) self.cancelButton = HighlightableButtonNode() self.cancelButton.setTitle(strings.Common_Cancel, with: Font.regular(17.0), with: theme.list.itemAccentColor, for: .normal) @@ -235,14 +249,14 @@ final class PremiumStickerPackAccessoryNode: ASDisplayNode, PeekControllerAccess self.addSubnode(self.proceedButton) self.addSubnode(self.cancelButton) - self.proceedButton.pressed = { - + self.proceedButton.pressed = { [weak self] in + self?.proceed() } self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) } @objc func cancelPressed() { - + self.dismiss() } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift index ba9d1ff642..b8065ede7f 100644 --- a/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift +++ b/submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift @@ -126,7 +126,7 @@ public func downloadAppUpdate(account: Account, source: String, messageId: Int32 case let .Fetching(_, progress): if let size = media.size { if progress == 0 { - subscriber.putNext(.started(size)) + subscriber.putNext(.started(Int(size))) } else { subscriber.putNext(.progress(Int(progress * Float(size)), Int(size))) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index 9f456df988..230ad8add6 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -493,6 +493,19 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + public var premiumEffect: TelegramMediaFile.VideoThumbnail? { + if let effect = self.videoThumbnails.first(where: { thumbnail in + if let resource = thumbnail.resource as? CloudDocumentSizeMediaResource, resource.sizeSpec == "f" { + return true + } else { + return false + } + }) { + return effect + } + return nil + } + public var isVideoSticker: Bool { if self.mimeType == "video/webm" { var hasSticker = false diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 16bfdc4530..cb3eb551ef 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -231,66 +231,78 @@ public struct ChatListFilterData: Equatable, Hashable { } } -public struct ChatListFilter: Codable, Equatable { - public var id: Int32 - public var title: String - public var emoticon: String? - public var data: ChatListFilterData +public enum ChatListFilter: Codable, Equatable { + case allChats + case filter(id: Int32, title: String, emoticon: String?, data: ChatListFilterData) - public init( - id: Int32, - title: String, - emoticon: String?, - data: ChatListFilterData - ) { - self.id = id - self.title = title - self.emoticon = emoticon - self.data = data + public var id: Int32 { + switch self { + case .allChats: + return 0 + case let .filter(id, _, _, _): + return id + } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) - - self.id = try container.decode(Int32.self, forKey: "id") - self.title = try container.decode(String.self, forKey: "title") - self.emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon") - - self.data = ChatListFilterData( - categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")), - excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0, - excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0, - excludeArchived: (try container.decode(Int32.self, forKey: "excludeArchived")) != 0, - includePeers: ChatListFilterIncludePeers( - peers: (try container.decode([Int64].self, forKey: "includePeers")).map(PeerId.init), - pinnedPeers: (try container.decode([Int64].self, forKey: "pinnedPeers")).map(PeerId.init) - ), - excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init) - ) + + let type = try container.decodeIfPresent(Int32.self, forKey: "t") ?? 1 + if type == 0 { + self = .allChats + } else { + let id = try container.decode(Int32.self, forKey: "id") + let title = try container.decode(String.self, forKey: "title") + let emoticon = try container.decodeIfPresent(String.self, forKey: "emoticon") + + let data = ChatListFilterData( + categories: ChatListFilterPeerCategories(rawValue: try container.decode(Int32.self, forKey: "categories")), + excludeMuted: (try container.decode(Int32.self, forKey: "excludeMuted")) != 0, + excludeRead: (try container.decode(Int32.self, forKey: "excludeRead")) != 0, + excludeArchived: (try container.decode(Int32.self, forKey: "excludeArchived")) != 0, + includePeers: ChatListFilterIncludePeers( + peers: (try container.decode([Int64].self, forKey: "includePeers")).map(PeerId.init), + pinnedPeers: (try container.decode([Int64].self, forKey: "pinnedPeers")).map(PeerId.init) + ), + excludePeers: (try container.decode([Int64].self, forKey: "excludePeers")).map(PeerId.init) + ) + self = .filter(id: id, title: title, emoticon: emoticon, data: data) + } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: StringCodingKey.self) - try container.encode(self.id, forKey: "id") - try container.encode(self.title, forKey: "title") - try container.encodeIfPresent(self.emoticon, forKey: "emoticon") - - try container.encode(self.data.categories.rawValue, forKey: "categories") - try container.encode((self.data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted") - try container.encode((self.data.excludeRead ? 1 : 0) as Int32, forKey: "excludeRead") - try container.encode((self.data.excludeArchived ? 1 : 0) as Int32, forKey: "excludeArchived") - try container.encode(self.data.includePeers.peers.map { $0.toInt64() }, forKey: "includePeers") - try container.encode(self.data.includePeers.pinnedPeers.map { $0.toInt64() }, forKey: "pinnedPeers") - try container.encode(self.data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers") + switch self { + case .allChats: + let type: Int32 = 0 + try container.encode(type, forKey: "t") + case let .filter(id, title, emoticon, data): + let type: Int32 = 1 + try container.encode(type, forKey: "t") + + try container.encode(id, forKey: "id") + try container.encode(title, forKey: "title") + try container.encodeIfPresent(emoticon, forKey: "emoticon") + + try container.encode(data.categories.rawValue, forKey: "categories") + try container.encode((data.excludeMuted ? 1 : 0) as Int32, forKey: "excludeMuted") + try container.encode((data.excludeRead ? 1 : 0) as Int32, forKey: "excludeRead") + try container.encode((data.excludeArchived ? 1 : 0) as Int32, forKey: "excludeArchived") + try container.encode(data.includePeers.peers.map { $0.toInt64() }, forKey: "includePeers") + try container.encode(data.includePeers.pinnedPeers.map { $0.toInt64() }, forKey: "pinnedPeers") + try container.encode(data.excludePeers.map { $0.toInt64() }, forKey: "excludePeers") + } } } extension ChatListFilter { init(apiFilter: Api.DialogFilter) { switch apiFilter { + case .dialogFilterDefault: + self = .allChats case let .dialogFilter(flags, id, title, emoticon, pinnedPeers, includePeers, excludePeers): - self.init( + self = .filter( id: id, title: title, emoticon: emoticon, @@ -341,31 +353,36 @@ extension ChatListFilter { } } - func apiFilter(transaction: Transaction) -> Api.DialogFilter { - var flags: Int32 = 0 - if self.data.excludeMuted { - flags |= 1 << 11 - } - if self.data.excludeRead { - flags |= 1 << 12 - } - if self.data.excludeArchived { - flags |= 1 << 13 - } - flags |= self.data.categories.apiFlags - if self.emoticon != nil { - flags |= 1 << 25 - } - return .dialogFilter(flags: flags, id: self.id, title: self.title, emoticon: self.emoticon, pinnedPeers: self.data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }, includePeers: self.data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in - if self.data.includePeers.pinnedPeers.contains(peerId) { + func apiFilter(transaction: Transaction) -> Api.DialogFilter? { + switch self { + case .allChats: return nil - } - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }, excludePeers: self.data.excludePeers.compactMap { peerId -> Api.InputPeer? in - return transaction.getPeer(peerId).flatMap(apiInputPeer) - }) + case let .filter(id, title, emoticon, data): + var flags: Int32 = 0 + if data.excludeMuted { + flags |= 1 << 11 + } + if data.excludeRead { + flags |= 1 << 12 + } + if data.excludeArchived { + flags |= 1 << 13 + } + flags |= data.categories.apiFlags + if emoticon != nil { + flags |= 1 << 25 + } + return .dialogFilter(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in + if data.includePeers.pinnedPeers.contains(peerId) { + return nil + } + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }, excludePeers: data.excludePeers.compactMap { peerId -> Api.InputPeer? in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + }) + } } } @@ -427,6 +444,8 @@ private func requestChatListFilters(accountPeerId: PeerId, postbox: Postbox, net let filter = ChatListFilter(apiFilter: apiFilter) filters.append(filter) switch apiFilter { + case .dialogFilterDefault: + break case let .dialogFilter(_, _, _, _, pinnedPeers, includePeers, excludePeers): for peer in pinnedPeers + includePeers + excludePeers { var peerId: PeerId? @@ -982,11 +1001,15 @@ func _internal_updateChatListFeaturedFilters(postbox: Postbox, network: Network) return postbox.transaction { transaction -> Void in transaction.updatePreferencesEntry(key: PreferencesKeys.chatListFiltersFeaturedState, { entry in var state = entry?.get(ChatListFiltersFeaturedState.self) ?? ChatListFiltersFeaturedState(filters: [], isSeen: false) - state.filters = result.map { item -> ChatListFeaturedFilter in + state.filters = result.compactMap { item -> ChatListFeaturedFilter? in switch item { case let .dialogFilterSuggested(filter, description): let parsedFilter = ChatListFilter(apiFilter: filter) - return ChatListFeaturedFilter(title: parsedFilter.title, description: description, data: parsedFilter.data) + if case let .filter(_, title, _, data) = parsedFilter { + return ChatListFeaturedFilter(title: title, description: description, data: data) + } else { + return nil + } } } return PreferencesEntry(state) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift index 76a96034af..cbaeba1c0c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RemovePeerChat.swift @@ -28,16 +28,23 @@ func _internal_removePeerChat(account: Account, transaction: Transaction, mediaB transaction.setPeerChatInterfaceState(peerId, state: nil) } _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in - var filters = filters + var updatedFilters: [ChatListFilter] = [] for i in 0 ..< filters.count { - if filters[i].data.includePeers.peers.contains(peerId) { - filters[i].data.includePeers.setPeers(filters[i].data.includePeers.peers.filter { $0 != peerId }) - } - if filters[i].data.excludePeers.contains(peerId) { - filters[i].data.excludePeers = filters[i].data.excludePeers.filter { $0 != peerId } + let filter = filters[i] + if case let .filter(id, title, emoticon, data) = filter { + var updatedData = data + if updatedData.includePeers.peers.contains(peerId) { + updatedData.includePeers.setPeers(data.includePeers.peers.filter { $0 != peerId }) + } + if updatedData.excludePeers.contains(peerId) { + updatedData.excludePeers = data.excludePeers.filter { $0 != peerId } + } + updatedFilters.append(.filter(id: id, title: title, emoticon: emoticon, data: updatedData)) + } else { + updatedFilters.append(filter) } } - return filters + return updatedFilters }) if peerId.namespace == Namespaces.Peer.SecretChat { if let state = transaction.getPeerChatState(peerId) as? SecretChatState, state.embeddedState != .terminated { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index c2c2a2dddf..2edbf22487 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -585,8 +585,10 @@ public extension TelegramEngine { sortedFilters.append(contentsOf: filters[index...]) sortedFilters.append(contentsOf: filters[0 ..< index]) for i in 0 ..< sortedFilters.count { - if let value = getForFilter(predicate: getFilterPredicate(sortedFilters[i].data), isArchived: false) { - return (peer: value.peer, unreadCount: value.unreadCount, location: i == 0 ? .same : .folder(id: sortedFilters[i].id, title: sortedFilters[i].title)) + if case let .filter(id, title, _, data) = sortedFilters[i] { + if let value = getForFilter(predicate: getFilterPredicate(data), isArchived: false) { + return (peer: value.peer, unreadCount: value.unreadCount, location: i == 0 ? .same : .folder(id: id, title: title)) + } } } return nil diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift index 938dd85d06..e329d996d3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift @@ -10,7 +10,7 @@ public enum TogglePeerChatPinnedLocation { public enum TogglePeerChatPinnedResult { case done - case limitExceeded(Int) + case limitExceeded(count: Int, limit: Int) } func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, location: TogglePeerChatPinnedLocation, itemId: PinnedItemId) -> Signal { @@ -50,8 +50,9 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio limitCount = Int(limitsConfiguration.maxArchivedPinnedChatCount) } - if sameKind.count + additionalCount > limitCount { - return .limitExceeded(limitCount) + let count = sameKind.count + additionalCount + if count > limitCount, itemIds.firstIndex(of: itemId) == nil { + return .limitExceeded(count: sameKind.count, limit: limitCount) } else { if let index = itemIds.firstIndex(of: itemId) { itemIds.remove(at: index) @@ -66,16 +67,18 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio var result: TogglePeerChatPinnedResult = .done _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in var filters = filters - if let index = filters.firstIndex(where: { $0.id == filterId }) { + if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(id, title, emoticon, data) = filters[index] { switch itemId { case let .peer(peerId): - if filters[index].data.includePeers.pinnedPeers.contains(peerId) { - filters[index].data.includePeers.removePinnedPeer(peerId) + var updatedData = data + if updatedData.includePeers.pinnedPeers.contains(peerId) { + updatedData.includePeers.removePinnedPeer(peerId) } else { - if !filters[index].data.includePeers.addPinnedPeer(peerId) { - result = .limitExceeded(100) + if !updatedData.includePeers.addPinnedPeer(peerId) { + result = .limitExceeded(count: updatedData.includePeers.pinnedPeers.count, limit: 100) } } + filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) } } return filters @@ -92,8 +95,8 @@ func _internal_getPinnedItemIds(transaction: Transaction, location: TogglePeerCh case let .filter(filterId): var itemIds: [PinnedItemId] = [] let _ = _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in - if let index = filters.firstIndex(where: { $0.id == filterId }) { - itemIds = filters[index].data.includePeers.pinnedPeers.map { peerId in + if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(_, _, _, data) = filters[index] { + itemIds = data.includePeers.pinnedPeers.map { peerId in return .peer(peerId) } } @@ -117,7 +120,7 @@ func _internal_reorderPinnedItemIds(transaction: Transaction, location: TogglePe var result: Bool = false _internal_updateChatListFiltersInteractively(transaction: transaction, { filters in var filters = filters - if let index = filters.firstIndex(where: { $0.id == filterId }) { + if let index = filters.firstIndex(where: { $0.id == filterId }), case let .filter(id, title, emoticon, data) = filters[index] { let peerIds: [PeerId] = itemIds.map { itemId -> PeerId in switch itemId { case let .peer(peerId): @@ -125,8 +128,10 @@ func _internal_reorderPinnedItemIds(transaction: Transaction, location: TogglePe } } - if filters[index].data.includePeers.pinnedPeers != peerIds { - filters[index].data.includePeers.reorderPinnedPeers(peerIds) + var updatedData = data + if updatedData.includePeers.pinnedPeers != peerIds { + updatedData.includePeers.reorderPinnedPeers(peerIds) + filters[index] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData) result = true } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index a7fe6e144b..24bd08d45b 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -225,7 +225,25 @@ public struct PresentationResourcesChatList { public static func verifiedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListVerifiedIcon.rawValue, { theme in - return UIImage(bundleImageName: "Chat List/PeerVerifiedIcon")?.precomposed() + if let backgroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Chat List/PeerVerifiedIconForeground") { + return generateImage(backgroundImage.size, contextGenerator: { size, context in + if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.saveGState() + context.clip(to: CGRect(origin: .zero, size: size), mask: backgroundCgImage) + + context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.restoreGState() + + context.clip(to: CGRect(origin: .zero, size: size), mask: foregroundCgImage) + context.setFillColor(theme.chatList.unreadBadgeActiveTextColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) + } else { + return nil + } }) } @@ -236,18 +254,9 @@ public struct PresentationResourcesChatList { if let cgImage = image.cgImage { context.clear(CGRect(origin: CGPoint(), size: size)) context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - let colorsArray: [CGColor] = [ - UIColor(rgb: 0x1d95fa).cgColor, - UIColor(rgb: 0x1d95fa).cgColor, - UIColor(rgb: 0x7c8cfe).cgColor, - UIColor(rgb: 0xcb87f7).cgColor, - UIColor(rgb: 0xcb87f7).cgColor - ] - var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0] - let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) } }, opaque: false) } else { diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/Contents.json index c2624d2477..c7e09bc23e 100644 --- a/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "premiumbadge_16.pdf", + "filename" : "premiumbadge_16 (1).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf b/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf new file mode 100644 index 0000000000..45d70d5dd0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf @@ -0,0 +1,97 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.050293 1.510986 cm +0.000000 0.000000 0.000000 scn +6.588397 2.211904 m +3.455913 0.292931 l +3.130194 0.093393 2.704389 0.195683 2.504852 0.521402 c +2.407398 0.680485 2.378342 0.872190 2.424281 1.053005 c +2.909188 2.961615 l +3.084232 3.650591 3.555697 4.226520 4.196528 4.534193 c +7.613911 6.174939 l +7.773231 6.251431 7.840376 6.442595 7.763884 6.601914 c +7.701937 6.730938 7.561847 6.803138 7.420821 6.778723 c +3.616836 6.120156 l +2.843574 5.986285 2.050602 6.199815 1.449121 6.703874 c +0.247410 7.710940 l +-0.045357 7.956288 -0.083799 8.392517 0.161549 8.685284 c +0.280877 8.827677 0.452473 8.916075 0.637689 8.930570 c +4.309271 9.217907 l +4.568659 9.238207 4.794709 9.402359 4.894286 9.642731 c +6.310713 13.061901 l +6.456904 13.414798 6.861495 13.582366 7.214393 13.436174 c +7.383842 13.365978 7.518470 13.231350 7.588666 13.061901 c +9.005094 9.642731 l +9.104671 9.402359 9.330721 9.238207 9.590108 9.217907 c +13.281865 8.928991 l +13.662680 8.899189 13.947231 8.566318 13.917429 8.185503 c +13.903092 8.002308 13.816444 7.832350 13.676609 7.713137 c +10.861062 5.312818 l +10.662857 5.143844 10.576386 4.877848 10.637341 4.624624 c +11.502927 1.028795 l +11.592322 0.657424 11.363736 0.283898 10.992365 0.194502 c +10.813918 0.151546 10.625716 0.181284 10.469205 0.277163 c +7.310984 2.211904 l +7.089269 2.347727 6.810111 2.347727 6.588397 2.211904 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1423 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001513 00000 n +0000001536 00000 n +0000001709 00000 n +0000001783 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1842 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16.pdf deleted file mode 100644 index 738347eb92..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16.pdf +++ /dev/null @@ -1,240 +0,0 @@ -%PDF-1.7 - -1 0 obj - << /Type /XObject - /Length 2 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 16.000000 16.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 -0.972168 3.548462 cm -0.976471 0.631373 0.101961 scn -8.051580 -0.065265 m -8.386255 0.136172 8.553593 0.236891 8.732291 0.276237 c -8.890351 0.311038 9.054091 0.311038 9.212152 0.276237 c -9.390850 0.236891 9.558187 0.136172 9.892862 -0.065265 c -10.895140 -0.668524 l -12.034245 -1.354137 12.603798 -1.696944 13.007534 -1.631411 c -13.357951 -1.574532 13.660391 -1.354298 13.822091 -1.038259 c -14.008394 -0.674131 13.856972 -0.026846 13.554128 1.267724 c -13.291615 2.389894 l -13.202210 2.772070 13.157508 2.963159 13.175235 3.146049 c -13.190914 3.307812 13.241741 3.464196 13.324162 3.604267 c -13.417347 3.762632 13.565852 3.890926 13.862864 4.147514 c -14.735314 4.901226 l -15.744158 5.772767 16.248579 6.208537 16.311743 6.613088 c -16.366564 6.964196 16.250860 7.320419 16.000189 7.572302 c -15.711361 7.862525 15.047148 7.918721 13.718721 8.031113 c -12.565221 8.128704 l -12.175616 8.161667 11.980813 8.178148 11.812840 8.251106 c -11.664268 8.315638 11.531527 8.411745 11.423840 8.532748 c -11.302092 8.669552 11.225623 8.849475 11.072685 9.209321 c -10.615263 10.285586 l -10.098071 11.502484 9.839476 12.110933 9.475130 12.294894 c -9.158871 12.454576 8.785572 12.454576 8.469313 12.294894 c -8.104968 12.110933 7.846372 11.502483 7.329179 10.285583 c -6.871758 9.209320 l -6.718821 8.849475 6.642353 8.669552 6.520605 8.532748 c -6.412918 8.411745 6.280177 8.315639 6.131604 8.251106 c -5.963631 8.178148 5.768828 8.161667 5.379222 8.128704 c -3.676359 7.984634 l -2.812796 7.911572 2.381014 7.875041 2.157691 7.739972 c -1.683759 7.453331 1.491831 6.862423 1.706876 6.352001 c -1.808207 6.111484 2.136114 5.828204 2.791929 5.261645 c -2.791929 5.261645 l -3.063776 5.026796 3.199700 4.909371 3.347136 4.820806 c -3.653106 4.637008 4.002688 4.538440 4.359606 4.535333 c -4.531591 4.533835 4.708836 4.562960 5.063324 4.621209 c -8.032967 5.109177 l -8.951306 5.260077 9.410476 5.335527 9.519723 5.245213 c -9.613404 5.167767 9.653514 5.042924 9.622472 4.925406 c -9.586272 4.788361 9.169083 4.582251 8.334703 4.170029 c -5.901720 2.968024 l -5.497570 2.768355 5.295495 2.668521 5.126820 2.533812 c -4.890324 2.344938 4.699779 2.104854 4.569541 1.831648 c -4.476653 1.636790 4.425312 1.417324 4.322631 0.978392 c -4.322631 0.978392 l -4.077288 -0.070379 3.954616 -0.594764 4.064208 -0.903394 c -4.218257 -1.337227 4.622936 -1.631910 5.083112 -1.645350 c -5.410482 -1.654910 5.871894 -1.377192 6.794718 -0.821755 c -8.051580 -0.065265 l -h -f* -n -Q -q -1.000000 0.000000 -0.000000 1.000000 -0.972168 3.548462 cm -0.000000 0.000000 0.000000 scn -8.051580 -0.065265 m -8.386255 0.136172 8.553593 0.236891 8.732291 0.276237 c -8.890351 0.311038 9.054091 0.311038 9.212152 0.276237 c -9.390850 0.236891 9.558187 0.136172 9.892862 -0.065265 c -10.895140 -0.668524 l -12.034245 -1.354137 12.603798 -1.696944 13.007534 -1.631411 c -13.357951 -1.574532 13.660391 -1.354298 13.822091 -1.038259 c -14.008394 -0.674131 13.856972 -0.026846 13.554128 1.267724 c -13.291615 2.389894 l -13.202210 2.772070 13.157508 2.963159 13.175235 3.146049 c -13.190914 3.307812 13.241741 3.464196 13.324162 3.604267 c -13.417347 3.762632 13.565852 3.890926 13.862864 4.147514 c -14.735314 4.901226 l -15.744158 5.772767 16.248579 6.208537 16.311743 6.613088 c -16.366564 6.964196 16.250860 7.320419 16.000189 7.572302 c -15.711361 7.862525 15.047148 7.918721 13.718721 8.031113 c -12.565221 8.128704 l -12.175616 8.161667 11.980813 8.178148 11.812840 8.251106 c -11.664268 8.315638 11.531527 8.411745 11.423840 8.532748 c -11.302092 8.669552 11.225623 8.849475 11.072685 9.209321 c -10.615263 10.285586 l -10.098071 11.502484 9.839476 12.110933 9.475130 12.294894 c -9.158871 12.454576 8.785572 12.454576 8.469313 12.294894 c -8.104968 12.110933 7.846372 11.502483 7.329179 10.285583 c -6.871758 9.209320 l -6.718821 8.849475 6.642353 8.669552 6.520605 8.532748 c -6.412918 8.411745 6.280177 8.315639 6.131604 8.251106 c -5.963631 8.178148 5.768828 8.161667 5.379222 8.128704 c -3.676359 7.984634 l -2.812796 7.911572 2.381014 7.875041 2.157691 7.739972 c -1.683759 7.453331 1.491831 6.862423 1.706876 6.352001 c -1.808207 6.111484 2.136114 5.828204 2.791929 5.261645 c -2.791929 5.261645 l -3.063776 5.026796 3.199700 4.909371 3.347136 4.820806 c -3.653106 4.637008 4.002688 4.538440 4.359606 4.535333 c -4.531591 4.533835 4.708836 4.562960 5.063324 4.621209 c -8.032967 5.109177 l -8.951306 5.260077 9.410476 5.335527 9.519723 5.245213 c -9.613404 5.167767 9.653514 5.042924 9.622472 4.925406 c -9.586272 4.788361 9.169083 4.582251 8.334703 4.170029 c -5.901720 2.968024 l -5.497570 2.768355 5.295495 2.668521 5.126820 2.533812 c -4.890324 2.344938 4.699779 2.104854 4.569541 1.831648 c -4.476653 1.636790 4.425312 1.417324 4.322631 0.978392 c -4.322631 0.978392 l -4.077288 -0.070379 3.954616 -0.594764 4.064208 -0.903394 c -4.218257 -1.337227 4.622936 -1.631910 5.083112 -1.645350 c -5.410482 -1.654910 5.871894 -1.377192 6.794718 -0.821755 c -8.051580 -0.065265 l -h -f* -n -Q - -endstream -endobj - -2 0 obj - 4928 -endobj - -3 0 obj - << /Type /XObject - /Length 4 0 R - /Group << /Type /Group - /S /Transparency - >> - /Subtype /Form - /Resources << >> - /BBox [ 0.000000 0.000000 16.000000 16.000000 ] - >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -0.000000 0.000000 0.000000 scn -0.000000 16.000000 m -16.000000 16.000000 l -16.000000 0.000000 l -0.000000 0.000000 l -0.000000 16.000000 l -h -f -n -Q - -endstream -endobj - -4 0 obj - 232 -endobj - -5 0 obj - << /XObject << /X1 1 0 R >> - /ExtGState << /E1 << /SMask << /Type /Mask - /G 3 0 R - /S /Alpha - >> - /Type /ExtGState - >> >> - >> -endobj - -6 0 obj - << /Length 7 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -/E1 gs -/X1 Do -Q - -endstream -endobj - -7 0 obj - 46 -endobj - -8 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] - /Resources 5 0 R - /Contents 6 0 R - /Parent 9 0 R - >> -endobj - -9 0 obj - << /Kids [ 8 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -10 0 obj - << /Pages 9 0 R - /Type /Catalog - >> -endobj - -xref -0 11 -0000000000 65535 f -0000000010 00000 n -0000005186 00000 n -0000005209 00000 n -0000005689 00000 n -0000005711 00000 n -0000006009 00000 n -0000006111 00000 n -0000006132 00000 n -0000006305 00000 n -0000006379 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 10 0 R - /Size 11 ->> -startxref -6439 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/Contents.json new file mode 100644 index 0000000000..64530da322 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "verifybadge1_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/verifybadge1_16.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/verifybadge1_16.pdf new file mode 100644 index 0000000000..3c35d1707e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/verifybadge1_16.pdf @@ -0,0 +1,111 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.300293 1.044312 cm +0.000000 0.000000 0.000000 scn +-0.030118 6.492163 m +0.081165 6.149670 0.378177 5.852658 0.972203 5.258633 c +1.449829 4.781006 l +1.449829 4.105689 l +1.449829 3.265610 1.449829 2.845571 1.613319 2.524703 c +1.757129 2.242459 1.986600 2.012989 2.268843 1.869179 c +2.589711 1.705688 3.009750 1.705688 3.849829 1.705688 c +4.525146 1.705688 l +5.002711 1.228124 l +5.596736 0.634098 5.893749 0.337086 6.236242 0.225803 c +6.537507 0.127916 6.862028 0.127916 7.163293 0.225803 c +7.505786 0.337086 7.802798 0.634098 8.396824 1.228124 c +8.874389 1.705688 l +9.549829 1.705688 l +10.389908 1.705688 10.809947 1.705688 11.130815 1.869179 c +11.413058 2.012989 11.642529 2.242459 11.786339 2.524703 c +11.949829 2.845571 11.949829 3.265610 11.949829 4.105688 c +11.949829 4.781129 l +12.427332 5.258632 l +12.427341 5.258640 l +13.021361 5.852661 13.318372 6.149672 13.429653 6.492163 c +13.527540 6.793428 13.527540 7.117949 13.429653 7.419214 c +13.318372 7.761705 13.021361 8.058716 12.427344 8.652733 c +12.427333 8.652744 l +11.949829 9.130249 l +11.949829 9.805689 l +11.949829 10.645767 11.949829 11.065806 11.786339 11.386674 c +11.642529 11.668918 11.413058 11.898388 11.130815 12.042198 c +10.809947 12.205688 10.389908 12.205688 9.549829 12.205688 c +8.874389 12.205688 l +8.396824 12.683253 l +7.802798 13.277279 7.505786 13.574291 7.163293 13.685574 c +6.862028 13.783461 6.537507 13.783461 6.236242 13.685574 c +5.893750 13.574291 5.596738 13.277280 5.002716 12.683257 c +5.002711 12.683253 l +4.525146 12.205688 l +3.849829 12.205688 l +3.009750 12.205688 2.589711 12.205688 2.268843 12.042198 c +1.986600 11.898388 1.757129 11.668918 1.613319 11.386674 c +1.449829 11.065806 1.449829 10.645767 1.449829 9.805689 c +1.449829 9.130371 l +0.972203 8.652744 l +0.972199 8.652741 l +0.378176 8.058718 0.081164 7.761706 -0.030118 7.419214 c +-0.128005 7.117949 -0.128005 6.793428 -0.030118 6.492163 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1960 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002050 00000 n +0000002073 00000 n +0000002246 00000 n +0000002320 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2379 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/Contents.json new file mode 100644 index 0000000000..14c81bae2c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "verifybadge2_16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/verifybadge2_16.pdf b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/verifybadge2_16.pdf new file mode 100644 index 0000000000..a71253cee3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/verifybadge2_16.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.375000 4.311890 cm +0.000000 0.000000 0.000000 scn +0.530330 3.926815 m +0.237437 4.219707 -0.237437 4.219707 -0.530330 3.926815 c +-0.823223 3.633921 -0.823223 3.159048 -0.530330 2.866154 c +0.530330 3.926815 l +h +1.750000 1.646484 m +1.219670 1.116154 l +1.512563 0.823261 1.987437 0.823261 2.280330 1.116154 c +1.750000 1.646484 l +h +5.780330 4.616154 m +6.073223 4.909048 6.073223 5.383921 5.780330 5.676815 c +5.487437 5.969707 5.012563 5.969707 4.719670 5.676815 c +5.780330 4.616154 l +h +-0.530330 2.866154 m +1.219670 1.116154 l +2.280330 2.176815 l +0.530330 3.926815 l +-0.530330 2.866154 l +h +2.280330 1.116154 m +5.780330 4.616154 l +4.719670 5.676815 l +1.219670 2.176815 l +2.280330 1.116154 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 762 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000852 00000 n +0000000874 00000 n +0000001047 00000 n +0000001121 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1180 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf new file mode 100644 index 0000000000..8c08ed90ad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf @@ -0,0 +1,76 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.000000 4.484375 cm +1.000000 1.000000 1.000000 scn +20.000000 11.044711 m +20.000000 16.065481 15.522848 20.135620 10.000000 20.135620 c +4.477152 20.135620 0.000000 16.065481 0.000000 11.044711 c +0.000000 8.181209 1.337573 5.834022 3.613619 4.167677 c +3.904685 3.954580 4.172771 2.770550 3.523984 1.775995 c +2.875197 0.781441 2.066323 0.326941 2.471971 0.156790 c +2.722059 0.051889 4.199766 -0.000002 5.266314 0.598131 c +6.791368 1.453400 7.217727 2.304844 7.545889 2.229574 c +8.331102 2.049473 9.153261 1.953802 10.000000 1.953802 c +15.522848 1.953802 20.000000 6.023941 20.000000 11.044711 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 668 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000758 00000 n +0000000780 00000 n +0000000953 00000 n +0000001027 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1086 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json new file mode 100644 index 0000000000..8941a63389 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Chat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json new file mode 100644 index 0000000000..8065bf20cd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Voice.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf new file mode 100644 index 0000000000..c18024241d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf @@ -0,0 +1,91 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 7.000000 4.400036 cm +1.000000 1.000000 1.000000 scn +4.000000 16.599964 m +4.000000 18.809103 5.790861 20.599964 8.000000 20.599964 c +10.209139 20.599964 12.000000 18.809103 12.000000 16.599964 c +12.000000 11.599964 l +12.000000 9.390825 10.209139 7.599964 8.000000 7.599964 c +5.790861 7.599964 4.000000 9.390825 4.000000 11.599964 c +4.000000 16.599964 l +h +1.000000 12.699940 m +1.552285 12.699940 2.000000 12.252225 2.000000 11.699940 c +2.000000 11.599937 l +2.000000 8.286230 4.686291 5.599939 8.000000 5.599939 c +11.313708 5.599939 14.000000 8.286230 14.000000 11.599938 c +14.000000 11.699940 l +14.000000 12.252225 14.447715 12.699940 15.000000 12.699940 c +15.552285 12.699940 16.000000 12.252225 16.000000 11.699940 c +16.000000 11.599938 l +16.000000 7.520320 12.946311 4.153931 9.000000 3.661833 c +9.000000 1.000000 l +9.000000 0.447716 8.552285 0.000000 8.000000 0.000000 c +7.447715 0.000000 7.000000 0.447716 7.000000 1.000000 c +7.000000 3.661833 l +3.053689 4.153931 0.000000 7.520320 0.000000 11.599937 c +0.000000 11.699940 l +0.000000 12.252225 0.447715 12.699940 1.000000 12.699940 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1162 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001252 00000 n +0000001275 00000 n +0000001448 00000 n +0000001522 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1581 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 85f0f75270..bccdf1eaf2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1095,8 +1095,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case .premium = value { controller?.dismiss() - let controller = PremiumStickersScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions) - strongSelf.present(controller, in: .window(.root)) + let controller = PremiumReactionsScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, reactions: premiumReactions) + strongSelf.push(controller) return } @@ -3481,8 +3481,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused() } } + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController }) - strongSelf.present(controller, in: .window(.root)) + controller.navigationPresentation = .flatModal + strongSelf.push(controller) +// strongSelf.present(controller, in: .window(.root)) strongSelf.currentMenuWebAppController = controller } else if simple { strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: peerId, url: url, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme)) @@ -3496,6 +3500,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: false, isSimple: true) let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, openUrl: { [weak self] url in self?.openUrl(url, concealed: true, forceExternal: true) + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController }) strongSelf.currentWebAppController = controller strongSelf.present(controller, in: .window(.root)) @@ -3519,6 +3525,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.openUrl(url, concealed: true, forceExternal: true) }, completion: { [weak self] in self?.chatDisplayNode.historyNode.scrollToEndOfHistory() + }, getNavigationController: { [weak self] in + return self?.effectiveNavigationController }) strongSelf.currentWebAppController = controller strongSelf.present(controller, in: .window(.root)) @@ -10745,6 +10753,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } public func presentAttachmentBot(botId: PeerId, payload: String?) { + self.attachmentController?.dismiss(animated: true, completion: nil) self.presentAttachmentMenu(editMediaOptions: nil, editMediaReference: nil, botId: botId, botPayload: payload) } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 5a6c14d730..845131605f 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1617,7 +1617,9 @@ final class ChatMediaInputNode: ChatInputNode { } } }))) - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: item, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: item, menu: menuItems, openPremiumIntro: { + + })) } else { return nil } @@ -1743,7 +1745,9 @@ final class ChatMediaInputNode: ChatInputNode { } })) ) - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 6a19e918cc..9958364622 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -748,7 +748,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { var imageHorizontalOffset: CGFloat = 0.0 if !(telegramFile?.videoThumbnails.isEmpty ?? true) { displaySize = CGSize(width: 240.0, height: 240.0) - imageVerticalInset = -30.0 + imageVerticalInset = -20.0 imageHorizontalOffset = 12.0 } @@ -1638,7 +1638,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } else { let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) let additionalAnimationNode = AnimatedStickerNode() - additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2), height: Int(animationSize.height * 2), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) + additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 2), height: Int(animationSize.height * 2), playbackMode: .once, mode: isStickerEffect ? .cached : .direct(cachePathPrefix: pathPrefix)) var animationFrame: CGRect if isStickerEffect { let scale: CGFloat = 0.245 diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index d0c5d65ccf..9c84aa6a0f 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -139,14 +139,16 @@ final class ChatTitleView: UIView, NavigationBarTitleView { segments = [.text(0, NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: titleFont, textColor: titleTheme.rootController.navigationBar.primaryTextColor))] } } - if peer.isFake { - titleCredibilityIcon = .fake - } else if peer.isScam { - titleCredibilityIcon = .scam - } else if peer.isVerified { - titleCredibilityIcon = .verified - } else if peer.isPremium { - titleCredibilityIcon = .premium + if peer.id != self.account.peerId { + if peer.isFake { + titleCredibilityIcon = .fake + } else if peer.isScam { + titleCredibilityIcon = .scam + } else if peer.isVerified { + titleCredibilityIcon = .verified + } else if peer.isPremium { + titleCredibilityIcon = .premium + } } } if peerView.peerId.namespace == Namespaces.Peer.SecretChat { @@ -619,6 +621,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { self.strings = strings let titleContent = self.titleContent + self.titleCredibilityIcon = .none self.titleContent = titleContent let _ = self.updateStatus() diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 0c9160ae16..c5aa863376 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -531,7 +531,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } })) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: item, menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: item, menu: menuItems, openPremiumIntro: { + + })) } else { return nil } @@ -595,7 +597,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } })) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, item: .pack(item), menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 4f9b362ab9..3402e0effb 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -174,7 +174,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont } }))) } - selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems)) + selectedItemNodeAndContent = (itemNode, StickerPreviewPeekContent(account: item.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .found(FoundStickerItem(file: file, stringRepresentations: [])), menu: menuItems, openPremiumIntro: { + + })) } else { var menuItems: [ContextMenuItem] = [] if case let .internalReference(internalReference) = item.result, let file = internalReference.file, file.isAnimated { diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 6a15cb7543..39aa0c3c87 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -220,7 +220,9 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } })) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index ba1d5a9529..10ec47256d 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -175,7 +175,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie } })) ) - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 1d957886a2..a7fc817a59 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -584,7 +584,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur |> `catch` { _ -> Signal in return .single(nil) }) - navigationController.pushViewController(BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + let checkoutController = BotCheckoutController(context: context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in /*strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .paymentSent(currencyValue: currencyValue, itemTitle: invoice.title), elevatedLayout: false, action: { action in guard let strongSelf = self, let receiptMessageId = receiptMessageId else { return false @@ -596,7 +596,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur } return false }), in: .current)*/ - })) + }) + checkoutController.navigationPresentation = .modal + navigationController.pushViewController(checkoutController) } } } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index a108ce37c8..c2999aaad2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2250,7 +2250,16 @@ final class PeerInfoHeaderNode: ASDisplayNode { self.avatarListNode.listContainerNode.updateEntryIsHidden(entry: entry) } - private var initializedCredibilityIcon = false + private enum CredibilityIcon { + case none + case premium + case verified + case fake + case scam + } + + private var currentCredibilityIcon: CredibilityIcon? + private var currentPanelStatusData: PeerInfoStatusData? func update(width: CGFloat, containerHeight: CGFloat, containerInset: CGFloat, statusBarHeight: CGFloat, navigationHeight: CGFloat, isModalOverlay: Bool, isMediaOnly: Bool, contentOffset: CGFloat, paneContainerY: CGFloat, presentationData: PresentationData, peer: Peer?, cachedData: CachedPeerData?, notificationSettings: TelegramPeerNotificationSettings?, statusData: PeerInfoStatusData?, panelStatusData: (PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), isSecretChat: Bool, isContact: Bool, isSettings: Bool, state: PeerInfoState, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition, additive: Bool) -> CGFloat { self.state = state @@ -2276,65 +2285,80 @@ final class PeerInfoHeaderNode: ASDisplayNode { let themeUpdated = self.presentationData?.theme !== presentationData.theme self.presentationData = presentationData - if themeUpdated || !initializedCredibilityIcon { + let credibilityIcon: CredibilityIcon + if let peer = peer { + if peer.isFake { + credibilityIcon = .fake + } else if peer.isScam { + credibilityIcon = .scam + } else if peer.isVerified { + credibilityIcon = .verified + } else if peer.isPremium { + credibilityIcon = .premium + } else { + credibilityIcon = .none + } + } else { + credibilityIcon = .none + } + + if themeUpdated || self.currentCredibilityIcon != credibilityIcon { + self.currentCredibilityIcon = credibilityIcon let image: UIImage? var expandedImage: UIImage? - if let peer = peer { - self.initializedCredibilityIcon = true - if peer.isFake { - image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if peer.isScam { - image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) - } else if peer.isVerified { - if let sourceImage = UIImage(bundleImageName: "Peer Info/VerifiedIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in + + if case .fake = credibilityIcon { + image = PresentationResourcesChatList.fakeIcon(presentationData.theme, strings: presentationData.strings, type: .regular) + } else if case .scam = credibilityIcon { + image = PresentationResourcesChatList.scamIcon(presentationData.theme, strings: presentationData.strings, type: .regular) + } else if case .verified = credibilityIcon { + if let sourceImage = UIImage(bundleImageName: "Peer Info/VerifiedIcon") { + image = generateImage(sourceImage.size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 7.0, dy: 7.0)) + context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) + context.clip(to: CGRect(origin: CGPoint(), size: size), mask: sourceImage.cgImage!) + context.fill(CGRect(origin: CGPoint(), size: size)) + }) + } else { + image = nil + } + } else if case .premium = credibilityIcon { + if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { + image = generateImage(sourceImage.size, contextGenerator: { size, context in + if let cgImage = sourceImage.cgImage { context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentationData.theme.list.itemCheckColors.foregroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 7.0, dy: 7.0)) - context.setFillColor(presentationData.theme.list.itemCheckColors.fillColor.cgColor) - context.clip(to: CGRect(origin: CGPoint(), size: size), mask: sourceImage.cgImage!) - context.fill(CGRect(origin: CGPoint(), size: size)) - }) - } else { - image = nil - } - } else if peer.isPremium { - if let sourceImage = UIImage(bundleImageName: "Peer Info/PremiumIcon") { - image = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - - let colorsArray: [CGColor] = [ - UIColor(rgb: 0x6B93FF).cgColor, - UIColor(rgb: 0x6B93FF).cgColor, - UIColor(rgb: 0x976FFF).cgColor, - UIColor(rgb: 0xE46ACE).cgColor, - UIColor(rgb: 0xE46ACE).cgColor - ] - var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0] - let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0x6B93FF).cgColor, + UIColor(rgb: 0x6B93FF).cgColor, + UIColor(rgb: 0x976FFF).cgColor, + UIColor(rgb: 0xE46ACE).cgColor, + UIColor(rgb: 0xE46ACE).cgColor + ] + var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! - context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) - } - }, opaque: false) - expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in - if let cgImage = sourceImage.cgImage { - context.clear(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) - context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - } - }, opaque: false) - } else { - image = nil - } + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + } + }, opaque: false) + expandedImage = generateImage(sourceImage.size, contextGenerator: { size, context in + if let cgImage = sourceImage.cgImage { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.75).cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + }, opaque: false) } else { image = nil } } else { image = nil } + self.titleCredibilityIconNode.image = image self.titleExpandedCredibilityIconNode.image = expandedImage ?? image } @@ -2409,6 +2433,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let buttonKeys: [PeerInfoHeaderButtonKey] = self.isSettings ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact) + var isPremium = false var isVerified = false var isFake = false let smallTitleString: NSAttributedString @@ -2419,6 +2444,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { var nextPanelSubtitleString: NSAttributedString? let usernameString: NSAttributedString if let peer = peer { + isPremium = peer.isPremium isVerified = peer.isVerified isFake = peer.isFake || peer.isScam } @@ -2494,7 +2520,7 @@ final class PeerInfoHeaderNode: ASDisplayNode { let textSideInset: CGFloat = 36.0 let expandedAvatarHeight: CGFloat = expandedAvatarListSize.height - let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) + let titleConstrainedSize = CGSize(width: width - textSideInset * 2.0 - (isPremium || isVerified || isFake ? 20.0 : 0.0), height: .greatestFiniteMagnitude) let titleNodeLayout = self.titleNode.updateLayout(states: [ TitleNodeStateRegular: MultiScaleTextState(attributedText: titleString, constrainedSize: titleConstrainedSize), diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 0a675f3210..26831d0301 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -176,7 +176,9 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } })) ] - return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems)) + return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), menu: menuItems, openPremiumIntro: { + + })) } else { return nil } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 64b594f5f7..6a50539e6a 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -242,6 +242,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { } else { return .join(String(component.dropFirst())) } + } else if pathComponents[0].hasPrefix("$") || pathComponents[0].hasPrefix("%24") { + let component = pathComponents[0].replacingOccurrences(of: "%24", with: "$") + return .invoice(component) } return .peerName(peerName, nil) } else if pathComponents.count == 2 || pathComponents.count == 3 { diff --git a/submodules/Utils/RangeSet/Package.swift b/submodules/Utils/RangeSet/Package.swift new file mode 100644 index 0000000000..54660c2fe6 --- /dev/null +++ b/submodules/Utils/RangeSet/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "RangeSet", + platforms: [.macOS(.v10_11)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "RangeSet", + targets: ["RangeSet"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "RangeSet", + dependencies: [], + path: "Sources") + ] +) diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index bce8b3e6ae..a3161a58c3 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -25,6 +25,7 @@ swift_library( "//submodules/LegacyComponents:LegacyComponents", "//submodules/UrlHandling:UrlHandling", "//submodules/MoreButtonNode:MoreButtonNode", + "//submodules/BotPaymentsUI:BotPaymentsUI", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index a1f3487425..7375ae9a30 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -18,6 +18,7 @@ import PhotoResources import LegacyComponents import UrlHandling import MoreButtonNode +import BotPaymentsUI private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] @@ -80,7 +81,7 @@ public final class WebAppController: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } - + fileprivate class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private weak var controller: WebAppController? @@ -93,23 +94,23 @@ public final class WebAppController: ViewController, AttachmentContainable { private let context: AccountContext var presentationData: PresentationData - private let present: (ViewController, Any?) -> Void private var queryId: Int64? private var placeholderDisposable: Disposable? private var iconDisposable: Disposable? private var keepAliveDisposable: Disposable? + private var paymentDisposable: Disposable? + private var didTransitionIn = false private var dismissed = false private var validLayout: (ContainerViewLayout, CGFloat)? - init(context: AccountContext, controller: WebAppController, present: @escaping (ViewController, Any?) -> Void) { + init(context: AccountContext, controller: WebAppController) { self.context = context self.controller = controller self.presentationData = controller.presentationData - self.present = present super.init() @@ -264,6 +265,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.placeholderDisposable?.dispose() self.iconDisposable?.dispose() self.keepAliveDisposable?.dispose() + self.paymentDisposable?.dispose() self.webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) } @@ -475,16 +477,38 @@ public final class WebAppController: ViewController, AttachmentContainable { case "web_app_request_theme": self.sendThemeChangedEvent() case "web_app_expand": - self.controller?.requestAttachmentMenuExpansion() + controller.requestAttachmentMenuExpansion() case "web_app_close": - self.controller?.dismiss() + controller.dismiss() case "web_app_open_tg_link": if let json = json, let path = json["path_full"] as? String { - print(path) + controller.openUrl("https://t.me\(path)") + controller.dismiss() } case "web_app_open_invoice": if let json = json, let slug = json["slug"] as? String { - print(slug) + self.paymentDisposable = (context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> deliverOnMainQueue).start(next: { [weak self] invoice in + if let strongSelf = self, let invoice = invoice { + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + if let navigationController = strongSelf.controller?.getNavigationController() { + let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + + }) + checkoutController.navigationPresentation = .modal + navigationController.pushViewController(checkoutController) + } + } + }) } default: break @@ -539,8 +563,27 @@ public final class WebAppController: ViewController, AttachmentContainable { self.webView?.sendEvent(name: "theme_changed", data: themeParamsString) } - private func sendInvoiceClosedEvent(slug: String, status: String) { - let paramsString = "{slug: \"\(slug)\", status: \"\(status)\"}" + enum InvoiceCloseResult { + case paid + case pending + case cancelled + case failed + + var string: String { + switch self { + case .paid: + return "paid" + case .pending: + return "pending" + case .cancelled: + return "cancelled" + case .failed: + return "failed" + } + } + } + private func sendInvoiceClosedEvent(slug: String, result: InvoiceCloseResult) { + let paramsString = "{slug: \"\(slug)\", status: \"\(result.string)\"}" self.webView?.sendEvent(name: "invoice_closed", data: paramsString) } } @@ -704,9 +747,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } override public func loadDisplayNode() { - self.displayNode = Node(context: self.context, controller: self, present: { [weak self] c, a in - self?.present(c, in: .window(.root), with: a) - }) + self.displayNode = Node(context: self.context, controller: self) self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) self.updateTabBarAlpha(1.0, .immediate) @@ -790,13 +831,14 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent } } -public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}) -> ViewController { +public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, params: WebAppParameters, openUrl: @escaping (String) -> Void, getInputContainerNode: @escaping () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil }, completion: @escaping () -> Void = {}, willDismiss: @escaping () -> Void = {}, didDismiss: @escaping () -> Void = {}, getNavigationController: @escaping () -> NavigationController? = { return nil }) -> ViewController { let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.fromMenu) controller.getInputContainerNode = getInputContainerNode controller.requestController = { _, present in let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, replyToMessageId: nil) webAppController.openUrl = openUrl webAppController.completion = completion + webAppController.getNavigationController = getNavigationController present(webAppController, webAppController.mediaPickerContext) } controller.willDismiss = willDismiss