From 57d8635db9deaa38affe1a97b33ba9d6e729b27b Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Tue, 26 Apr 2022 20:04:33 +0400 Subject: [PATCH 1/7] premium effect --- .../SyncCore/SyncCore_TelegramMediaFile.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index ad27b08f9a..30b505a88d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -491,6 +491,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 From 60c23f8a135967d7abdef3507ea1016ea11219f8 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Mon, 16 May 2022 11:42:06 +0400 Subject: [PATCH 2/7] macos related --- submodules/TelegramCore/Sources/MacOS/MacInternalUpdater.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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))) } From 529f4e19f55f453bc617ee153dd30a61775fd92c Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Wed, 18 May 2022 10:46:37 +0400 Subject: [PATCH 3/7] macos package --- submodules/Postbox/Package.swift | 2 ++ 1 file changed, 2 insertions(+) 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)], From 712eb75c7b4b8df1b5125b5678bbfb25599c5a12 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Wed, 18 May 2022 10:46:55 +0400 Subject: [PATCH 4/7] macos package --- submodules/Utils/RangeSet/Package.swift | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 submodules/Utils/RangeSet/Package.swift 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") + ] +) From 4c88a9618ff73fc580128b2654c3ef939d943e12 Mon Sep 17 00:00:00 2001 From: Mike Renoir <> Date: Wed, 18 May 2022 10:51:53 +0400 Subject: [PATCH 5/7] fix limit unpin --- .../Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift index 938dd85d06..c43dc42ddc 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift @@ -50,7 +50,7 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio limitCount = Int(limitsConfiguration.maxArchivedPinnedChatCount) } - if sameKind.count + additionalCount > limitCount { + if sameKind.count + additionalCount > limitCount, itemIds.firstIndex(of: itemId) == nil { return .limitExceeded(limitCount) } else { if let index = itemIds.firstIndex(of: itemId) { From bb0d62f42ebeb7d10361c17f552c74d959578ffa Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 18 May 2022 18:17:32 +0400 Subject: [PATCH 6/7] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 +- .../Sources/AttachmentController.swift | 2 + .../ChatListUI/Sources/ChatContextMenus.swift | 118 +++--- .../Sources/ChatListController.swift | 110 +++-- .../Sources/ChatListControllerNode.swift | 21 +- .../ChatListFilterPresetController.swift | 203 +++++---- .../ChatListFilterPresetListController.swift | 42 +- .../ChatListFilterPresetListItem.swift | 11 +- .../ChatListFilterTabContainerNode.swift | 48 ++- ...ChatListFilterTabInlineContainerNode.swift | 2 +- .../Sources/Node/ChatListNode.swift | 16 +- .../Sources/Node/ChatListNodeLocation.swift | 7 +- .../Sources/Node/ChatListViewTransition.swift | 4 +- .../TabBarChatListFilterController.swift | 186 +++++---- .../Sources/ItemListPeerItem.swift | 18 +- .../Sources/ItemListStickerPackItem.swift | 1 + .../Sources/IncreaseLimitHeaderItem.swift | 4 +- submodules/PremiumUI/BUILD | 2 + submodules/PremiumUI/Resources/star | Bin 0 -> 49022 bytes submodules/PremiumUI/Resources/star.scn | Bin 111215 -> 0 bytes .../Sources/PremiumIntroScreen.swift | 392 ++++-------------- .../Sources/PremiumLimitScreen.swift | 83 +++- .../Sources/PremiumStarComponent.swift | 373 +++++++++++++++++ .../Sources/StickerPackScreen.swift | 1 + submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api4.swift | 12 + .../Peers/ChatListFiltering.swift | 161 ++++--- .../TelegramEngine/Peers/RemovePeerChat.swift | 21 +- .../Peers/TelegramEnginePeers.swift | 6 +- .../Peers/TogglePeerChatPinned.swift | 31 +- .../PresentationResourcesChatList.swift | 33 +- .../PeerPremiumIcon.imageset/Contents.json | 2 +- .../premiumbadge_16 (1).pdf | 97 +++++ .../premiumbadge_16.pdf | 240 ----------- .../Contents.json | 12 + .../verifybadge1_16.pdf | 111 +++++ .../Contents.json | 12 + .../verifybadge2_16.pdf | 92 ++++ .../Premium/Perk/Chat.imageset/Chat.pdf | 76 ++++ .../Premium/Perk/Chat.imageset/Contents.json | 12 + .../Premium/Perk/Voice.imageset/Contents.json | 12 + .../Premium/Perk/Voice.imageset/Voice.pdf | 91 ++++ .../TelegramUI/Sources/ChatController.swift | 11 +- .../ChatMessageAnimatedStickerItemNode.swift | 4 +- .../TelegramUI/Sources/ChatTitleView.swift | 18 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 6 +- .../Sources/PeerInfo/PeerInfoHeaderNode.swift | 128 +++--- .../UrlHandling/Sources/UrlHandling.swift | 3 + submodules/WebUI/BUILD | 1 + .../WebUI/Sources/WebAppController.swift | 70 +++- 50 files changed, 1850 insertions(+), 1066 deletions(-) create mode 100644 submodules/PremiumUI/Resources/star delete mode 100644 submodules/PremiumUI/Resources/star.scn create mode 100644 submodules/PremiumUI/Sources/PremiumStarComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16 (1).pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerPremiumIcon.imageset/premiumbadge_16.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconBackground.imageset/verifybadge1_16.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat List/PeerVerifiedIconForeground.imageset/verifybadge2_16.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Chat.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/Chat.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Perk/Voice.imageset/Voice.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 215d112550..404c7d68cb 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,13 +7594,17 @@ 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)."; 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..f61b11caaa 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) @@ -322,7 +330,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch f(.default) var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, action: { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: 0, action: { let premiumScreen = PremiumIntroScreen(context: context) replaceImpl?(premiumScreen) }) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index d91976ebc4..a2c8f32dd8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -191,8 +191,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 +762,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: false, 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 +833,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: true, 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) } @@ -1210,8 +1210,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 } } @@ -1271,8 +1271,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 } @@ -1305,7 +1312,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 @@ -1328,20 +1339,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) }) @@ -1799,7 +1810,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: true, 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 { @@ -1865,7 +1876,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 } @@ -1915,6 +1926,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } + private var initializedFilters = false private func reloadFilters(firstUpdate: (() -> Void)? = nil) { let preferencesKey: PostboxViewKey = .preferences(keys: Set([ ApplicationSpecificPreferencesKeys.chatListFilterSettings @@ -1930,21 +1942,30 @@ 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.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) ) - |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom in + |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom, accountPeer in guard let strongSelf = self else { return } + + let isPremium = accountPeer?.isPremium ?? false + 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 @@ -1958,7 +1979,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 @@ -1982,12 +2013,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 @@ -2006,7 +2051,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)) } } @@ -2342,7 +2387,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 { @@ -3189,15 +3239,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: @@ -3223,7 +3273,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 a5d4d91775..a3293354db 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 } }) @@ -987,18 +1038,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 } @@ -1090,16 +1146,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..64aeebffc0 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -844,7 +844,7 @@ public final class ChatListNode: ListView { break case .limitExceeded: var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .pins, action: { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: 0, 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/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/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index 3fe8dd5760..a5be59dbc1 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -42,6 +42,8 @@ swift_library( "//submodules/Components/Forms/PrefixSectionGroupComponent:PrefixSectionGroupComponent", "//submodules/InAppPurchaseManager:InAppPurchaseManager", "//submodules/ConfettiEffect:ConfettiEffect", + "//submodules/TextFormat:TextFormat", + "//submodules/GZip:GZip", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star new file mode 100644 index 0000000000000000000000000000000000000000..16390db8f19af1e1e917e467871927d588fc4ad3 GIT binary patch literal 49022 zcmV)xK$E{8iwFo|&4Xe919Nm?axQaYZUF4P2UrtX_dk9oGeHnb>|K#!7ZDIE$-PLi zpeRk0W`H1QAeaON>)3k*dj$)&wXbUnGuQ5}*n4+v*bDahzmpJ{*>&H2f4}eh{Qlqf zc{V;FXU?3t_sp4d&i$NwZ(>}UHrZfwb2|?xAOkdDfE?I>GN5dP^AL^RkgQAX>#Wwt zCnb}kP-mkqEy7tBH&7FAG*kxE#FeoMMM&qK-8Gq-1XL__2G|0-P`yrPWT`EcsHVUk zlmqUdI|u;*APhu-K_C;%1X*A{SOgY>Wncr?OjV|;Q?)4vsv+e_wV~QmAyg<8K}Axr z)Bq}pN~Y4N3@U}nq=r$;sTI^pDu>FYR#B^|HPl)vk6K5qr#4URpfS~z+D!2j zrVdaCsYBFZ>Iij|Iz}C*&QNEmi&P=?hsvZ|oDteUKrte(t4)>U0gdChb5w(_XZa_Mt=RFgl#>M@P~9X*Hcf z52p3>D0(bCj-Ewl(b;qkeV=|nKcb(~ujtqGTgHYd%amtoGx1CUGk{5AjLdXq1~ZeH z!(=lHm}N{Zvx?cm9A=I%N10!l0_HSxfw{^&U_LRQnQwAR&dBZLW#!f7&E>B0)^bmI zdwBcW|P&SNBS(}PBHEinH)VFcAX>Q|Y<7v~zrlSqC39t#Y3AX8D6KxZ3Gswnh zGt_33%{ZH>HrY0dZI;+9w^?Dc(k90y*JiCvp3OR&%{Du1_S*0^*yg^?1Dl67g*K0D z9@{*zd1~{_=DE!ao0m4PY+ltNsh2sdpYkTSvG%@K0jUE||`sBEDqs9=csAGuNYc#1LY3g`QuqIJU0)T`@ z2oXrogr}*ActWhg`G-P1)EUVs=_$S>%+#sznsDcMt=d4EtemJD;HybBBpWmRGSYOZ znp9(~qV`aAs?iYgqjvhmspAI?(CgAu6T;KeP)0IoLqXaIF{<@OYwLY9sR^OUDVjdT z!5gb^__5~dlr*iTGil5iLE7l7)9Unn6ZPs8O|aUiiPWSHN!II9Q;1G|4Z3uFye2F- zu&+VrNoYNWT;i9SVC|HZjiJgrA^?D`qwg>P#!yY1R4;+~Wjlr{8)zX}mGIXI6u%Ud zLNSJNc7i=E$785+VXb(D&XWJ2U0^UGLo*V=S_<+v97x)2x zl2Ez`?g6?17^5e*uF)r_4hYhdPx2^@YT-^Jb^2s-8)iBpO-Ia4h%iIETB{-Ui`NP# zOP_Qf9kCO6B9ZZG19=Wf>IB`;SVhHnVw3?oeWp*cv4`3)C_E`SAwiSck0d;8g5WQP zm=xU*bzHKRT-qf$)o7~#K?()(rLF~{I z^a8oCAVbCI|Fp<*ZGtqHaicKVMyZy72gdi#@uBT0cmmCyCnb zEC>gEKm_R9&*F)x>d`g$JlbW z6cvk~x0XEbHXwyO^FKdv9Y_O%fgTuukvwzf;%A-#V5p#bR9aG|AvxaQpRCa)*#5xm zZ21x^S6}pq|G`sW?IfnR6>M&Q4GaUri6$e!NH7YF1}qo@#)5Ig=O%!OM5{?ehbdqx z`8yrV2v-xk4AF$^5)%y?BXKWX8gZ|RDaong*pF3IBbmbBr`0B>8Fa}BYOP;}kvyab zgOPae0AmulBvGx^l2)uKp|2(*UYl+pZTC$`))!}jB(*lta;2Tbf!MN6%B{t5=fo$n zX39yme$k?p0!tEyV#yqHbAc)LfqPH-c?`@m1i{^;v?B$;7YcfrLP5GA@O6R*Q+yg$w6e8VQ8{mYdNNs>>>|BObb`7IacdqbY<1PGQp}qIT01!-@lJmu?mZeSZ-@07?=Y# zv6TdO_5oYKRcFvz8tTMELTeIyGR1yE@=q=6h5mX=t_fG`lhu~19Fv8{ zLNtTZH6)t4rjlH)*7^&hmPkmUNl7*cW1nxj-pmR$Xfz4YLWmjTll)1b>h!USs-j^* zq*pab55c;j<{^^kYt^cAl3G7N++9_JF45Sl_^iP%Rmhwq0xFfQL-nzR^0nqEU14@sLM8<(^juF>j*!M0DbAy7vK_k?g2V9*`s$rNmpR4AmzS^bxA_)ub7de9}p9=mlp?AoeZx;vYEy zF+jRjt+(1S(vU_x-Rj|$N<*wTIg5hJdPzyEF+leIAH0iT7UFkk%ko$ z^%_H}Mq7%!tBJcRkqp!7bZLet@q-YBn%MzGt=5#>H&H1)CDoij*s69;uo!kYsR~Qr zUg=B0*g8Smzrcy)BG|=B6mx9Z8d6-X`jnX1-#qfG9#|+R)gH^p&sGkUHdfiH%JE`> z`nI<0G~1F3oS?H+v_)}uwk5&@K~|=!P*tgF1OvLpWfI3_s{~bI^_3KUcF-t=e_!5`DUM z?%cO$h`HCUzAVi$igH1fWW=a4XLVYdmLXTU6!3na9<}{#jn~Dod)Bc=Gv$yfyJ@ zdLsZ-g7Bn=8q)-NGNG&LiBg1KD$oNgw16wC##!h?W|`37&V;^9Xoq-GC=vPtp z_ya_AalgXLo&e%Btvb`8m&e~|`+xB=YBG$%XJ=hnrd~+24v1JuNEh`s+mfn&qLer8769Jd_wV2LmGZiA0Bj>ymfcgzYtjyYO7;L8+d~q9+RUQK zTb7sz!0S8!%J&7J%4u>&yDsu64 z0Ql6i4ICOe{-`#SIC!p;v*ft69BbgvsBxd-Q@M?tfi0*6YMbXg+zE2-O43em5JJki z{$LP6$D_bBf}HaRfu2gwwcNUl zDzS0+HdQgd|7B^(<9~st1B|sJd*m9I8HBowW%~ zQyY^?Msb5{TIzrsloRDlxloOWd}FE!)s$*RHK$roEh$&36@i-&yr)`I?gZKrpeg`d z%7gOkOGX@mdemAnYSc8(sh|XPnoy#Jq{k&B4>1oF(P{c*0#67tG&4;THZ(awsK6@6 z>-4Dvz-R^t^>Z=)t7JYY=7lF|1m_}ylA*N4K+O=1mNa2GCl-&gv?>%!=|Yt)x+p|r zMo5N8l1L32tyWEjZ?>i#TTfBem!jkp3(R!}YL zzC$&Dp;)sg`*In_K^2F07pTH*3aRyZV}0c}s>^2Dg(UIl(cJQ)B4 zB2t@d?4{B7BG6SEk(g{@5==xu$RGnsi4(9zpJbufNK7QfHhCE8A*9qc1d(wiu(%^d zMMuF|?d)vX+H4&=h0M;D002^Q28uQD(I%&+THHB|3XdeGdlWlWF4YG_iWwHEzEr=M z5~nwpwyUVVLUC(39YsY)2z9YmovK+yj$$lr1ZZ?A8lyhb8Wa7gcq%cMQd4nk1J)sz zN}x1sLpFw;8fFgZPyy#&AaVn#L6nwCp;FC$-<;C*OHNHoH--o^TYb%>a2FQJCs}Ri ztG41WHkx&`_BNO@g5X?APZ?Mz);X6-r-raDY$Mht%$#*125BerchI4P*3EPxy#x`eLuq|0vwiWBfwr1T~ z57v`y!+Hr#%%Emcv#8nB9P-X0|7KJ3$$KH;3v?FSmTkwjXM@>(P82vRi%x{*YBggPO?9QHAZ>I4Gpt;nvQk(5F~ z<{6wQbvlVcjR0RXhJNaj`N0@Nye3s6luSNq^PF#Gy(UFBL?cu{!i=hUmN6mST5@Ox zB&Q0(QC3G4V#_?qlb{JoH(EQk8g~oz3kc4mwo==u?bHrxC-pP6i|xR6WIM6mtddo+ zFpt_z?V6fIM6+4M`mmkZP&SMhB$%~{9HQ2yYYZ(DNRB1lSWOXbL1Dtf6mHQF zZ6(Zt;g*Zcu}$1LO{W#3b>dF(>7|??1^nZHt756js?lHGkk4FU%JPrLe|&|yxwdw< z-twOj)G5}F^=EA&gyPLWYG$kCIqEzJUb&LBu`(`Ew*+flrmj#| zscY1A>IQX_4Pd*lUDA8pF<+vvRS;Ak zq;M#))2W?0^XKEfs|2%Hr^v1M^N#u`F~uk9GaJtKDZSxu)OU#mWHMRVzq5eMUa)|y zLa_xR%ogZtwSc0$C}0vw3N>r{)nzp-9hwuEthTI9tfHQf!ivY|UVo}?b7ggf>A`<3 z^n>grYe14xthL{U1Sw0BlB^L)O8rZ3ys50ie=-Znyv12a=KH5Cq?Tl%Dbg%tRjP+9 zR#2*vtfwqU){ELI3zmggBUu(f{VeN8?UF^wqGd5`92?IjkjU1si6zlJfW&ta3)y6L zAUjBCroT)ri<8C65@Z@`k8FS}NtP@dC>tcx5*1TrI$0X4WmDKR*1#Isp(GDvu*2CA z>}Zx{#}LKFvy<4#!fHUZcw+eE)O2%+UXmU9#gQSwavZ5kH-@TH2MBI2EQ$OvgiNf7|t4 zNlxl5%}K?EP$kbDtJ3b+pxk~q(c1PtS-wOYPR6t8?2yvV56TWnwK*y~^*=KN$zI;u zb#`WLlt!H@ptgY9g z>x%WE>(TYuvFy0g?i4{1>-UUaI}^`)F?8@>VK#Zs4%B?V+}Z8!TSz@=;G%RP~smb~pD;`TeR7 zRs}vJ+C8*tIY+GJ=pSk6`R{2tfu2ZDq9@Z+=&66!^3=ofi{};{T0{YsQwJ?pvGW4u z!KrY~yq(r1xn&PlF522Fc2S^XC9d!9!K%QqM7!lyEwja1&i;{>?jkMedBuAES<40V zLV6Lsm|j9J740w}T85eH)E}-eFJu=4O1=9>EB&P_%;r^n`&){ZbS}M$UQMr|*HS7v zk6uTwr#H|W=}q)c^k#Ysy%p4=a_MdKc6tZBlm40BMgKzYCfe^I_qdndNAIUibUx@n zb2Lw5`T%{9K13g;kI+ZyWAt(QSL!urO#ep8{?~K?Df?ejUCHrDsw;hpK25!#7n1i` z`W)p60_cnMC30>d@Tac;XZjj_oxVZjU(+{*yC>HL(0AzH$?;wK9tkUhd1HiV6G@bX zQ!>3FEOYy65;c0gCc(0c!VosBr$()hA|<`rvUY9g8y2S?q)CV{#HX6)APB7ZQ^W4D zirR)GUAi{GhsbtKwJd^L>-1PfO;Vd_Qq$Fz?Kq;HFhon8-k78tpjW3Q5ln8`@KG(r zvKz;uQ>koqQ8S{QEu`E_wTPN)??{qB(uDP-biJlT+0JT1JSpdj%g?H2C+RHRFCxC_ z2x1-Ku$NAoIefQm7Uiy@xu@5N5V*jBp((IW_u{)jho~PUBa)(TCWw)X=^XGmG`_7a<&2 z>ZNLlI#TEueAMxS{MGRqVhPf|!Q55X)YOu$stU!lMIllQ4798RMjNae3KJT!inxLz(S%OEA0#qnPHN|U?wM7rj{}03B|!(<$LkV+z1QooM7K#vBf0xSK6|R9DO{=(Q->KcI*;IWvxYv9ZA2y z3C@CM**OAIlYrUbD+W4W+w>%hZP{Z= zY|)?PB`@)+iJ^GaBn_0Vnq-60Rg-m~bk$@(C|xx<4N6x{?h#s1ylV0Wl&+eX`2$3> zXw{^aLAdd*ex1EqxA*dD>F(OvAuu_W{r~v&w<{>^|850EmkU6pK{X&)xeFaa zN72c&md>Qd61+3Za!-G`Vlvm75S|DLf!_aLk`Myi9E5+Z{mq~Mgz`V3{NEeOTfi=` z8|(%9!9j2woC6oYC2&Rdm6p-v>FTr--H7&}JJE>lYPqMs4CTOc^fQ9YpVKesmsBrd zf3AUGn;~kgnM~8fC#$vWQg&JRaJLo?ZX+!fY!Us2Xl$Mx%cYAz@LtCLXP0F&Pi&5 z@U-SuLqY|2jwa9=r6wD6M$%NKqCC5RU18@qdi3}2=KEyIFt(&|(W05P%Us4zsMUXT z%5tSWQ!c`~IG4-V|7b&~xr}2fhzmJJ!Bk=@GgX+XmSU5sVJ+msQCX$IFpV6Q(ISv4=$Q7~XHu{sdnu&{5G|~`YZ6IZjc^TcZ zYf{)zo=RLHM7-it&cfb|Ixg~SC(3+#?Yi>qsB+A6|5Ip zTAaqjX?41k&?LQvlyusJu*47nFPQg1bny))QvLB(BZm5RTo}??1 zWz8j;Wv76@u$fdNQlg589a6H9y-I0IC9)C1+Jc@`iKKS>fkTySuxt1)Ol_TFP)Kf_ zlzTDO7A-5dmd%&N#b(LwQ;T7`8a39gY74cgunfTVLjTrh z(ClAy*i)xZQEN-A;;0e|VwH!7ha*lC%5ejsIyqe`tFTklvUOCRI8k6Oo*m&14-Z>M z#FO;8%2By!(PDPBqcZSBfx^bohg@jqsBG8!sUs5FfGqhVh=g}GCv?kbpxwlurFInanU@>W>*NO#LwuQL;b$v5@(6=@KV6Da|Y&p-V|kb|4umR3M7WBWAX_JbL_JQXT~<8o9Z)H^17sw{~}E?doQJ zwQk*BN(U(_Sq0mBx_LUZc4+-iG4)R|_1{}eod);715gMagIC}^MNu>*r^?XHX*aqf z?MwHjLuoZVkk-*dE%)@7#ndos;Aj0aFJgZE6UzU&*FT~B|KGfbFaz?hCpi{^7L4`} zlN?KdzoG)0XPr~{+gXx7HIM`ROD`Adp??n#u3^?PdCWRyJ+tA@cyPy@AXU3uw!@Is z)l|C=PU^^RXSW5eoOi9m)M~9crpGj40-m3g3Y`uJTFYj=j;3YF2+)Rkw2&rC?@<84vC>Jd>#8STqC~zmc+lm5r6{A2;i+rC%J*gH+2_dXETE~Lp z|Aip1t$VvRULKzA?JU5ry@z|d)*cqx)5EiU`!-e(*shJcdz&_G+Ijo~0{`JQ2yTz0`vZW;m#8bx9#7DfPcLFium$>5B^z`2m&%OQ$<^Rn9Pe5olr3g)KOAy*#D?;mQ zerAT`1kJyN)#T;m<>eLR73B(frN3IbWjWz*v%1Gs>{0dr2`+xL^<6BJSH6u|M)k#0 z6?l?hGy%Sx{C|tpve~Hx&S+Lqc_J|d$9WI`v%d|*avvp5C%gZz%I^Phpyu`uP&2>& z3FZIX>z`2mUk=pd75)I!+7Y03zzWnNw3@_XoL0$#)8rkCQJU6*)8w7x-V&^)l0!L? z`^Y=XeJxl`9w3J$8?;k(2_zmP1{D7^QUnS7WMgJm-yiM48$;Rt?RRerPMq+zzQ<5@ z1j@0;$#n$J-H-`q`z|9iT8;3fP6KPtUbAD*v49Z2vZuySWyVl8>?QLLib@~iZQ;``~BGRS&s zigF5?Jz-ag@nQ?ulR=f-Cm%Rb12N&DD67L7xbf6m*fA{?9oFYUmGM1X80djsb>9IM z9X;SikVoXvsvZPWP!3(>Pcig+a9(`ZH^TYO7vE-1{QQ|f$=(b z)c#@~JU--ie&TU=bn);G`2Jo;e%3t>IWOqT{~mr^dHMo}`VR@?z55?g`r5BS_ea&? zE3Z`YZELSVA2u}L%ePH~6TYlQ4{JK`ug?yK&!0>|-S6Mv3OXEA-uW~IeOr8<8!1P; zUD>H9)8+>Ei+utArRG#*bbiliZaskZ&e5p#h;>}Q@&*{`6OG<>o53}G!YMcOk48P8 zkK-y=?aa>^6^$+>Pv_p>JIUW(N@Vt|<=#Di2=m<(9@{f&#y~oYsf9ZVyt-lfpXVr@0RY#g3UFAk-LEQyhW<+yzXxk0w>=MSwRusZJ z&u_rItT*`&qKxoS^P5oi%f$R)w{|OECEbLF52O5Vjr{o!Yi>fi@woh_6VLGA?oBw? z_$>dptO4G6tP#pHW^;#5Ho&$Hw_x2oDx5D?eve9x5Li1wAh{qw>Pblf&&;n&5TdK}v2J@Rw_KfmMUPHys_J2mckmT%IyKTPXCHhIDf8XK9Psp<{&333+T7!L4mdlqsdsC8JI+5l94n%4K_+}HH`u!meo;pa zx%1OFyQzKfpicqbzcd-gMUR?@Yb?G6%g=nr^}9C_hs;;Q2de9wt6~!NS(NCVz4{!d z&B(!?mu|sXUM=|LFLLlMMI1cp+JOIU_C@U5`!)piviS-v_v21gZ^MT7HN5KQ{kZnD zI9N{Bm;ciA5*{g!hd=Q%_?8WGak-^qy%#^J%};x}A5VBP$Ghoop?u}+>1c=7g8e=BVOT&Au9%3fz#}h47l)*p9yM1M@F5 zGD0KUvD1v4p`H8!;!xh^7=F~Y!rObI`KObo;s!xBSIDxaUh0em8mpXX>DX z18+v*pMz#`pSR^GYx7Zf-|Gon{271VGdBu%Ntwwlk;;NFR!xqDSpVDRoKxcR`_oYVeg z%GmW&@bC36ahjvI;gSohvByVOenr+mxbDbm{Ah7QzS(*|e$e76c+ccpBz{lvzSE}Q z>Z?C;_RhZiyLGGa=4K7~E7=8n+r_JK$6BuZQ@$SVHDU@@?x@U9svd^@0;2G?UUq!a zoDdwHG6i>wY0WbOrl8O_Q*e)>e*DYolM#}w!6BO`@Ksfjs7KLioPIx^Z}7$i&APW5 zx2xvIhtHgd_8#Z(GS^)G!+{9&c`t`&ZJ5c2S8zsyH*q-b+93N{^y;Ae%5!*fzr<3o$a^J5J`IMD%t-y7)b z-QW^{%TFot?e%lKm6=KSyAtv1bUEC$sY~#+3GTeNZzcX_m3(~Y)QkN4OZxEb58uZp zHw=fKdvp1I>Cdop90yO<+Rm#FKEz{xm!aSbTlnW!FX5p@RZwu*W&DQzZE^R>ePA)Ung)DXY{{|8Wt?%1L(=B zb%g#heIeiN=f+TwIg~w_-xpRM3D-_=SghKE)(Kd~+dEG}OZuJR6^|Zr zA6Je>zd!oI&rZC}O>Hm=^<7XC>pk9ZkJip7WByTGIB5@mch_8WCigP#cliYWTf_0_ z$g|(^lXWNgf%_BDX^-1@!+8_Gb1SD3p7oROmH8B}I;PLpm*7F??fH>AOx(kAIr!k1 zsuF(?%kVF1n_e7WM&i1>q@8KCcdB-j6LM}92`!cp>o85LK{MapA$dGsLW^a@`u7?! z9__qx50A+>ExAXzUpBhC>k5uxPx4|rd>yh7`6!R#!{4N|SVnBaK2KQ`)4e8+=A^V( zMr{A3xnoex{@?j?RetBh&*=BR9U zDJ_u^`F6Ft4bZYmp6G0c`JCAQH;z}Jfjt6HyKYiiEF+GcBNIGe&WB5=-&-jymJ!F@ z$+Rrh;5zrw{b!3LvFWm9G3QPAT|5@^;`n`))Rw!j(snD_=7Hrt-)@BJUmJ#Qrfiaiy-9-Y9m(pSxaZVeme1z|h zlB4V0w@LEe=z%Zr(nI+$AMcRl#zQ_2u_0a$%WGFjXt9hquNs{>Jl?e_XLm?Si)F+) zxkKfpILHa{&T$G}oVQKENqA}XUVNW<%Otc|Mx66sm!Y87W-foi)~ga)EF&H-+$y_6 z)9ugsn!DS)#bZj^&|o-o%NVZzr3v2Rap&-?Fc=?K#8pl0>@6Of+%C6-FX%>mwN+YIgHGnz@1;b2R3lEgP%KSxf8=M z+gLTADb$r{lG4S-E zJiNL5V7}3eAj!20Pn^VukI&&IFRKWF?j$}rV;)C+1`__W<)5&&VJcL4ZBq{O`h+VK z4T9yr7I};1#WD@uWCsq#{t6eQP0rsSlO5>79f#eG-}1%R*4zCBFH5S5ln-6=SM>OT zI}NRhwkrzqzfLM7xw;t|h^weNQiXVRj9h# z1Spml%Uqk<8Ye^tpfsP^@aEvwctigHRAc{S$+Z#fo$zba7ZuE%2^)nt5#R2MY${KZ z@bARM^0Qk%Lak~nQ;p;M^YX)mDCgQ}l~`UZbNd?4^}qiCdEBh6>euQ3H+AL*bO>@v z@wI--cdF(QnOf89shpeaQgKA4eBL3YQ&9vI+Bx5OrE1Nfe&qQTqMHrIsl#|q}cZo^w4!45V7(dHl;MAGE zNU+Zp+sP8XOL`$0|C^%sw2!L%tA{Abu^Cc(*s8?xVi`~N3%X*ein^cs+$px^7gW8V zDq3z6m@mGT_>7M(;Ws$H!wi!LBlF2R@+(}PxhQ|uflo+PPY0XqDWhr?{Ry3Zt%dew zmng;jTeq`Oy8$ox)sr*e=e^m;ZTU;S)wE~a1T zt>oHA$7Y~$tsHTS<_0L+KLZ(mZiwwsf`rdpT#FjWFW`FF$MXGmtwmploX7SH>TzOu zv5e|S4k~K!1lLc==Eg72L5;UP#-~D9e88v1?c-Ka zdV~hQ#Z{VC;*K^QjK0)-i#-l@lkhtXTdfjud-~RJSk^UHCFF2Y5kaxMSY~=!s_Ed{ zO*qpg8frcbG`U~cgwKMWl554|g?L=>n7W7SF>ebtEK7h@UhA=E?i)<|p2-)}SsiBL z24sxLwV%q3ROR4j%OB&^X=6F>7Hje0mFIEN{PFo>`qIvYc_6S@vE&w)~k;CEk zN1?OOf@Jr_RN-+1L0e0M|@#Ke#DPPkEs zr*3V6mg4RzF+GL}z#W{FsNh5v6w}A=yWt8!{;2GP1@Ka3N1Ssp0#!OXPeKo9(T`u_ z{RoYnwnrtVx%2zD+e<&7Sl>RXwAd}GYRf(#&8@zwuDKylkU6({zl8Rz7@Zk?)v=0BRGylx}& zv2S_;M*P%5X}|gt8r8uFd%Gi*nC50?qYsmx^YUrqpqN$`&On(q_V|w91X!c(4AjZw zh^HN7CG^C8Yf;9u3pnSkTfUe!B;}y0u1|1UwwC)nc_z}${eTy(7|m@|8c@F5TO1|Z znlGlGjoF|Qe1197Krwyl(qPk&!JBZ)Q-dUNaqRH{?t9K={GiWZl5dUpTtN|j@Gv}I zOfU7Fi91w&kL!Qy$GIo2#7(4hh^YSpb=IUqe{XN2e7z|%E zUC716pMXA%+QKMC$;m&RfHOwh!L08MIXt^SLU&!3i4&Ys`89)9Na&5qb$H>8X#P~< zQ22b%Nqk~QBDa)lDxvcVKjLq%hQY^XIj!)mAYats#4@<>Qv>W(&ybRzaXI_is~Vu z&6b^)LouB_sU;G0sNZWP+<3|t3A$CtUn-$%{a#4Mg+}PSW}Zq+4=nhMhSjQ!OdH=R zt4*hTdQk=N-LRv|TFfW3qhL7P;Ad2cX}1$uXrS8@{>YsvP)wgIn2sF#m&J>>O@sbb zr=xAXobdHG6D4$opf%|Emkan_;mS^8de4lNsNU7bct?yUhX&6?MJwK8hf)5V`k)aV ziF$+Y@2-+Brt@~}PzgRipV31x?S9p0QipED{qJW;;$rfIlbo#6PuTJ6FnGgx2+kPx z8pqY>ZxYk>8_&cu7QDq1IydK_^9np_*&{sQc|C4v?kZAGUBKGZwccWSQOEf>q2nw( z;T&LA=f}wih`gx6Ur>ZL?^pg=oIAP(mN7 zRR;-eM*Nx!#q`IO&C!C{2-&<{4ViFXB<*CGU%Y&a#%HfJ= z7u!U&={MlxcDVrRb(K|BUVlJY)5pSNFJ?$++f8$k|F#Ewm%N!!Oz(!%&`ymVzBrrU z!(Z3Q~t{4qXMAm_&Qo{6@&yu)E!b?$o7 z5Ol$=2pbBOoy7F!W}Hg!`HY>zp_s1Ne5fgA?|S@g`eoh{S#YqhN63o)}hXjw>iuO6W-`o#CMKD!#(CQxf`ijuF;;YRf8I*XSIZ0 zHYbnNagZOdWDM+NI)T&r%Q&|&Z6);k^!NC_?UcTS-nXi1$M8a2u}K5uU3Q6t-o3>SuV3wn#{RStifQqh`^2lYu&|E*-e#4AKJ~sm zFRb0S82M5qraK;8%9YD{gL-uPNwTIdUh5ar;(Y>QIw86#5_Fh6ArCIi?TiH7Dvw(w zp=Tu&qL*bGprO^atHgBAxR0nx#qwyzOMg}5w(qFh!jo`+w!5nBl=sMf;Uvg2izW2i zZgbGLk$3nJ>N!wMmra_2%DLI%Q;Avd*uu%Ec5i3=wDL>|z2{2~(q>%5YbK3Ris=X3 zGIYuF5#C<;xBNZjXQG{5-r$qLkMsA-GtlV1uW*f5H@wC4wnM+E1fO5BiG^ai{HzSq z(ckiL;^gs?xTx@<7k}C}8^`5jN%&&<*H4Zrb3L=k7&H%_tmT4p!cXE!xfLWa^{szT z-W0zLAE~Z~*DpomC5b0-&3V^V;+PW0+WFekaQ`7E@s*)dRO0yRWSWjwt=fh+)s;!& zYn9VX95Nvr5BBzw#M;u_1TrqAV&?Y=l31(F$KihI*?8sJ{_w1>IiB;(9Un=_gWD&B z^Akoq=T~hxB8kBoU7BI8LMm=NZaxgI_z8A@tmVTsM6coUZSI0t+ zkI(qQ`Xdk=->*E|;W_{7*JG0SeNTOY(anJOsJq7`v3!Xi3oi`J?=-r{X-O=9&f2Q% zeqRUAk0fK_zMaZjRA#3N*UrKK2L?`Q@lz+d(OH2u7|J=s}2lhz9YZ^`4k4A2S?OY(y_hg4`e2e*Iz z3|2~;A;}e+y*A^Co1Q_vMeNjE%1uL&*6=; zJ0v+|9XP|6?URiXyUvj0la`Tp`1t23sO8gKD9$HGQiAz`F(=WTxHj;mM+ST|@+5lw z*i(`#2KWw#5$m?0chCSg@A5*?DIVx}18EL9FmXO=Kl&wH1*gMN(|4e!{7cv^CSHf%o&RaHLiP&oIO`0Xt$&JweP6+SulM|A&biX_ z87h();jN$xlANRNyc1RVKE~VWL%JmA^r*G~`Nt083*EBdR@#7Sdp_gepU9EqqPimt z=;H5#_+5(i1mo$^Vq15-ke?^XOUq-j(W|>zxN4z?Bu9y5BHHUvgEv`t>aiG!{I)x3 zNY*?BFM2s1R+~Nt`CTr+cb<<|8IR0D-$EbbJ!z~-lHWw_`&Z3E_vmdnX4!X@IPYaX z8h~acJjT;6R#S=d-j+oy{I&Bod}`%jN#47VF$S)8&c^dJS@7oIP(E?;HWa_d0GIS# zqKsX*4Lxj=0k2mI=i3d)MjpMg;mG+bl!-&LQBl@>SoY0G5@XqDeU)rj*rN|m@&ht~ z4RG?6RZ5a`(4#J!;6w+OJd;#(>)?90#U+C8{@5M8zqc3Sdh3)G?LE+XHxv9=bqqY; z+ylML6XvDw@*z8(!?1f&P45;jz!ElntdiYKA z8}{t3h2_7tfj#DJ#?Mb`;kph+sLz~(*9E6Qr}5cjOmByR8Z`87ymCJ*nrT2jReii? zPu@skc0M{ZR1f!rWWmZV3y`N>ym$50^WdKHJJIORdN?C)Fsyg=G>I1jblKug{IDIG zov7ufIPZnWe*OsAU!L*jD{#bb+o6K=XZ-yO+u?*oW8vt5?)b5CFVwyL0G+zI<7Ria z!_ljGrSCL%tU0=$V3@Wjwy8S~|79)w#XgL;-Qh@ECu)Jp9?dc#*!FL!S3xyES!7~9knTV?86*r zH`NQpbWg#r3+BO`i%oC~O*Rfc&%$5)Zt&kivoW`LI-CjL!QoZ2v5V7m7#iFL^TfCmEKz|$Vv@J3}kOgrg; zE;(((DTeNFO^XzCv&uF+yK6JJ;CpLy>U{y;?q41b+&>VNEiAxC-&|JhDvHBZT_51) zZo7E(%(dK4$!qYlfKOb?fC+eE>LZe`f91t8pMP6`uVr4xqu(9l+fG@EpZ7b8Gv{vO zH{70uZ)T0h4nHFap9yD4-1y|Pkf-7cyFM?5RftMkgK#Qbr+ z+rpH?H@U*`*}Pb0?r$4aTM2)` zPqTTaqChx~@ZHJ}=Wm_>b3ly0uYm@VYp3k$!-@H97Z)Vxa3`}XCziPq+a3w}bn%Ge z>gQcWyDm0B;}fRyV*cS#&(Q9B_Q=~emlw-?yZ;7F4C7!G-!(jc?FpJ;uZA_1S$xfE z(^2)Cclcw&_i$oITt6OT1AiWxBkcYFdUCg?|6Xst9J5e#Zr~v+m-5nr4uh4{>zQ@mKlYyW&) zcf>V3=X3$Dh*(1E&LcR|_7{F={!Dyl+IZ}?yoZFJv||)b2L5>N4n`u=!O0KT^{Ige zzMITdKF|OTt5t=c=f6oQ=2yAk4Ubm3%dyWlD#bEgxQ=kj!ui~_PurBS5AmwmWO z-*+p=FQ?$p_mF$CfmdoaB;oVFB=M0knVgt!3SNfKMD-GK%xeiRSwJeKof{_NI8IKVp}{_eC@B9qYZDb7)pMSSE>yifiW%si}zUg#I_x|i*7 zyAMKc%HqU)AEgW4zN|fZXFrh>%S?!^fp-;kLlcbYdRwQ9S^v2|u_oxz@Hi>OroZbY~nV=D)w^gajR28)R}~ z8QJ1?NYH0Z*C||^7gtc<6ZOy*+qt}$AJOe8>K0QL-3-m+#WFn%Md(Bm4u0;ifnUG+ zF}iv@0V=jEl%0W=o0kp z;89%7xRV$2CpBJxknB1>{rXp4EOTEy0ohG0#2;G}@YAowqbsrZal+g^{M^ksD#5q8 z3bqpd*$MSbK0!IS=`%_qBaQ{{CGj}8-CcaC2FJJ6?%;;z=i>W26udTLJofN?fcy77 zE0HM^JCBSJSMlyGr}^OHi}09vN3j3Nz5G)+0}ss~kMAXhOZW{NjKFJp2H-V=DoAA9 z!#d-t9<^|o+j4HqQy17fu?`PwHc^WC_NWW2(&91a;?!6vmbt&%4~|sL-~w;7Q8vEX z93FkuikqelP%aFl;J$O!IN#3^%E|Fbc<$zC{{FMsoR}Z9WhtILt|#B3f;}&mY46A5 zE;qxtMrk2@+Q~v(z0E)`LE(%;X|?c(5c!Ei41$}F@FBO47xdZE5D1mg4>j- zgM5>h5X{&fcPrNljh(iJ6Z6}|IN>=J+oDUK=5u11u@|f2tPWjKz2nQc!`?o;r{fir zerysi<|o%%!|i|b0Ok5E;>9vGKTna_*)d#^r5=a4PxKV$$zYVwus8$D{7Y9^hwhPw_yPfYzAq z;xmpW-u3W$mEhZ#Dk~)XxP5g@ZW~tMb?faUGU8Y;|MIvzT|UI}UrtKDnUgCYYW=?` zr6(-kzsVH7e>03d{ja}&GqU*mH=~8`->_%w*t3>zzzosNP(J-ITG?=Jit6?4W8P(Z z_$Wu$7^X_xxy8H6*-zfC4MwWuo@123i}osOW@=Oo{i^MY&@WTAUT9Fwd_nJR8JVT5 z(>z_(#P_zT)2Hv=SJOtQ8p@uVluz$_|CT#K_2Km3~qhqG1K-qHXqCw!Z~#g zF*(Qd%=b$i#+8ZPv-i=cp82y)!?>wa>v)?nVHFLF4xRdd4NlP^MNBcn6hQ>oslm2iMb=V`PCbEYqMSQhv`OezrR`S?Xb`( zf85RC+^2vorW-bIO?GWZa&@*eGnG?)Flp|N-~1@ z3)Ae?Be-FQi@fh$T5WpKXC&8TY)1Yb{L<8Q{1DD_c}9Mx&We@)|^)Ks8H?}*Owmcoh#kxD2c9yATdbVOT*K^F&eW&_XGo3#-nmgFo z(VIKsY?`uQG}k$}e5Z$_c6-+;8pVD6Uco!Ns;x48?nv$;TJ62l(Z!VdeiS$J<6`fM zrO5E7YduSG~? zmduKfA@dMI_w>E@>`&+WyWiKn_w~Ae-Pe7cSKjaEy`H`H+Iv6GI%^%zc^#%QBaK!Y zbuzhWdaUx>R64k*lbJQJke4?gjXwIO1L`dYS-q>tbaj(1tazZueu++@r|J#BAb5by z`0NxK@WvHlK0jmIhb7X^tJ5IaQlH(Kn?z5Yb%(JtnosZ8nM4l`NPuC>f3m6kc-kBp z4@=hTv)y)lI$wGhD9zr%7F$MB>AD=4mvxS{+7(N4XXZlCdwsSeJB}Jv6~hK|MRrts zG|h~OhcA;IYTOd}^y=&N;N7*QX1+}v9j)q49CMdjmzO8ekevCX_N4{0qb8Bo4>Taz z>e1F!`6=|st}gb9nwNE?QW`ZIyq%R?DPd!6mrnm4v1MOp=+>$qO{WR{^~tY%1Ew(` zg=X@)*g2z&nZI{a={wlY9t_uHCMc!T*QfM|UTO*Rpgx(lh5uruW|lM8#--9!TE?2| zmow)a(x^J$g6)1Yok<8ur_b&fvdINyjI-d`t?I8s3PQS=H`kNsUGtyp4yP_gRX>G# zJt<|y)Vr9s7OAvpxgq=b-7m)JK`IS-VSIY{@GW%=4zp+dFQ&XDl^#&o%Urthf-%oYrN#%lnM;c^80XYfsx4_m`p+H#cE*YH zLvAPA{aPOa4kXbh$4l7Q+xoEebTZ8{HDF6>^+9f73RRe9bXu)c9~?KQ(6krY%+y?c znCG8D7Yy3Nr1JG){DovX>&F+SWS2ff3g(IlN(N-kd>8PXn?R2&|Hkfrc%&$|^LCzPaUReQ!-b<)Laxt^#PzFf?nQIcMUY-sNYjTCq-~_rhpoMrR4+0}Qf4aTJhXk>FIPh!8a{$narwc08>vNoBT49ES5%kuaxjvnNBa#P&Q5e3hz^TI`ymyX6-iy^4Q#TYM-gdmfsj% zyRIpnj#aO)(XZ$Werzi3*z<+iwYQN;Y)_#_D*|EW=+(R*QHgZRoXw!L*pheRNG#3o zR}Sy9-_*t)TSE5@+6QOO7O^(V!vme8)N0njO4MHZ!nP!dsLb2)4WSs{p%S>p%T zj1X||tugdf@(H$d;zrW=HHN-_r%#rD^dzc>;%P$hHCAP{1Tig0p(fuAh~zCr^80Th zjr_inlo}2u@-Jg(<$`-J-5ZoS4wXBZYyi3~wZkuB0G^6!4t|#Wh!(WNi;87!U@@o$J zaA+D0xg5gCT`OUZuxZq%A=JjIyM%dfkWP2L3$-aL&t;1pOwmn!7#5U5Jv`#s z{_43f%RP?Hxp|q<_?rZ2&*N!O*kfkjw=8()A4fkwbY%0)qv20DUxfGenF@WN4yqWf zJ1I2F{zh%1?s+C%a3A{YF=Y+*GfZ__I!(*D&RobET(h_`jW+L#W?!wEQ?qhyI&He1 z#|*DH#YRj^qX$N0vY}%ak{g0uwNAYW@l(0RzE(&U;k|uot+LpT76)RsBZ>aipGn54 zh?7HJe9GVMK~zVVum=t#(n9AXR%O)*c6MqSO;(K{yULtSDf||Ue{>gn!mjRLIg+=0 z1^GH!kxB20r`GYdkhl?8=n@r)G_%+ z-lSz8pZZ>KVsal^t$Q;#=~+A`|fipJv6|SJwDQneC8+7ii}2fy80S6XG9u3 zt#*_hq3lh5-Qm+ElCH$2{tlZqJCQ0)^dZfHa~*c$)0_{R*+Rh>G*%~z@ZLV5i)4uI zvIJ%b3f7Gs=C!S8l^X>xc@AK4GLDX__kru<)S0t+i6XqW&*XRq7^XZ1N+%`I-?LW1 z`CxhG_11XmaVrKQ?3SNa+{34rpGd&Qi-fVcm_oZQdBUqzW$dx(f@^%bnz>wB%PP!F zqf-0l!Ll(D` z&Ua#^E!4@GsucPq?-JX5UV;>qrcmt{21IQCGP3__3{{_GM)=0IZ1(eHTK%DrY?^$R zwabjAc00^T#G3UC2~MUsYX>pU`NWK?T|&#^hLOu-`a$ZEB%0ZtMV7va1k2n=dV5hB z>(f{b@(mGm5m{J!$j%#>n~5U4x6iMT!L0G6(=ga1f*v23#XR=O5WMdvP^mj{%*azo zFuf*$TBT;PM?EWG^y4V%n&izaJ`f95V+3n`gXbd-MMLHQ~yQ z6uL;el;kxUc@#a?vK#yc3?*)H zDfC`61<);wq6>cQgx);*kr6b%Or|!mE12H=i_KM*wTh#6&#nf|V8NP*A5I{*H@=`lUF|1P?<1es-h7>ZFdZN4jlfPd_Fp#1hhL)?KwKA>ifXHGuxrf@68pMH+@)E|Rz0()BV09c;GBUla?xaZ*IfhOFn8Fg zT7mS7STmgS*M#0Y`Cb?;HL8O{q>Am$-|LN`7u2nAn{x*9X%r3`=*_n#=(=zmIX0BS@qoqndwv_w{b_?BuKlqrDVnTP7=vpDSz+bMZ07xD zE1dbl1m8%HCN)g~cuAre_IB$*Z$7y@3{y<%AZx@Pwl_b%(;i1SYvO*XVQ@S|6^BN< zU{i`7bX`)#zBgS_qVpmfRyY|qx@h3D+Dq*2f}>C%xNn7Tb>ZjAgYfZeBu>;m!CtET z%E$@s`HmF~^yVJgtC;D6d+uYyXM6L;<(+~xpf~QaC}ds{dn~ISfq8uo@D!D;u%%2N z4`CvSO1%eL!~&4#VnUP;rQ>VYsqBK$N;NTG;!tkoM)s82YNBf&g1ek1HiQJk@VT6&1AwUO%ioWuzsxH!b`LA zBuaw4PT1!Fa-T0oHw)%)>(_sn-n_(IfqKoKL0!VWu)X<)wk*u+r;=>N80U+Z%?hZLNP7OdBXkzvcSz<7TgO^UwC3}tfRw?Ma9GL-N- z>mla8JC&XC5PI`DpVuUEG|5Vz3VW_O23oIn2bYt*PdNJU>2p{R}OLsU{kl`B+!d8W7dXdoDJ9ee;<%J!6r=W;I zqpDzRi8QV5XOBOf&w$;D@nruNFAUrHU4-i{tt4aQrel59duZ_LLp!o8&}PUdxZ^dN zCY|e#i;lelnS+z*#nF#o;CWHrs5FEgoiiR&3TwdNVKIqW=8G4skHOX4olueKi+sQ1 z;1oO<-+GM4YZqCNYjQ-ZF^_;(@>Yb~W{pBZ`(qCC797*0(P@PR=JfpnWtR?smSAk= z^&g-ycpP+X^uq7#9~;W{mWSQoa1ENLr+Osh#))2u>o>TU(4qnTh~5JvwlEd7MBx(I)pYfF6Q4yDd^lks|HIoUP3KXsU5hZi*8 z5z|-Flrb8QYnKh9y88FXXWNk?eAmNC^q|=SlIJoEts7lvJRf*dHx5Ok1;(^0?mh%- z567`*1#8K5gYb=>4knv>lfwCi=(W{Kgon)DO!l48#^l}O@#ENLGI@>y8tk&dqlaYZ z@3OaGy1@cBJ{&}!kGukG>sS%KV09}Ab=O7be%e?*ESS_sAhw*<#AB=dp;^Em8Eavz z{S!Fms*8VbYvYr31JTL!3WR;WL;Ebj7=k8X_~8vO{j4@B znONZr+j$@}#}M0pi}I=P7iK*fgzra;z`zTJm|^q~^$+8H^aW0O>ksk8krDhTzrxNkshN79Zsgcv7!?MlrVhp~cymECNm5s#9!^eZeYk~~UTY>&r@B^~Y!Xuaa-&1DGhgP&`~-MXFvkGiNVE zVBBzPLTYWHLp4E!H(y|gL8l4KdYpi-`umd=_p_NZ(ur7oVB7AM}bW(VIADMeL3b>u)sWu?6tTCv1xuHe7B=7>9)ND+j0Uh;Cc>ukb4vk>lgJ|aALQzF@ORUCOGGw^irUsmh3B9w*~QzAlcl2F%1ViApFF9Ge~QE5bv+m&1gPBFME0z>Lf_kezcB3`Yjyox}6M z(f$%>eholv?NE5EdkyZi`-$)qDUU$^?@2H`H3L7tmBeFFtKrRDcU<>z5H_pu7>!6* z{P}GFUN+y$`n+_*FO99xVHQu!iWZ3QgUg$t@J%8~dhU&dx1NDu4@Ay=^TCim@4$pS zW#_;2!MSTXVaNyx;(TI(2wyl-4An~Z6E!y%9Hajj?reBU-p4y(<@Uue*inU&6;3#; zxQS^|8$#>MT=3MQ-E7ni37R?FON7rIw}Z_v{!1QvExxt zUlNwO0B1k3uiX@ImG~!xitu4m29xPeK9J(Bg$QrF$={||L`Hf6x_&Ar0deO@r}|+8a;1))4#1>Ey@aU=jZHXE`}v(8S(0i^3iEHxrq_DE8dQSo~(ToXq$; zulDEE1f1^rmF2%*z^FE+itr%yt?a^#bxijDG!zrBVK+J*;q7~phLTDr*g=o8Sqr~3 zTxB80ZVZtkMb)Wzs>9J{iTV&yWSEM-mwB-3kLZ!LQ&L2D#upFrJPXO&wm2MUy^^?X z8cqKEjK(tlUJ}uzL7wVI;wzPFwVkhgQR&tfvin&GGj6vJ z`YW`MufHciVU;hMxLhPoJEA~FA^^XJMv+TaS+H+sqzK2CV~JA03J8{o!NZTI5sflF z*nN)1>$?{bS*IY_R~(HM_H#+mq*dMnBX^-RJrE_v<*~tu=vI>g@q3f@6IFx=EJAqHc@Mfi!d)8JuWN`}UG<6_HF$moBbTn}1+qxQ`MMf2BW zzwH7X`>Q`_IesMeL%eZ=#(5jdO;^dDm{1XZb6$Ve@puiHDi(oVGrelA+^ivY^dkh< z^_I81riS<{M&KgZ;p~Z~=|tTp33(vLDBV6zUVV?i_N;X^4aet`j$esr^d{Tp;sZA_ zaDNiUw=H0ObdHm84Y+ z*JQ}zrM#K->9{(@%I2DFBWqThhNr8R*$i7aka=-C9p_bHjl~`xtK^YNu^&Mw`cBT;paU-7&XT zac3-wuZ}1EPDt5k^ZB^+T`_5@=*ygKjYbkyPE^M{;KHbCU&q>9yc{b#+ z?+i%jACI>Oo0IM>%fM@X44&UKmK;5_7uJl8Kvlne>;*|ahZ1*y_2w9? zaJs>+w$BIOA+h*cHXfFzm4c&gD28uoW)8RhW!`U2K`D)LraCDBrmslAm$n}m&Ekj5 zvAPrti8X-si%PJuD-k75y8$Erj!AsW$K0WO$T#`MBqqk=gZh=QZ1+%jaXSW!l5}C+ z>S|{3!DL+Y?mM$=LJ4!kITd$I&4-|kCz*BrvDp9FZura;Ft5KX!OW-<_||xsc?nBU zzpWgejabi!cSYfLk5U-NH>&+TG#by#9f1HzBX-hdLB1rcgRM1#*yIOsxTP`!-c38f zZaNu{wS5*q@wBmQ)A2-%lGBIo%DwFN3&|K*VFuIIpJa2@k}=Ft9~QmRWfujc2-Y7u z;BN7YExeV4Nq;85?0fQLk$oZtPfWl`agyL383g}{zkOi9yd_;;%vsCMim$6bIt- zz-XJ~y@yHkkSM$ma;x@mX$3jqumsiqj$#-eSE8Ajgxx=U*<*j>!BCcui5I?1C^bxg z1rdBaYdwn1oZ$xA@kw}MO@HgxX|93{OTv^>Zp_I|t}r_&2_>fMvqNX;!wbI@JTcqI z`e~{jBGafMG3Q^U;#_?@>wV!r7%BBMe4TCBQ;WsKx258! zfW19+Q%v^kG(6Cx(^EIa*nUsLp)H=)17b^MiN$B{zzZsn=%~ z=cMB)ukxOnCFYD^zAD46o;oE){9!7pJ<{)~M`AWFNWnGXxx}$un|YWWhfX`=iS{HN z=Di&s+kQ{$sk35s<|g5hU0uwAk-JYtDy3n+K?V>oC-_uVehL=ebnB^EVx;1daI~sB zoSZ3t>Qs3WN=Nd0>XaBg9ll`Sod->;fAPFi;;^Bhi@Ds#ko|i%6@wGXnJW=x>^X-t zjMD33I&90>YvWR}&pzXxnjF^YK`M^;RL1lP{KdYtNX34AN~~?q{9^B_q@nKQUrfmO zU#zNr3J#d3)l>7s7PO?Iq{<$ax9laWlaq=c6g7Kla)^_E3R(mmvc6lQPaFj~u3NFI zr*4Pn3-ZED%DAUKhja<*-`{`}2eO?5j@#4kY36WuJO#a!Bi4J|40! z?y1QkEl(0LS)sJ2u7<=mCE_^e&Yt=iV%fk)W~*jTJq$Uk9*1gbAA0IxNXP1UteRTX zQ>#PjI^$6}c5hGZ4O#sz9xF`0_0+(S6-n`Ewo9j{?uD4j#9>S8ex^QRCz;|AEtrS3 z1oKuNX`2;|%cl27wJuBg-QXta&6hGS*%w=<(s^k^XlL|F@OJQ^yBCPlRi~_|(xh`l zwPOH1chZ~Yn`E%zSHF|k&AwF2F%R54>qL0;AWLe~FalfqZz2!cr_(8Q?;+`rU=QY@ zLKiAKVX{*U$yUEb)>rwXlZmSc*N6zCZD&g%xz>mLZMLRgJPq*kbr*8_UI5+Kd7cbT z4Z4rKWj2nHk=@#oRqe{aW8=C+5EqPNZ zMs=qQq^d^--KerbP8 z>c*Z0%SyzBnR4{eRCB!gQUTc+uZexB4@#sLz}iJ;MEDM+zWDqHgAjS*;o6==6QT2R{fnd!Q_UV)e-=#bdBUFdtP;mvCs#yflD`#P*iTIv2Qge)lyzi29 zb9D)%?^?UuJmZ(e--q4o4Je)QUf55%u#4Oy5m zm%dYOC*OV7!4UyhQhZ77o{_-mdw!6L9Tv1V@Bd^{?UxD(nyoRP4xFaCmNL5DcJ$jx1M2dm9Av~siSQU_DO$8;5Lz>uRPN|7nl$4z zOz_pGwTEtzn2d2~aBl!5qxO)6&Gy*-TtS3?96X7(E?NuD27~C-s~YswP(|cDR-l{G zKrr?ql2kF6`t&(R#5~5*0ZY}WHea6P3-UVBXfUCFAtL;NcQ`8+WlK9o z8Pc=EPcqpFQ)ofL72zg(&y%wnO6aT3 zqcOWClJf?pIM2kAsaCAJk5MI8RP`xd2_>pddy~7g-C16xMM29v+DQ5(reFwxXh%hilkt+qXCZm z0MuJv8b_^fX3do5((>Woq1f#TNqRn+eq4D77CQAjA1e`#Pqk3JX(ZbI4FUu-vK%BVm2N7)BuCyq8Xb5wp3bt z9q)aj1ifZ4pB8;wPfjK%(64S&Xux`Lnj8ItY|OG0;R|nC*gT8Xp%?Et(DuKfBwR{^ zYLwa1Z!4m}NWlAB+tTJ)H(}4qWHSDjj|j)f+05BB5yZ%gPk(siLCVRs#C=8>-RPD} zCLSGByXTvfR`JUJWhwP`&N6Y1C znE!loG}?_GTa!-|i@Qn0s_FFWxK?)k^#N2~+l@Zv8^D-qCA#^LC=V07HM^hdOV=Is zq^tU?z^?lWH1g|o`eD{ocD0=tU2C?0Dm=N%emvhn_};!Eys{>Q93&6e+qDVwR@)p> z(6WnEHHK5G0k_%ZP3u5DFP4tXlV!&Be*z9mgJ{KjZIEf`gi*8Si}1BYVPxXOqp;d8 zn6^cqU`JhS18vO!8smSO{541i6A~zRjz5W7|4+Q-LEa+#Dl?PpiJJ+2B$kdpe}nwG zDF#1RETXbgyx?xgOxWoaOWW0(;L#~DxO^^%8ejYkd(=PiT+_f_w7aW z+;JA3Xr{w7O}WWC+y!mUxXWeeZVv>&xP?8vD8QJS1ln= zK+8LbPU{~9cD^^*9q9=)=&?7vUb>U?sST%>XAALlW{1(; z1$$ws_A)ZYCxj}!dkDYO_{^#DAo?Wd2AF)`0>_3e5aF*C*F%VfZmrMUIBK7D6~@=; zK=A7z>KUKH-aNFJd|S$=l}p!=>8@+Zx*K8C-~Sr96_ZRXt9(WH@t^Z-6#mo@H;o8N z{Y*)pk*CQ1>QFjLYaHkrog(l2Lg|11F3Yl>@3JhPX05 z{|c+I2zvThQKB55Z=%Y$6T+# zyiskCKJE$}IoS<%qYgu{MjI?rJPbd|yCHqqW~j}phY@C*;kjZPl*(=f^{w4duqpv0 zmezyfnRqbneg9LYgy1FhpuOG>qCPwWTce}Q*Xi}}^@jpf`qqQoJ9#+X z@(dKNA7*}Ce+F~S?4UGJaNZ1g=m_bCy~7+ZVSpG8k#@i(o{A`WWIV2x6GP20#&|$d z3>%BoaeHSsToY5mXYJh(Rx}v@)$zWqBA$&Ij{&*rxaF@A7OYgq z?gVwLNmoa+?~3@_N)g!?iugpJNt=&H={jSa+-QVWr;X8ghB|s3F~->IikMSjjLW8p zWB(|1Y;X|Ay4}V&J+vLh1*_xDgm(B|XpG|{ufUKE#%P!-c#dm~(doV=&VOcwr?ZSO z?|~IQ)=|f%TUI!yS`lR$tpxj1apaw|!jpOJkWpiWJFBihb+r|KcqzEw`>n7w#1aGE zIHJlJBP@UBh(p_l;hzVNxFuf^?QT2bXiGsHH9DeV@i6>&ekP6xRm9osOf)>!4iC>c z;vJc*pjqpPVlUc3xpF4nPq+$!r8Dtu^bv?X>WIp?1x)r@;o`qr;N$^E{J3TkYJGOZ zOD`v4>qCKGn*~}l&BV;hhFExICN}6z#F$-vSUK4OUvKpj+#f^qS?`Arw5s8B(M;U> zu^RdW`l0080?^tr6IU)yhCh29@oQZw#O2P!9H~M`UEqh#_8IVHmLE>r^#$ZUPRC`x zav)kJ2G7Z-!t2Obl$`GaYI>14rNk3FWP@14#TC$3mVJL%z*A=j%P7+^u%c1-TalG@UFMi)BhG8ex!Pszltkrde`-yV6%|;fS z--+X+Q&CL)-WI5}8^=7mbPa|p`Z7gTwXk>4A*SJVH8?#efuNFoV0Z5@T$3n)_?A=f zIinCHU%!E>_BG7yF`dxbK8Cq&Dup**j$y1^WwA$(WOcB|&+m*r_8ez*W+e9Xb8UPm zR(4jy%3n(`KDQReK8nSt!fOzxvjl(VwSar%M9g}lhE8_?&6aE7=6JyMJOhkWaz^R6 zAbhJi1+N50V#!xqY^{#PU&pQRyGsn7YLmvE`*@k-zm(4-(Y~&XL!mBw&bM6*&^~Yv2%rD)6(i7U z=~fPfx-d>uW#o__w~|AlE{vzsE2U7YS%-_W(zNejc2|cH#$Qg{Yv?&nh(cW$r>|U3 zLeF`HDAa}VoKCBtXS~8V-x*f|J=Y~fp)Sk^C6$AWW4|LD3Uy)LL@k=deD7)dQLS2}*fojtrB2=Es=c$+1L9CAgheBPL z_YLp*qJ6eFheBOAKRA4nMXf#(+`RGhy*z4}NOAMYdQAl^z15eSXU zH;4}VN1t;z7_v^JqwVw%IJ!2S^P6ck5j1!4F=EX`unp#8y{i>i4OonN>#bnO<#5iH zx#R(XYo?&5*bI0$Z3@>e)D@@3z$FPw^jI1Lqb^u*ev9M^V9-z_T%=zBp$`o?`-7G& zDxT_x!zE?WGQS_!F4SMXk;Bm4eeqR|92V#F<^0ZEkwbM~XlZK@RhSS*7t8zpgLhzuV2EXmoG-&VlKNM-Dpngs)Ml(}}HuCmAiDt(mj;ZRHX zo~Fe4%{Y3CS${|hU+Ok7>#s?1_THGSJZHbB5YV!dr%~|~gzZAT=Vvb$es6@+>&&go zHLq}f>Mx^sX$Pyp_-86lst#r@tj*HO(0WLSUelSx3eJsV9KVb@Z$PVaA+afjSF3*%B>g>^aNNh?J)B+^zQR%Pn`Py7Gy0H?{eN#D<$6z?Fl#VC1*h6?KAD2(Ifj*lSb9m##ENHNE z;;{UD99W8t$K`hlKyivOhl3u=;_+Hp4pX%h(0zyup8F<`%Fm_H$W9I$6eLk9Mg}tq zB{*E@yaGC0l{oxsWC6O96fwfAi7`AT$stp^m$$pN1%&8fs$t`j)d;>$%XprBj={Y( za~QwO3J%L`i{O0xP7dE!?St>K+u-KB!!SfTpTnihODHaoWQ1tnDTs>+Lv3zq3F2bN zP@68kJoe~9e0NJ9dyW(0lrm%N>DR)?413NaM8lM5RL-y9aNwn6-1zt?G%QR|DgtuJiC~~oK2q4QtrgzvGIHeEE|t%Wm`c^-Gsw&A#xa&F2|v2KSh*^l))2C z3ivuh3XfLFVV=1J4iv;gz(H{iRr{;}*zVFm%7A#Jru~K+LAxW`3g-W|)`TSYDFD9$kn#HTALQI3eyBZHztr&X{P1|2p6QK-;ir zd}4QiLkq`b?4x`fHttG8S(Qdu=bMB<5l=W=qc#Pvl@CWDUWyg0MQ4t{XjOZ3(=p~S zIm;c#Sw?Vp-PIN6F6LvwcL)5jDGiPDd3fS?3Wwz_(%AFdg_xez4Tes0xOgaD`58W1 zc;a*OEI84W%%ON!Fl?TaikF8vLwHF7DqpdKEB6+27%MX$R>(SYC~KMwmNTu8Eh_}~ zKPDVbm5|3LGvzs)Bcp^9ugc*3ON!XkUkV2p%i|sp$0dUI4^kq=q2b--U>Bgk;qo*~ zNbwfTV%Hupp+h7%oM_U>s~i6qglHn6!rMMTux^Ol$GcZ>0{-}UG3)g!IrO?&1bc_> z;ZXRwXW4?I(DQkz5LNYFL(k`=LOiqJJBXnoPx!g1@N-ll3O{cZqQfpD?CE#PAT#Va zj}XHl8Z#{_Ijkv2Li4y2P<ICPmSQPc;YO&_MGTaqKA8;Nq|8lwi$BxUKBO4{3n6V*kYH;gA>b{$n>RXsv*#eqtzp zbsaqKFNS@ZeZf;rjEi5lsusq>M2w5y#p?{=iH;b)GxcH49D4d+;#hNZ0Fy0sj*DMm zT>s4SgBv&NxH!&TYy>Bt)j{~Rr_Amzb+Aey2VR}2gIbyWpjB1J#r3Y@E1>(pm5c99 z+Zv#5&k7L6cld~xkkMJmEVWfaeq1FZjAJ%R2}iC`0n^tb@$;#F;(2ek6aF=Sf(Ui*vvyW5IS^ZovS#!;-8Q9V4 zm`5|t7~i{8ddMbHpzw8lAh|Qdwp8d?KWia7-};Wj7)LndimAP>H1np-d+Hsx>f0eI zBjXMC!u9M;lft0eH}{Z0^h`C9d+VCuNo{QR^s3?Kv+yJcXoG9rON@~4Lz`tcnW*A|R&i)h4YR=V>V@r# zZ>4I$H~~@*y|5o;0K0THO1k4nSfb-sd$90zqsL%#s}a3LHosx@L8J~$!qwS?Lt=q0 zygABfqZ?N%*S;p)RVP$1(agd0+jn0M!SjqxGlDg{?|Nvz%YD^@l{V)eMoCm7Gc8U$ zwY0k)P<8|O2B=u5))&>a;!?oa&3?EE!z4pV8@TEA#wg<&xZJOm;Q<(YnWU8=9Xk}; zk`yscnz+ytDJ0Al6M*#-Q>O3$ItR1uIyoxY*a%&Wx`1)g@px1kt!dl&t4!v}aRl>j zhKy%XhFM-o$bTmhx4R4%cW-&LJ?crTk~;yLaoBzy;p?Xi#4cl3Z<}Id&hMidtxNuGD0AvID_N|cLH8Tg>H_hNF>RBTb0H!V*i%I$ zq5*zoBH<5THXg-OGj)oWK7QK^_qZG!0b&+H$1JI*C0-eyZI@x!g_s-j^N|0@o1z+l z<=UY~rFprpRfnw0;4TXP6<}{xD{tJNe5UzUQg8nqY|pZhsmzN*4t2;!T4#@bz{&JY z-H`e>9!{bD{J@CE2??#^gEekJT&f!C?PEcZg=_Vuk{TUZx8hdi+yzI*hv-n>Ce>Y{Z?ffD||H7G*=vGJrQ*QqJx2VBW|J)@(T7r}K zA^2QL-Ja+h$D_WL^ME5Lo#>Mvqf5JD54F!p+-VbxlW-&W&E&O>bm!Zp#6}jq;SJdgXf7?pQ0+XQ zF(Q#=!~y^$dh)m(xGhW(aoZCUc~MPn96I&HI4Ho$4J_0kx@JQEH|rJ8CU%J%e#--a z#+`H@z5nZbg3I6d#E4tYYN;lq+Z)3L-uSj@I&mur#AdhuF8Xr8+>>AJ^Co-zRsOe~cW`%65Alq{V`} zw^F-(Ocq%$a(1euhHwKQCoXqjJnapd^_7C&5`Rmqz?{+e1}|`o0+QJmf9+zhjw%a2z0QGZe2jG+Z zz3P6J;VeZwz`cuAF7g z&%cSi(8#fNxeTtKrxP0;O?EE4bb)HXpFD|MjU_IwHJqy*Tp|`u98^P0c-UjH!JNoDm z0m|?9^{_!Xx-_BxMRkb0$WOb9K&p_qzd=GH!?NQ3m_B`cOD2y>UI=}Ll8~N}cmJYI z?qf^K8q17npWk9>dJ>>8Q}2?^bGG^}`64VFZm|-_J~0~`-{X6x=K8oh=o1GC!1Fpu z7}Utf@68`n(l@V4kOhyC8dc3qD-_lUPLifIm+$9O^)CSLM9h7iNXozy^>Sfy z7r!c;Bbq5)+srxzZY4m4G)60U`G>T*+;L$D)0^aJt()W=*V?i}OUO0GLHtzCPvI}l zgj$4isoY7c>_P@(8vpvzIoDpiiU%|nYyAq}Mg2G`BR=(GC`4gC4q{@6$fJedV-Bj| z+{Q;^;U;F$yc1_&WOE4tS~!@+fz6u!6HV#@u!~u_es=imzy5Q$En2^}`@DM~gs}b= zS`S?v?IVl&6waAobWF>(?-?27VP@7J#|lJzBv=bhY9n{vL8kuzy4NF+RD8HZTxq;K zez}FU6hm-HcA2JFK&lbfk3jJC?aSU8LR|87YvqY`#I)jE{dyVkEhQI@ zINlHPe+@V;`o7(j_vLcUA8}W^hv<3*1}eQ8ZO07e{q1Ugfm_sgA9Y;?oyiL*BVKY- zED*GiBG3~M2t(wa9S=oJ00$Q|!lN$Wy!bhBxwut?wbD{Ca%^o0IuIuk~Ix0Z2tC5dp)I}+Oy zAMud}p9q3$A+s+(ZY21zae~&Pj=RA$$u#mTt4G1O|E-Zx40i+$zkd|_Zf0IsOUxGz zMY3W&L;KM8sjcoimiZQChfDP58f*mx*LZY_aY6cF@)}(E!S5o5^&dX^)NsSMJ$pAMP#*{6KaA7+Vy@1*Hs!YA*Q{z4O8_?oZ6vZkyq-BfoiIoOokpTklm0SR6>?6HWfSovpDy_JHx|r@@suQ1^tSJjG|bpa{n!nAUlEKA`f^_6Q}<~(qVvmgd@Bb1 z*2lN6J24qN=&zrH9@8(|6_=Mt2_*&sd>|~KyNv~@;V02mp7f1YlMDV1eKmWEsRS~w z9r6S{sgGdMshwtmXv_;=j!XK^+3)`3Ml_M0(kSikJN_U5L&@tJ9%m#RbyzI3zft0T zI-`5JWa(quya%~JQ|2r;T_0<^Y4edX+;7otb>ksbMR(X%G?cZ3m6NV`pvr;NO}2|B zpUAoCO@oVEeTLcZo)(b#_xWTz(PX}m_ohTVSAI&JA57m-4cKpbUPSt6butOXjCTsf zv<{Hg>vS@#zB`M#8xX2D@$a_pn5Xx;T%?yG>F8J7w7iJ9*Y;pLFDmay|{FQ>auS7@Tb57HjsJI^*ogb{5;*#t6)COT_3kF`vF?=z4MaS$N7l-T} zFsrN&TGj+v%+uD9V?PY|jBbBGtneFJkL4;`k2#(u4|&|J^Qqrsj>@>zQHOSZ#xF_Z zd1F;b%^U?Ibo$%U(ubX^mHHXTWjvZ3gzmI?UmRYJ-7V27gkBAKhk&r7u_Y?@KXXNTeUX4FT*9XFMqYscJ-x@Z%3yb_m9}^yx{>i?COC_ZeQW* zl1p0O+D`RVo{W*5OXTgaOL{{BnBsP^TZAziU1}nXC8=`T&aiSj^^(m{9R}KNycGSd zP?L7F?YE8bAW==W9Sg|e2Bz@z*$Q306FG_Te`f=R#a{|$p6~!OoVyP`Zs!2RKSP;> zdmNz4oNJNO8^c^KVkp1gsVUO0g)CS+)c6zF42|6F2#G+@e$&{nj1p|)XI3YP??kzX zq5poPR+&WCoCJ5l?Vj&fh(8$Qy^wz?9SaK>lk;M^f2AVb>Ox- zphM-AGqLBm?~=@yT21dmP{j05QdiG0`$LBWa-%vd^^zpFMkYUUq4^T`03KV%k*3Mh zSEFeNLFq80IMkUJWNY-p^IG}Q;fLzAk{0Dh>?N6&v53~OI;sf4>yWAlTSV!g`BA%w z(*Ye-hoE-6RuuaYc!`%IHRE}qYX7&0-yy@Ue-dmGi=#o;>lBTn(_tp89Z{ZWCTwgl z!%|08=y1qC-^|wWNbN<#u6Yu&!uqcIw#TXIy9Th^Dek)tu-7T~yWW$zu#BfJrdey_ zVG(|>Q<1V}lzD6AVJS3Z#i^p>k<_cB(&~ohxb&B{Cy5vMm%t6facQ~74e_zfFBMN* zuSV?_G5n5&in7OdUf>GTL*e;x4af}k{7La+g;w2?-i=n7d283Gj%;4CuN~5u^7h9} zonaY3<&2pinvLq=2SE~>m81v2Q4NkZ{g_{cxt++EAA@3d6@eS$nAb5!v6XQBHM5c$C?YF`C^YnR>V}0*z~1orS&+VHBj3)R>`SqrkV@kh@#sRC+h&Hic2eO0aRrbI(TEu@9oW78Y{GJ06D+7b?^X7?-sBcSCUi# zOD2Mq2Y{R+n_n=q7ECwhvC?vbL$}QFuZ=VB067tzL^h4d?q)4zb{f@yoZ=cyy*>xJ zZ9%}2o=N3yuS1Y$UOTPYRg!S`0n)ZaCe5$iN_2p@{>m?>(COGD;lAUqp0VxkY~K8J zJoFNwiR?sGi((+_J;2)jkV#X1svT?Y^~12}G<4cxdj%k_z@>B7eXQ?U+fJZ1#Bdfh zjXi7)0M{GV&;~EW9Ln6h%@s=~=yS;O`y`3e_|7iE5W6fjDY56kYI_SHE-zWA+jFel zY$%CcHZxpi)}kO;ogcFBPlm(~UyVn;CS)38+c48n`{=J zx2TiOnd>WbF>+ZA3|_iG^=ql5v16$wnBh(k7dh3YD^C=C3g6;Wq+5DVeHx9mU(TXW3Af(Vi7pbqpn8f2 zFOgfVO@Uvf-JX?R#xBS~Ln5y~12u*Y9^O5~KB~UdsQr0W&C@RXR-g;&KFckTY1g>Q zD|${{jNei+Ec?;%b&<14sA-D1I{CCWTp&D4D%AxQIE_Eh};x=^`*iy^t_2WAO*P4ZEHa5l-6IaHz; zt4WjB&PT8IoDiqO;&dw>kFJEtiq$~yjJ{7MxWJg8}>s{RC# z_bgt?+gfT$@l;-_5a1GC%&!_{)bu1+9Jwz20NxjP)JjTuS9*l&r)kbYQ!WbI&ZGUz zHf6>U8<;vVwa}TGa6BST@man)uN-_pz3NVuk}m$vJK=O(&|p}~I5qwlmL{3}dS3EL zdZmD)WMbweDYQ{SVLw9Rr{A>LPYf4zXUUHQ+xb$%GRq5OqU1F%H#~OHQen`j{n(~2 zPtuc*=)Ni@KTQJUZj?BCttI7_Cfs}nl_nA@j3*K-Y+v@DXmgI>xX>QbtIRMMJHez_ z@C^R;!jq1aI`ywBPh_~KWXIe1fAwQr`jLak;_b$mEX-2~WyO2l;ggX#*0}dJ=f9I$ zf+?=I4Asj8iMTsZl%Ek>WEIgIxAck!$xRC8?{p1|J?ptQS9qJC-L2m}vGqvk8QLs< z&{ht8%b1HVzM-HM#TPYI|IfP$s-LKsuyssyN*k6t{H-{-WEmLeVPFumTFt#%p`NCd z;R<*1Ux|7CoOYlZ=gI39o87H%r0qSJ862O+KgcwUGng42XYo_hq@2aJ>@~biBj)+5 z>QpMlWQ>P=K3|nP&Yg#%uB*DJ8@spedvVJJjm3Ad>^Jz^x~J8Djb^MAmX*h{tIAyU z^s4+R-6*})WOUs@@A%T6$}UX>17dtWaaOAs6x(}}iu&c%%!zLAXB>*H#5zD^fE71g-YIySO zZ|qIH)zO#hJU`@xN0UdV0CCYr&45H`D)oygM4RV>QJ4B!rD*Eik@cgL+x5;P@(a?! zW0>D>Q?-`Yq(J@J_liNEq$>*ED*_AYCKFSuYyWaw+cn$_U2i>#x?QtevqSP70-6J5 zYr|$(ZnOYKWd*bF?&Fe214GYt8EM5M&6Qts`^|u*s(({A0C73z|7NOqrr}3qW?t!m z8eM3OjW@6+o1PtxCYvLUo64q|KknBGFO*{~hjTYn0C00)s%|Azp{qo==hzrx(1q7H zcf{4Sdq{PyYdCl0`|oTy_Zat|vx7VHTV_c7EJ{G*YK?U8g3%2=gxJP;j&7=5XRXkC zvfYH|FWo_+`K{VZ%qxja#yg**nnl)U2D zWE{Ob+GhrQrTeehSo$Zyp&Z~TTB<6s`xxg1uUhe1`Gu?c-6QWy-Ycz5!5_sN0zii$ z;`0%<{)-X6^vr5xA16Dwo!>maPoj(pJvT~{@7!3U3f1=o8EPyYt695-h^Gkwk{)Kk*BVcCh7 zL4)gT$zSzINfXCacL=(tI_tGoEKa?Us=^?riN7-w{DZ|O6$~@$vp5ciy=9o6N3JYX zh`yk&ilo>W9CEQ0{1B&ZK4YUFuWmNuBoepcLSg%f0=&Vu7%gzyZ5K!AcpNsP&oFV1 z!@U)cwq>t>;?Pg^WZoQ`{Ea#k##mhGipJeRp)_c&f8yl94-J{IVi>rm<<^R)9VDpB zw)sh^Xf7D>_yTG>R**!zF=p0Sp$nUt(0&>+7fgRdc(=u+@Ln=jEn5GOK(5@FVup9y zMdPLtafew)Gs*){tn0KR%{_jEXi_m3hh1mbv|Xo2O|6^*mP;}`_$Bb<+Ld_PerSH7 zYm;F3c`f80pSl^hZ+1`7^_{qvjyo}Ct05lZOQ#7Y9UGHE0si8ekqGerBw?xYlSti z2FT^Esy-UHsk#8W{y}Z#w|E^l#w%&>H_@bkZ}((X+GSg9n`aBL4Lk7V{fy5GVJ%YS zi47^$Q>h*8X#%FoSrs1OP0n4H9&k-&cnghel#Z9S+B)(&3PKo)2HyXfYjTv8*Rc#} zZtQe~T2dWmv#6I>J2+5pRk2kTmHliet*EcBsPXdG3TtesUtRdPm-)RyDK(pl!@%9w zv!s!}bz(u_GEto6@t^Rct~#Zr$%?U-&XL2WY5N48a8+k#`-0%I{x>(>0?6teTn6We z(k^#qi;|s1M=!kt>c~^$ctr8E_l_z4CT(YYW+E~rd-t%k;_)!6dg|{b80O4X0B)jo zbAD*qfu&EMR%^C~H&fe-uCm9c1C1t{{HHIqU?b_Mr?jVB(#Nyh!lDbLE^6zn@FT6F zVkt)~6g+{LPs!i>qKmTAZKL+>za`MML-9X`br_w-=JM~2Q6%P-CaM->N(lx;LZyUH zc2guvA;dVbZbx>9M|K9bTl>95$zhMJo;q`aO##gWz+53=p_!TI8PPEwD0q%GOL9uH zA*GQ|^BD2K=$i?ordi={oYK=LNsn0HBBRN7J=qO|m7Frl#na>S0$Dwq=Ck}f<;MuJDZ z^Jw2jkLq?3Z_N05dj*T?)sc-RNWmazT^c|%(;+S&Bbn*VjHCa4A02dCocmLgKdF6o zD6K61pF?(~>UG&lH=3=H@yQ5iDaLUw*YVD?nZwSG<6FnBF;GKm!>&v1>G5DZt;tjB z*Nw1Fx_aSCUW<3%ujJoNU=0pmb*WktFVl|l z%LeATBDbSMkdU@&{*+ZBk9r*o$l7`lQ*Yu|jf0&sajob^d#9&^ z{mohYk%JL~g99ZtlT%;%KU~8Lf;!i`?nTdh zO-%Y;loBOt-BMCNntNa9Ccp)S=c!C+&D0%w=d>I`Hn!AsAw!grcL>j&x903 zEL4AjTX^J5fVQR|Mh8eV7%2O3d5xOB|F(d($#ar;$2|3iSw!eF<&)YEym6RyZbVY zQ$R%8vS>V!H7-zFCGdkXF>{8p%d&Zdc>rscrjCIFo#W3X51BbRM$txOt<6v z4c_T3n?y`WA)n5@`n9d_vMx|AC}EEkUY=i_A1kTn3*C6g+EX_*HBB}(FlFeh=rrsk z1xp?@kJuN?Ij_<{#KC)h>gP_k(2*y?Grdmf+t-nyA(q*)TDV97b3u;3?tSNO(7 zex8`e3E-!ke3U57>1kbQ-!1;ygWvt&{p}T`-#h*6!y3qc?doL^-p4b`GY3ELE?HX% z{3pQsKy!7rU2rTt)-@Ouf>1_`zha8Fd&ZrJ{AVdYWG^Q5=b||Q=rxsBB(~$le&*p> zhA9r2v<(Utea~(7x%+SM1F||=CzXppouye~_hvT?paFSJo$mt+$carx*#Oef@Ypdu}<4b@bjI;u&)zcqB|HLm0qN zs<2FWq1jJw#A4)Zly-u@1Z(NL{Rox@HwVg9X(Ebx_b6fB9ZhJ_nfg_}yG( z!(12yIu>4F#@i3W3|+6VAhHU;o45W^aS6H}+=sF7`I}_1Dq& zInmudoVRUc;0FbU`}Us`dMDodSzQhuWI{D%hg0rS+omf)qPS3C_kG0Ho%-h-fOl^c*^r6< z!axoWHst4H3K`3znunTy>0fQJQV;6`r9Hr&+g^PGz4aGZLi%3%nnB>tBIf~`cTw!_ z?9P=E05;H+V=BJdM3(>4ww%_Zih;mRPQ`LXM@3+!n9pH=R-!tU&`Gi_sA5RFw#K!V zt=a&%RPSYYa^y+qnK9#7d8R4d8hBQ+kip$~{jKj$WE7S4)J-oA`ZlT)m3%dP|Yt6?=;^HF+c@Ldi-|veb}J_iQBYJta{6Laip8D)pB;Y zZRP$Fof}(NIjEhm}2^>>xLphtO7M6{zb)Jjf~AW4c4QY|a0SYuCum z`Qdz4s#VG_2jST0^mKU}x-_?J-etK;Bh8cB=5kd;8++O@eKoNwe#bol>~vDPPBBW) z?X({?5kET(x;re{_~NYZ_niM7+_JWGx2khzaF!Fac6O(?f|zWOfz5Iznn3rFuTZDy z?jL(LG1vT9uL|E0L`g{?HnbA|N;TX~CMQCnA1nvHr)_XJ2dYTUKF=2pu}HkW68oxG z8KpJZEgb$gb5%g1E+p-HUp`eP@^kVzeW-y;Uxsc)bO3SvFLpB|fbF{_{5){Khk1^hTK}uVZN08#5!bD< z+O*D=tsBDHuBX92!a@+y^YOXW!@F5nj++pqP859g*vh|V+QAe6%dWOHDgd*bNzDbA|F(nl|NOMZ@ETX4ZiZD7(}PmXxx@q9z0%U%nCV$l6$4R*(Ep!UUNPhzF? z>qhE9LGFFi4*xXhO6u;(fAaKv1$M`HYyc4sIE(%F{~ce7_G8t%I1!h_dhx)#<)HC7 zDIkxSnRwl%*=P?LKAyz&>&8XY9bLJ!+h)%1wa1{2>yG=g{1@oiol#;s^$J3+j{p>C ze6BUDp7^}HwHH4WNw@U!Wt@j40sbVoJ*OB1dp@aN8+s00?~hA$Vj(L~7HQtN{xe;W zS1#(l>k#ih>~ZhR1b3Sz?Dpzq<(z0pSKW@e%4&;ajrZOx^Fzk!LIytUj?Ch^NN?8q+1*mIs$Z$&JI z&&@+8-rda_HrMz3h%-60w|13-SoXde++AHSk$(Jzc4q%@=)iO@vvd~VDUb)qBVx!H zIZyAlE}iCjG8)@5N&Dz7XFXSbliC{F0^9%`rK|Nl2SC{?YisNr4p#%Bv=;$cD-vId z%JhVqW(FoFA}`srp?JkbIfsW|ijGE0HF1Q!NZd|eevC%Q%W;5ob+5&}CxyHdHSjz= zRk<$BdCtu2KaK70FYX>5=1I}`zdVb3;b7rD(#`xFnB19}7#x&YIjPaR>~1MOPvxo@ zxDFYRW)$FjUc*sdv1lUuae`j*jfWL+w)(G5{J*X>SZ2ZWYs>#+{?~jh0DfIOP8fCt zaU>gpRINuAY^=A|-i-a)@5;*{A;q)j$ZC@mI)zj63mLY(2_j@Kl9x3i;tg+b3}+?U z`c$6D%uTG9BDBmyOISHTS+C~s-rPskfwYDv%R449z`|eb>F2}sEybbMKL2W2Z`tVH zlsnrPovrHgnif?-Y0!Q4r1sU^T%66-LM_Gk=^oTZ!YAaPWomQQ2;_hLVzB#I)`?99lBv5Wix_ z&AAi}$1JJJv}bg^s%!I})z|uIJCNj)5z3R&xE2Yb&Pi?VVT@)Ns4ptGS1Z5Gm*F6> z$=rT<6LYbJ)mzGWW_j3MP++B&s);i%&iWsaoi$ zJ31huJX%ce+LY&&(_=^T0#wLRKoUgSetFYkzxZAKfzT2BK*D1AS&uMB-^&!SeYLpv zj?L89u}c*&EhTY7XY(X||8OVUM#rhM81*;ZTeYdoLw(}D%;Ne`Qg*x3e|Icv4{_MB zZ%v=SsRq9Zx)}iBY@9Lmm)_h=V1WLqsU~vI{8W;CzQm0r1h!Ex1wO~;VQ->V#}Sb_;73Y(0wb6mrY%V=|o&Ptnf3f@vujWAY`bzq)X9rk7)slODC7t-~PFiXrclFiP=4mMFgbvga zl)LciTKWwk()hA9cALn*Bt7Bq|8|}cb3<9Xu!%}dP+lbqr~}8NOHDhWj9a@1&fA*H zH3@&k{$zMZE1A-m(zy6~Sg6RX(Fk@^NIzm^;`*&fNt0D09TI)ADk#Xn#U#nWv~r_J zP)<>vWt(M?l~rDoN=rfUZ*G2stsJ46=M3FNDc~_(6WR;Dk~V+l2^1cYDA*v)bbdba zPTh8q%W*Ee(^bA%GAglqifeXW3sR@z`Rly_H+exZDWGgpcCFbwgIJB^1^JJ)|MA}| zpljrIN^2O9n7C0Cn5KjyZi>N+M>n^A5QwRB?cCCl=cH+E>)g@FjUO1Y`u~`(LHZC= z?RAIjeGtbe*O)ic38N{x=ex9-zOpfgs()>3J^1mNLsbIjBVKhPGKjTm$1Isu;`0~dh?`OOh?_0PwON?%foCBcMK`D5d8u~U zgUUVj=MIuZ%Tly`7-qS-CkiW|*u`*R{1xqp#*`(ur&yJ=e8$Oe3tG+NE@h@+~`x zkc<=I5IuD|TG~sgo?2z60U_*pn=>b9jN^x;SEI%5gzWeAw;U=Qe!1zAjaPBend+J7 zNVyjQKW_bK-#x_9)9YHS$ZiQrD|Rg^*r`1=R_W|9_#_%tE|5^5YhbV@@^kiPV(nB4 zgrk$TymH|n{P27|8d<90X{k7eu;1HXtSBAq!aLt9u$-iEZ2|kpSzB87U5}do$k1t? zIMndG5gE5CrMaIA2&>G@KeEJ}OO>N42aa-F9ggOhARI=9NA1dvfNv;H9kMU?`iuuy zQH4phh16VE+?}dq{*93R)%LwDptS7_w`cuI$<8sMI&)U5*djUsG^s)Hi9S*vrd(lp zjBp34>bS?lsF-z4b%Aq#1nLKO%uG0P(}Mh$FLREzNMA+~dQwggPdptjS3_L?PRPu@ z#yTTrc%Uy`-#BJ8bDF#GXI$GGC;kN7Aep#$9UyEuX8NQ71!kBcY~W1w95^7c#@Zoo zOH?Q~R-CIyXRvv6J|?i3r5QKJsef2v5RFXj`E-~@D*wo^m0efsEOZccI;@(ahq=2*iU|dt^6Pl zYJqRtaGQs5v$IridGp6ysLa!Z2Y+6FB$>SVtt{kn63Ea)4nQ{U%`QNv(k~{!Lg7Wa znk7Tl#0{WmBdkJ$hmduH&B(fc;-Uk{7~YC}Bum1$?EVNqBMCJ(BUxWiVC!zeU52ew z3~~afjIj-SB)a+Ijk;}uD=D73Ti3~gb8`G;0MRHC@V{?xQ3irRnE)*I!0B5vwyWOl^o}R#N{>fi1?x@H}2`l=I zIq2aivqu=MPQ}Q6-j&F6p*cuL9&#Y3{W&~gZ!mf%s!t@9e|rEf6iKmLcwKlbviWzT z&|M9Z+R9{x_>pTZYE@63U>gv>1|wYCJAW&XrH!DwMEuGNG;u18z0spjJ70+De%oCj zK@Jc5x+UXt(M>Ap7fKx*?_*Jkbj4?}tZn^&5ipG8Q5 z3!Qn|ZbS8IAJ(zYN$1}{QRlz6vUJmR{_6~jk>hh?%;(<;B0ZfC3U$__p6MaC-!VJ* z(xFrhV5<3lKytqgai;Sh!sPaEo7>|)7nsLcpTXo7uRF!xFPHQFind;ZYNQ_pMm{4~=6x+s z;6d!kqeU&04r%u+7Vm);NjgkR_P&rsvtCdLNP`y5(BWPioTVRTLnpht&a+7_$FJuOHWZ1A2hLpNt4m zgL8^Ee)VPOM3|4?Cu!FYzz1*9S^ofy%)>9yT=!GZ?sBH^ClfV;O- z14&N*34gq!9}^81jTAt!&>MCP-itbaew$C%dnM-+hy^Fuc%YY%MnhoJ=SRHdm;1aV z`^SEm`CO=*K<<1kNgWAF9$F(eY0GLHx!G5EZ^a77u^um!;2{Z!*hCceH+3V$;Imo@meH-x|QyReGKgQ^=Y>y6?dK@x=jLgJ6_G6zL$8O2LmhQmSvA88;xZ5Ds|ejC%pZJw)(! z0d~nE10u|U=41SE_f&(_ebMs&Ox%_59(D37T+96biTg(K!U4M!gA#r7U45CL|9^Zp zn*~akP!M4W-qakfia4Onbdi9i(#uSgFC-2+FrxeHRffBDf%@jWMp=-x2(+F1w@?%e`4fBJOWM zPP+bUqq$Z6B=*$Z>ebutb$y!oq&a>krs>-Zs8=0J>ux5bi8wLU7%WIvy&yd#q)GND z<2Gy#&1MB>+Cxy1r1fK%bnja&cxI1~j+z?OIF6c#j;dNw9EQF-RqYQdbGBF%>pBd4 z$FG|I^^U6ff1JPH;|=RIIG(c8d>mJhqfvEl2{^p$nY@fP262RSXQ@J76zVYLyMxe< zqPrD7%wI9EN`A<6M|1xke1_%Egc8WGE{u{B-;MF%9FFzOj{;@z2}A>bLUWHc!zY}V zMe|qe_Wu7uYrKdR!@uP?euw$0_?O;6V~5w^OshU_hcOt&olskA4+M{{u|8v=&YtS2 z8ZXdsJFEpcICmftGN0KIKX03ZJHO{Jjr0&3A22Wob+AW1&Jw!Sr+SKwe;J6ZT|C#J zg4*?wIn1ZUdoJpi{a?Uabv$0?-`UbW4NnN zURsFO61qheznGnnvOdEa{wHH2;N`|GE}B6Dsv}lBo{!Hn?pDKXEPf*{T1FGpyjAP< zJfrs9+(;^3KRe-f+)piT-(bn(bv%#&nXe}kzqJ_3IOzw9U(6-E9`C~^+K=xW@}9>h zOwECq^Lr=r=`>x<#XRCib&dG{V2jwxzc)dBov1e?pSr3qq=tHbv?rJ5J{+~cWt|w` zzc++S^c)*gYNMNldOj^|e#^`R|G7t|di|pwz8fbdI4&%6`=cWM?9O{{&vi2V=iU&Y z$e(!BmH!3(*s%NU_ZTA4$oR9{WYZL9m?9$NWT3as5&Z_3+QvBqOinF*P41br^*xlqM)f zpfpNx@m6awjAzk|deQyN3=1%ZNg(56ETeZcV`2iMa9l3dFO3iN)1Qw8QeHj~(Z~}G zeOO5R5M!OZ&rY-y`-vyHiY4Wqa^#ueqVV%IA$qNbgz)!Hq3=n6=Yjc*pm^iB3dzWMWH z8^6!)E>iQ@_6EkTo2^x1-+hzW{fFem#%zqKWn3Pm$B%M+&- z#k`rUVxW}#banjAfeWK6p~uPM{D}Nn1VcK3-Z`;H$l`pB{4$!{0)$)7AGydJDLBZN zpyb)Z$VanZ_f#jEoq6B%W$2yrS_u4^md&$r(PaPuI%4H zxSkV5b@gxDxz1}*ztTj-_nT9>&dW=H{U!YS`G@(p=tO$Lg~ zeB7pd?7sV)#_ug$^h41uyU;G7XvZwbfyC>=A-@>`rYQBC$U#cofkbr${}X>w3XVma z%?w^+1gxRuXLb7#*Oj9=up{TOm?>gcNr!kc*3~k0%MejB_Q(*uri=xQ%$xA}e&wtG zkfoojVyqt7XXTL*XTsQcYvs>G0b$h2U`#b)A}-L1)?zyzVC!EdxosrbO%0_hTd!ri zx5t62(=(Tt!P&OM*zTQi;A$aol4t%1Nrnjh)KHqTw^v%x{}Ouat_*2y$={_kC&BDT zYSz!%o2Kj+5^2q47Dkfm9hg^VAzXK6`I7Tdes96xy0VfUYf7&yg_R@){OrN@sk$41 z%Qk^ceM+z?rJypU;5SOR8jDRy>RhSi(18RC&FfT_vsg=-TBi!jV3}p@!TcAoBXt%G zt8nDm0-37OZfAZUH4I-T46p}=S4v1V$jp*rIYXv%jIjXDC}c%M##f)LQUl+n$l7Nn zei{$IQt{^_PT<>+BJLWos)hIS@W;$$#Ue@l_^1Bq@j^=ItH*rtKJ{6En3jK}hwoqR zUXS_dD>E;5Dnuw1!a@w3V9d8}Smm6}j8M1ku_wL#5g}z(!9barPFcPX~&bhH_%y*bgjt}%}99*rc7M7YW09}J^)$Z-(y< zU#+nSJS{)1!x;5rlDet8k=f1Q;m zbro~;P{l!KG|oc0N4)hWS_{3ZHkce3{EAa|gYsOBB+YGRiau5u?E9_`iXy@j9wj@g z7ac%G#DX&tgcDN`QC0hP<((Ul{)p2mFT%xSI2R{Ni{72xDpz)p%_=_PTzmKtr+!HG zc3#%v;PWb-(%R=)V-*SZ$PSB5W!^vym1kB3_<%7`1(&N-ic6t&;2y~PWX~)5mA9Rp z8Iw|un#K}+exKaI1DDqY_vVJ&!DS4&I}m_jLTvE~Tht8uNdsF-r#H~3H-PEh1zYJe zwy!TXZxb4VlOF#EKn;8AM^f02w_3{cWy;ZPOn)Vr`m3-doKe7oXvh8^p3@uV(llc; zh!mAKs(<7{ejI(-s3mz~9mrR{7}1>7RIb+lu9~h{n?@9*e9@tK3Dum{`#zy;vPH=A zWu)xOq!HT@CsSaX)P~ok1WUV%g`QfQD-kI7m{9FjisdPmxWJPyA+=dN2BP*nH}aS_ z-=12Z7kAP>o7X?icU#A2p+Z5Ou5p#l>qT+JW%%Xa=ju->e-*I}Z z$oa3J)#lU3*1j=x4@7r+b)fhkK!E4WyT|d21Q;ADVFs! z#&bgU4%u1ftt0Kk^$JE^_okP`dy7W>p7v*O{(Ayk6uIuNUy*QnTmUE}A(LjftjJqE z17;swLU91tD#%(gg1^vj);0eiQOfY^qvxZkhk<(h>X17zrBH#vXA##HHKr@w`rTKZ zKKW#pVBXTX|0Sy7xT`o`D5rQf)jj`1Y7AAniR)o-wG{|GoK)GB1~*4IuA!&!8}4ZlnIMlG`4s(=()% z`+Rmsr#sU`Uy*D#`y0k7(gbl9=mRxlugpKNP><U@VIIbe%NJPgEcE>6ktl z#CRFkzlXfV(krux9n4KkA&s5$S+-2+Jn1jlHKt3lz+1Ccr3WOiYCCG(>(8`x*Whuc zi#W#oMnt ztKdK5mi(g_RNoV#iq;B+gO`xt)Q?CXyW}QMzRvRhc)%ZUihM;0M0XLoZc2K{L%PkP z!?QpREJ%d!W}x~D73}S7kv-hoOTov63NGu*o_`1L?eHK8BX258!C`Co=grTFNYLai zs$*1_%*3;uKHzI>aN7^K|7Di?tMfYJ$0&51exzNE;FWhC=;zu<>i-vx5e-OSp5V&R zKqMM6GevYVWdGktr#K3sBf;cf>(IJ`kcG5@o!|WjLzxAVBcBh+`fxLP)y=_rje5e6 z#96~zWgS|RG7Uc;lgGR&^zMB=cHTBJ7@PqWUA4_QVgNHJ=B(?g zD|BDiykgEdi>?{7uKv|C4BXjW-@gAp@BhyC&RIM&Q&U~{c6D{#x|OC4Pl$~%CAztt zXIMtXa176=89Sy3Q#8nRsNQIbF~kSE>WmT5G4QRYYoZ|`$kh-&NFR}CD#NgoR)%Rb zf$h6?(I@L8aX!@<#-4HLX*3uTMRsdxwjNWADbBbvU6??oBh!oN%M50cnORHzEOWTV*_Hh~?^#<9ul2zEKUf?dg` zvl;9vb~T&Hu3^`*>)7?|26iL6iQUZ9VSU*xtjrSj0Q(DjkUhj6W{k?d3U8T*`l!Mxd+@s z?lJe2d&Rxx-tu;QQN9FUnUCNj`GI^ipUBVPXY#Z7x%_;75xW zv`e?kuv=xf+HRfQdb_Q5yX=0pBX$Su9@;&!%e8xK_r&fuyQg-~?4H}buzP9u%I>vY zp4}U}w@gFE)$TpxWB0-Ca}l@7`MrUX-xDaB}*(o7k~ktxfRW6CoX zn2Jm#rZQ87smfGisxvhhC#ELj%(yVFOf9B1QwKilG4+`S@UJ1$h-u6;VceLe%J(2e zYr7gE_5G7fdLvFuG{%G{CF)IKnkuFUqh22$n4pW$2k4_>VFAF>2n2#ieeVPvkVl4T zT>p~DUpG7^E-B6%mYFU-Lf_jpB35UDDa%9|272q`O)-hd9fl_u;`Q-~VVcUrbn%I% zz`r_KNVqOy@Ia#>DL%4yQUV?x19NC#j=)5nG0{4EAANje&zLxUpZvuerg8d9rUG?w z39FkFB!QJK9z z4CxRbX+5c^8q1b&#tg&QJA02{n6YeK7vUv*RWM`OqJ9=o6qHXF%NK^xuo}%+*3|`d zwP41w#mBO>Zo=L4V45+_nHEe-rWMngX#@AREz^$iWVCSS5Q7;nraj}$bYOhIpmb8y zgXzqm{zkZTy)hO{1}1srBkkMM7Za!`S*M{gL*2w_f3dSHpN7kd}8#m zk@kO*G03w-ft1UqJ(mAOAqt{{jp*$anHRgpj9^BB%zt1;F{7C=jL3{-#xdhT=O!|f zKo2JaCsUbe@IHf?*;@y)8mjMYh>9}l6G6QU37}r3;$q_IwI8M_2bRLrAvQK9!DNVu z)Wvofo(MN7$dm}$J1{XChD7ONV__DnNeI>tkBCh&!Q8=-F-D8o35nLl0)aA8VZ889 zEb5}&X8CIGB1eg4&_!6EmbMZ=`+QomaOt(kVPkEGjkN>Y2UTGMWO+^$QWI0 zN26{isI^(VtC+dWJSOySsNgCJCozw+=*WC#&`Pnm#VjoHrZV0JRQm>-$l%umc7W-qgk`I*_z zWHH%{#K;U`4lutk2bn|6Vde;PlsU#6XMSZ)FgeUg<`i?9Im4V~&N1hi3(Q645_6fk z!dwMKyvAH-ZZJ2QTg+|d4s(~e$J}QgFb|nWOfK`7dBXh0JY}9S&zTp@OHj{_u%W$X z@&IbUykXwLyDMyc4dL@Wygr!m4VaJ2CxBd;FU;@ESH_iPSQd`fVO1>0@&Gkp)i4UN zc0l9GIw%-r17(Y`#n}=7BlA4Umg>rC%tw{;HelZNvkKiePB&2BKRm{m80~FU;4n=E zT~rh(vp&)*IbYe#dII*Rl69bu(PDpk>x?mG>m8!a*i z6p=Nu68L*iq z7-1tYCMLHx#3vd-Mi5egb&QP}7_F#6fwnjfGsb|Q+0)>yPe_dRN`l1!I$AC!5_aKy zEv8OeAbSu%QmoEsm1C$W0W{sJ;iU_gSiT9T7MFEM!L0p<>5Oqn3D&cPY0BHks6ZFN zp|oD~AI@Gd6$s5{EQUSMEDww3l%iIi1ykvdE4E;&f}0Com*Vb*$AS;s9pD1VRm|W0 z3ly;MP=O_}>Tkic$~J4%o8tAb|5`oo0$o#1RY2J*)?i35^`&nFh=!6tY@uF;X)4-W zH?Wix2P*@7Y_Y6^3(EHgU4&foz6>rM^{3(OU68#6@h1-jyk^wNa?Azg53(04*zD&n z!l$UH0dUaXMR;g|T!g)ronI_tb1oND#xjYEa4esoy1l(P9Y*F4a6zv56sVyEu!n>B z#Na`eVIA4BY&q}&8;2)@;);&lO7rS)Yc7FRalG2D$iJOKDAhI4HicksO(3i(R=h&B z>a{Ep--Qw-OKD1%aV%S|Jh-Cm4RMxxXX@28FrcHC83kdCIYt8m3lvGFL_?g}6qu3u zp;tFe9k<5KJeqp6bZ_3QwUb+8k5>8Ln!}e?O$*;w|)EIu7T#W`g)6;$ZLvs zE3E>ds$6vm39&o~H4ZESuh7U@{m=d->W9O1Fid+xLb6dYv`!eLq<8X-k7(S`$=$7KONLQoZaHgS53?-U z@+S&$<8LvHr}XXHx1S4foh63Z4kL^1EyUGr#W1CdG0f4uU=VcX`8ar2MMW{p>$MD1 zBA8(uPs5l{QuA|!f>pHQ|&t7TAS5AqqZw#<4Qv|M9F!$Lv-Z}W~T?F;zF1Kkcb-& zk&d=Zieq&kIo-N5%gk)S*wmRp-b%A767#GVKxF5KQ+%#^UMQkdw0s>su z^4s7g#|ZwMjbTYfytR$;3#0qJa2v0OS^T`Vu!^-Rzu zMi=bjrp5{J18=Y{tSeiKtqoSE4qKP4$JS>XunpNpY-6?wxVeaV&o*V^P7N(8a>8QPG@J!I8QIB}5BM3XhB#YW7w7B^YDCTLEOx4)lLVG7GJn!K-NRwblL0-hYHL4o>2 z#ThV#f+02OV`FvTpo$e8#A=$N-YlzTMTiC<_-9+Nt8>|+2iG+=C@RK+1P2{l$-yQZ7p~-U`ot)~MpP68+i){ESSXr#%piw#4gSQ1Cr<#1oNUyu?P$LiwstJuD5f6E*l^@ccoqA}UJCI+w(Y*YrT zW5dN7qEiMN$?C-{9v7hK{J=+jAkqO9PjjTy@5nVIbBzCA+ORO#0^)ja^gCNlaH#$yl zr60-8U;?&kH0%%TD0VbEh85Yd>^OEjJAs|ZPGTpsQ`o8OG6YGl&#D-!c zv9Z`hbQ7D3?xKg-Ol&T;P^Os4&SGb?bJ)4?N`ZfK*oE*)1-t^Mh%LodVrwx#41rfD z{0k8K!mAtL6*v@lG&{VZMty=_XI-gftQ;4*+#!$(13%fk&=X;$2k9as&C9-jlr9p& zNUPHuk_frYZsrV5UtJQcLRig4$k&AE3Npd{O%eKdy^;g<(wTFJJD0rLY@k(W%saq*?sKK?0yi@91)9NVtcWt*b4*_Aliiv)x{?1O^qVK#=>c= zqUdd@@rw4q9H36H^a?QDG{|b}t)GI(mJ!B$ zc>|vrO3Lz&->r1k&z^3Lo4Q*s`8b=S=-)*41n7r%278h{C3X;fM7tm*cr!uFY^6NM zo@WA9t`zO8h)e7(MOv5HE9_PF8hf3+!QK=*ik(DXv9s7k^b`HpvbRBKy$`i14&>F-%aCWy>8*uE`tIKPl8v8%I8bv8@O?nF|(QSsRs*)c3*F=9|y;L>8D1}+iTN6BG8>6JE4Mu4| zfh1J*RBir~R!HRu;wi8~DsT8^YlUU^H7*Q0*0)rF8$>J2H z8c;3*at|@_NswqUYqZ5WgoMMEVEGAwEO-lpMRiw{mw?b;E|co2px4vN)oUySu7;w%k2bJ^x$o{-yn< zZR>9TJ5FFrg(s<<;p30CRrOKz#fH1zRbRzX;^+eIIF?h{aL1{+5`W9x-^eaM;iK?l z`O~QKrNvd^s?z*$)wt^7IB|R-KQ%dLTYhSBjsBLOf2mp4Q@7;6b{6iqTwAW4I8mHb zcxr(|wx@2-b@|&<|KEy&(kw;8JP(f_FMx(+|Hi%jgJyAkxF8EZ=3R&h;X*T%YNYua z*N^KDjtjLXS&BRvT$r*C{rNz!HFsya^@rNyUsjd42riP-b5YztE*gR;s`jXqk*-at zn0ESpjm0Xc3pKpnLJ(*B-Eeo4C+@C?N?FQCbGr6Qt)3Q^7VvF{cEV+Y;P(c~N6O1w zMw&&Hk+}GRGE!&(k4_%{9+!z+5;v3^#tr9^|IFpp&OVEqXfIX%_-%~z^YSKwIL~jy zn${@R-wov#yS#K0)5;#zRPOA)DLbV65W(*eaHzb@T+XGr9P?LPHv9LuoXAb$CUaA` zsobVSBZxnP&IB)9D0%Cv3W!_xemOH09bp(HmB zI9zV!az4%FoWJ7Io#K*PV6Hd+MYWk*#HDg+++uDCw-mUvb{IglxtBRk{mTk-s<_A| z!}_P~%v|YL3a^%zxvF36mZlMx!L8y}bD7*4R^Zlh>$vsY25uv_iQCL=;kI(ym`ZF0 zx1HO;?c{cGKXSXdpSV3tEVq{_!|mgK=Js=0TsG5&lQ@|p+yU+v?jU!FJIo#7j&jGi zV|{GN2B3lJ(_Iai`fATq?ZIa_87)Oh@h_cL|QAGCtfD#+AFq zUFU89{cG+fWKZtd9&bMZ}wP5a5gcX<~P%CYYiPNwJY$K5u6nqzOs z(8NJGN0*pnRH6k7v+Z@zOcH77rGZ?CSxMU4_KVOL9R$fAMF!msvB?9$fr9E#3CQ zF!eIa7OH!OLaRl)jFmZP)RQRdIh zI-=l$_9|&{AW3zERIvHaJc-CaEeA5Y=3|S+RB@%fnC9%&3@Ddd&f*|0an@E!TkMR0 z#s$q*J{YEt&XSzUubyzZ=Awz}zouwX*R64jrf%*npyuJBltRAu#ZonC*}~1OS?iW= zZY_b-@})(yrcDdJC`bN{s!3_6nm9p9-RUo@CiftBpPz{Szpa{doYQ;7AlGY~eoGM> z`A-eYGvA``ztL3_Q-0MX0m!XYllcH!t0wCdIKOJLUxD+hCQu0g8cRjwKERs%s>vIG ztyL2Z(-`Kbi40TmY9Y>bCBu}Oz%aW{fy<*SxCpqX|NAQ@XQ71l74%QVIDYq-z;Un zbKEoV%%5{FxR-2qr9amMzRgfwtQkzuM?gVRTq-W>J<_eAliMgu1e?dbfpV=mk(R;b zF`+lOciemK1NV{p#C_(zaKFQhU%78Q!yRS2^DNVlO9RimJNV^@=it2~uV#F}8&!Hu zqhc(bM?NvJ;EDCMb{{F;TCZW?2=|0ubtvRP)v`j22`UzGhIsJEpg5torgFiY#_?D(6UxlyASL3VmHMmW@ z6JL{e=3RJKz7}7b+s)VE>+<#Z`g{YvA=8#`#5d-f@NRrlwgd0Z#PS||Grl?Bf^W&U z;#>1=__jO!h-k0ypcj5iG}_#(yU41W}0+hQ$Ra z>0v*^Fu3vI`OWhEqM%RRWR1i6MjE0D`s~6qwWyyiX+E{*H@IPzk2k2-DQ~7w57>UbX)P1r-TH2ng4ti}i|4GE(Re5I{`4ISTD>27O`u zL2#!O$Ls!?TQ76x${)p>Khhf7z$zXV4F!8I9h8(K^gRoM)BfSmw-(nk8k*dq4Y85E zq5_p)ZkhWaI(Y{`xclcb=9l}1#te+N^dodP4AVpG2~iaERGC9GOQ!(zI>K$FZj+-q zz_g$P3mVxS3uh{z4N__gMu-w2sQ)tu)@o>&rsls8wKc_{vAJ{z_xf9Bv{Z1xpd=Io zn+4sc7Qy&}aWv~N@S((}N_!&Qve;xCIS6{jOd)1X4KXQ!em-5jf5DQu>!Co{A<(3l zpAHBy^Vr=0+yzD%hQisDD7G7wmH-{hkB60;sr>e*!=Dyf&txBEtYe%(iJKL@DiCO> z4l2~p65SEn=xu+0^vCs5Fa-3|tQ-Y02sF3-Dyztn8nH5vFRDUiD`g(1oZos6>i-L& zbTt^`bg>0eaTb(-Sn%-ha3&BqISVF$+PPdRs&UX%vUk>=IFVxxo}JN74-b21EJHZG z%2}J1wpd*4to1vQqp@@Lf`Jas+EzWDI%8!HB*L3Q(!0Icpj&neJs%YeyM+an!Rw*v z$YSsRt@Iy1h#$lYQ`jUB1HKZ5YMSL0*lpC0rJBmevUajdh%Zq9(B{yc5B?){MX98sk>9t#%|`n zrcGPh!re5bt%R+cL60f4o&FO{{S!?6_vS@T!_Sf)!cUT(Ft4C9lZ77=so5f21I`U9 zjow@j_yM$z8wCBN!^~@)`EFihgmvMk{F4_ki|U^gr$uA_6Yu||IRBld@c%z~5hVli zuag|9OhZ2Q4@nLv#%fB6Yppqje?0+YN&YlJI^$C~T#1MNJ%2EhU&F8E*YWH54S)6r zch2o5w8{|M3|(DL*z?Qew&D(PyWh$M*V;@g*Hq&Drz;7BqV=w~Ib^VJ;k9RZo9o~& zL4rXPwg9jPUjMWH;5MZc%QsVv#PgvYp;068&lKM&U0(XgftX|;md@m3x zY06)pD0l)vB7_i1z0tZC9Q!YH0$aMbY7Qkc_f{6iueFDJtEL_nxS2<@)~%Z>KQvOD zz*f!O-J3UW-U<%>;{^Uw9Q$uBj{V1-z!U#Cfo7}tj}vGX!++%MA1CmCyA!zf4^H5D z@Zq-q`!3)ghhL$G|9kG=b^Zo_lfT8^=I{L3{cCu_UR$EyNTJ?bhwKT}PY9wc?(-Wn zdM7S{6v+HMm442Xe$baBJnq9x?1+Jgc{%U!QUJX-0W?k7c7|2TO65#P={$v+O>|7HhINz&c4b@0^o;GylaI(Wh6 zCI}P!;`ZP2)zrn+CDbL=rPLaA=|B5w7OfKDF&+$4zv?kTJR%-|RGd7<`pWlO)|O~) zrV+k06Z}qskEVDu%BM9|_dnpPsVgYHnz|D0tEnp&`fBQ`7GF(WqtI7VyKvXlFbc-i zvZm?Ob=CD0Urk;A|It@FXz|s45f58^wL|$yx@3zy^BpyHqkkMVr+?bi%{uW>Q%Q>d(zF>YC(_6P2JLuw_KdH0(7^mhBLzXr zIy}C@*ZZ$}@W!&X4?+Hm?2}*4i4&gI*I3pLTsiR=jDybYn`%E6o-d~MXJXmL>aOre z{O(K{wI^F&-Ggn!3|GT{TA=Qw_6C1zxH?!Jq7GH}Wg4pcso_`WRR>!i-DG*-OQ1du z8aQJj%zeGkQT(05J>iMY27?j$eye|v!txjmc#O(-2$Ua<>W6>-xTW_p2%deaJi^Q* z789>Ii09zKMF!yG;;FGLJeR$Qc**>HG4s>K)C1t@VrrfEt624_I#R7yN2v#@qt!9$ zLF&OEkXUsb^Hv?NHb8qgQV&rZ)h70Ji1L^r==cQ(9e!!k8L>rraAz}FQ_2BdJ+@8id3G!5 z?Aj5z?wv^j)gQ>|orfgkI)m)P1ZF(L!K=^Eg8zBk(AUKB-`zGA+_Hk;$3VyDYJ5%K-17; z*G!v2%2n$@jCcIdSbj3h-IZkTFNx@}^qdccKyvwhb@Jf#6tw+LcQVZMrRR*T z?TLOWwq9D|xfi`dZPDfmYo)4QX(-@X0~9)cqI7=oUevc$ee{BxAcZVXL%SZ>BW8V) zRHei|G$5r2`thJq3MjQ0Mb8~Z91e=|pwH7$&WS|wX3c2%(AOXv8hY&MgKNo_5<}$L zGrOTv4>QU0ka$@dP}*iJJtt#0NlwmOAhDkrRLu1R8Fe~Ede~>OFrvjLQZjEaD)D=s zXWNAL#ARR%@{Q^t(KPhf4Jy@vPSP>dCGl(anYqk?5i^gViaT~o^jLb%75-mHyJ2PV z4oyz>s<9tQ$mKG)$PXK&wrnohsI89&mD#0@h<`wS3a^WOCzO{ikM$u(Qw3b!Z6fN> z(VHw2J+a@cE{LX~$KD*$lx!H#5m(qh1$8twA;;Z2;BNB*Y{t@a)(&(5JrBlqWhSGY zBWsconZ5Crj@{6Bd4Q}wl#A1^!DAjO_Lu!yKE(Zh?Bhw(&|^cI9FV5X`hX82NxOfG zB)*jUMpSVj{0QCKOl$0_DhoI93bMeg@L8t

B)<7jzuT6Tpr#_QqSqp1JBWm1!VpYZ9| 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
mbAmabVUFQUhiy2=PJ}Fk4AT5E8+xGh|mVTN$z& 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 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 private final class SectionGroupComponent: Component { public final class Item: Equatable { @@ -813,6 +502,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 +644,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 +732,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 +868,33 @@ 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 + ), + 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 +1046,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 +1066,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 ) @@ -1538,7 +1298,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..33942e5f52 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,7 +591,9 @@ 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 @@ -557,28 +601,36 @@ private final class LimitSheetContent: CombinedComponent { iconName = "Premium/Folder" badgeText = "\(limit)" 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)" 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)" 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/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/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index fa46e2a395..dd7787860e 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -1319,6 +1319,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/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 7b282e7966..db9cecb919 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -174,6 +174,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1460809483] = { return Api.Dialog.parse_dialog($0) } dict[1908216652] = { return Api.Dialog.parse_dialogFolder($0) } dict[1949890536] = { return Api.DialogFilter.parse_dialogFilter($0) } + dict[909284270] = { return Api.DialogFilter.parse_dialogFilterDefault($0) } dict[2004110666] = { return Api.DialogFilterSuggested.parse_dialogFilterSuggested($0) } dict[-445792507] = { return Api.DialogPeer.parse_dialogPeer($0) } dict[1363483106] = { return Api.DialogPeer.parse_dialogPeerFolder($0) } diff --git a/submodules/TelegramApi/Sources/Api4.swift b/submodules/TelegramApi/Sources/Api4.swift index 04dce18cf0..17edf43042 100644 --- a/submodules/TelegramApi/Sources/Api4.swift +++ b/submodules/TelegramApi/Sources/Api4.swift @@ -897,6 +897,7 @@ public extension Api { public extension Api { enum DialogFilter: TypeConstructorDescription { case dialogFilter(flags: Int32, id: Int32, title: String, emoticon: String?, pinnedPeers: [Api.InputPeer], includePeers: [Api.InputPeer], excludePeers: [Api.InputPeer]) + case dialogFilterDefault public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -923,6 +924,12 @@ public extension Api { for item in excludePeers { item.serialize(buffer, true) } + break + case .dialogFilterDefault: + if boxed { + buffer.appendInt32(909284270) + } + break } } @@ -931,6 +938,8 @@ public extension Api { switch self { case .dialogFilter(let flags, let id, let title, let emoticon, let pinnedPeers, let includePeers, let excludePeers): return ("dialogFilter", [("flags", String(describing: flags)), ("id", String(describing: id)), ("title", String(describing: title)), ("emoticon", String(describing: emoticon)), ("pinnedPeers", String(describing: pinnedPeers)), ("includePeers", String(describing: includePeers)), ("excludePeers", String(describing: excludePeers))]) + case .dialogFilterDefault: + return ("dialogFilterDefault", []) } } @@ -969,6 +978,9 @@ public extension Api { return nil } } + public static func parse_dialogFilterDefault(_ reader: BufferReader) -> DialogFilter? { + return Api.DialogFilter.dialogFilterDefault + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index 22668d0803..2cb2f5f900 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, @@ -339,31 +351,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) + }) + } } } @@ -425,6 +442,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? @@ -978,11 +997,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 f359f6f588..17b2f87fa2 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..a8230c82c6 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 { + return .limitExceeded(count: 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 a68e95ddc4..fed5a15ca1 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -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)) @@ -10738,6 +10746,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/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..0822142af5 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 { 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/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/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 From 3fb304bd6ffd03c9bc1ae63db6a33c0f9afe229c Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 19 May 2022 21:15:31 +0400 Subject: [PATCH 7/7] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../ChatListUI/Sources/ChatContextMenus.swift | 4 +- .../Sources/ChatListController.swift | 15 ++-- .../Sources/Node/ChatListNode.swift | 4 +- .../Sources/MultilineTextComponent.swift | 61 ++++++++----- .../Sources/ViewControllerComponent.swift | 57 ++++++++++-- .../Sources/PeekControllerContent.swift | 2 + .../Sources/PeekControllerNode.swift | 4 + .../Display/Source/ImmediateTextNode.swift | 2 +- submodules/Display/Source/NavigationBar.swift | 2 +- submodules/Display/Source/TextNode.swift | 1 - submodules/InstantPageCache/BUILD | 2 + .../Sources/CachedInternalInstantPages.swift | 88 +++++++++++++++++++ submodules/PremiumUI/BUILD | 1 + .../Sources/PremiumIntroScreen.swift | 39 +++++++- .../Sources/PremiumLimitScreen.swift | 6 +- .../Sources/PremiumReactionsScreen.swift | 24 +++-- .../Sources/CachedFaqInstantPage.swift | 63 ------------- .../StickerPackPreviewControllerNode.swift | 4 +- .../Sources/StickerPackScreen.swift | 4 +- .../Sources/StickerPreviewPeekContent.swift | 30 +++++-- .../Peers/TogglePeerChatPinned.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 4 +- .../Sources/ChatMediaInputNode.swift | 8 +- .../TelegramUI/Sources/ChatTitleView.swift | 1 + .../Sources/FeaturedStickersScreen.swift | 8 +- ...textResultsChatInputContextPanelNode.swift | 4 +- ...rizontalStickersChatContextPanelNode.swift | 4 +- .../Sources/InlineReactionSearchPanel.swift | 4 +- .../StickersChatInputContextPanelNode.swift | 4 +- 30 files changed, 319 insertions(+), 136 deletions(-) create mode 100644 submodules/InstantPageCache/Sources/CachedInternalInstantPages.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 26d1043107..f7b9e72e03 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7612,3 +7612,6 @@ Sorry for the inconvenience."; "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/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index f61b11caaa..3de098e3ac 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -326,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, count: 0, 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 a2c8f32dd8..8a2ba8738a 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) { @@ -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, canReorderAllChats: false, 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, canReorderAllChats: true, 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) } @@ -1810,7 +1812,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, canReorderAllChats: true, 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 { @@ -1947,14 +1949,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController ]), filterItems, displayTabsAtBottom, - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + self.context.account.postbox.peerView(id: self.context.account.peerId) ) - |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom, accountPeer in + |> deliverOnMainQueue).start(next: { [weak self] _, countAndFilterItems, displayTabsAtBottom, peerView in guard let strongSelf = self else { return } - let isPremium = accountPeer?.isPremium ?? false + let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false + strongSelf.isPremium = isPremium let (_, items) = countAndFilterItems var filterItems: [ChatListFilterTabEntry] = [] diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index 64aeebffc0..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, count: 0, action: { + let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: { let premiumScreen = PremiumIntroScreen(context: context) replaceImpl?(premiumScreen) }) 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/PremiumUI/BUILD b/submodules/PremiumUI/BUILD index a5be59dbc1..3bfca16d19 100644 --- a/submodules/PremiumUI/BUILD +++ b/submodules/PremiumUI/BUILD @@ -44,6 +44,7 @@ swift_library( "//submodules/ConfettiEffect:ConfettiEffect", "//submodules/TextFormat:TextFormat", "//submodules/GZip:GZip", + "//submodules/InstantPageCache:InstantPageCache", ], visibility = [ "//visibility:public", diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 8ad86c20f9..e7cc10b0ef 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -15,6 +15,7 @@ import Markdown import InAppPurchaseManager import ConfettiEffect import TextFormat +import InstantPageCache private final class SectionGroupComponent: Component { public final class Item: Equatable { @@ -884,7 +885,39 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ), horizontalAlignment: .natural, maximumNumberOfLines: 0, - lineSpacing: 0.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), @@ -1222,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 @@ -1235,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() diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 33942e5f52..93aacedcd6 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -599,7 +599,7 @@ private final class LimitSheetContent: CombinedComponent { 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)" @@ -608,7 +608,7 @@ private final class LimitSheetContent: CombinedComponent { 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)" @@ -617,7 +617,7 @@ private final class LimitSheetContent: CombinedComponent { 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)" 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/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 dd7787860e..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 } 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/TelegramEngine/Peers/TogglePeerChatPinned.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift index 1a9a7d2a5f..e329d996d3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TogglePeerChatPinned.swift @@ -52,7 +52,7 @@ func _internal_toggleItemPinned(postbox: Postbox, accountPeerId: PeerId, locatio let count = sameKind.count + additionalCount if count > limitCount, itemIds.firstIndex(of: itemId) == nil { - return .limitExceeded(count: count, limit: limitCount) + return .limitExceeded(count: sameKind.count, limit: limitCount) } else { if let index = itemIds.firstIndex(of: itemId) { itemIds.remove(at: index) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index f2f176f5fc..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 } 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/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index 0822142af5..9c84aa6a0f 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -621,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/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 }