diff --git a/Telegram/Telegram-iOS/Resources/Anvil.tgs b/Telegram/Telegram-iOS/Resources/Anvil.tgs new file mode 100644 index 0000000000..7bcdbda3b2 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/Anvil.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/CraftFail.tgs b/Telegram/Telegram-iOS/Resources/CraftFail.tgs new file mode 100644 index 0000000000..eaf5e1db3f Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/CraftFail.tgs differ diff --git a/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs b/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs new file mode 100644 index 0000000000..983d067102 Binary files /dev/null and b/Telegram/Telegram-iOS/Resources/CraftFailOverlay.tgs differ diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 169b1f39bc..efab15b20f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -185,16 +185,6 @@ public enum WallpaperUrlParameter { case gradient([UInt32], Int32?) } -public enum ResolvedUrlSettingsSection { - case theme - case devices - case autoremoveMessages - case twoStepAuth - case enableLog - case phonePrivacy - case loginEmail -} - public struct ResolvedBotChoosePeerTypes: OptionSet { public var rawValue: UInt32 @@ -315,13 +305,12 @@ public enum ResolvedUrl { case share(url: String?, text: String?, to: String?) case wallpaper(WallpaperUrlParameter) case theme(String) - case settings(ResolvedUrlSettingsSection) case joinVoiceChat(PeerId, String?) case importStickers case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?) case invoice(slug: String, invoice: TelegramMediaInvoice?) case premiumOffer(reference: String?) - case starsTopup(amount: Int64, purpose: String?) + case starsTopup(amount: Int64?, purpose: String?) case chatFolder(slug: String) case story(peerId: PeerId, id: Int32) case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) @@ -336,6 +325,53 @@ public enum ResolvedUrl { case storyFolder(peerId: PeerId, id: Int64) case giftCollection(peerId: PeerId, id: Int64) case sendGift(peerId: PeerId?) + case unknownDeepLink(path: String) + case oauth(url: String) + + public enum ChatsSection { + case search + case edit + case emojiStatus + } + case chats(ChatsSection?) + + public enum ComposeSection { + case group + case channel + case contact + } + case compose(ComposeSection?) + + public enum PostStorySection { + case photo + case video + case live + } + case postStory(PostStorySection?) + + public enum ContactsSection { + case search + case sort + case new + case invite + case manage + } + case contacts(ContactsSection?) + + public enum SettingsSection { + public enum Legacy { + case theme + case devices + case autoremoveMessages + case enableLog + case phonePrivacy + case loginEmail + } + + case legacy(Legacy) + case path(String) + } + case settings(SettingsSection) } public enum ResolveUrlResult { @@ -972,18 +1008,30 @@ public protocol MediaEditorScreenResult { var target: Stories.PendingTarget { get } } +public enum StoryCameraMode { + case photo + case video + case live +} + public protocol TelegramRootControllerInterface: NavigationController { @discardableResult - func openStoryCamera(customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? + func openStoryCamera(mode: StoryCameraMode, customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) func getContactsController() -> ViewController? func getChatsController() -> ViewController? + func getSettingsController() -> ViewController? + func getPrivacySettings() -> Promise? - func openSettings() + func getTwoStepAuthData() -> Promise? + + func openContacts() + func openSettings(edit: Bool) func openBirthdaySetup() func openPhotoSetup(completedWithUploadingImage: @escaping (UIImage, Signal) -> UIView?) func openAvatars() + func startNewCall() } public protocol QuickReplySetupScreenInitialData: AnyObject { @@ -1185,6 +1233,7 @@ public enum ChannelMembersSearchControllerMode { case promote case ban case inviteToCall + case ownershipTransfer } public enum ChannelMembersSearchFilter { @@ -1412,6 +1461,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftOfferScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeGiftUpgradeVariantsScreen(context: AccountContext, gift: StarGift, onlyCrafted: Bool, attributes: [StarGift.UniqueGift.Attribute], selectedAttributes: [StarGift.UniqueGift.Attribute]?, focusedAttribute: StarGift.UniqueGift.Attribute?) -> ViewController func makeGiftAuctionWearPreviewScreen(context: AccountContext, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?, attributes: [StarGift.UniqueGift.Attribute], completion: @escaping () -> Void) -> ViewController + func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController func makeGiftDemoScreen(context: AccountContext) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController func makeContentReportScreen(context: AccountContext, subject: ReportContentSubject, forceDark: Bool, present: @escaping (ViewController) -> Void, completion: @escaping () -> Void, requestSelectMessages: ((String, Data, String?) -> Void)?) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index e499fc07b7..3e9c3644ab 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -976,13 +976,19 @@ public enum PeerInfoAvatarUploadStatus { public protocol PeerInfoScreen: ViewController { var peerId: PeerId { get } var privacySettings: Promise { get } + var twoStepAuthData: Promise { get } + func activateEdit() + func openEmojiStatusSetup() func openBirthdaySetup() func toggleStorySelection(ids: [Int32], isSelected: Bool) func togglePaneIsReordering(isReordering: Bool) func cancelItemSelection() func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal) -> UIView?) func openAvatars() + + func updateProfilePhoto(_ image: UIImage) + func updateProfileVideo(_ image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) } public extension Peer { diff --git a/submodules/AccountContext/Sources/ChatListController.swift b/submodules/AccountContext/Sources/ChatListController.swift index 68a72e1ea2..a9d7ade2d0 100644 --- a/submodules/AccountContext/Sources/ChatListController.swift +++ b/submodules/AccountContext/Sources/ChatListController.swift @@ -28,4 +28,7 @@ public protocol ChatListController: ViewController { func openStoriesFromNotification(peerId: EnginePeer.Id, storyId: Int32) func resetForumStackIfOpen() + + func activateEdit() + func openEmojiStatusSetup() } diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index dce6b3d676..617ed64ed7 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -779,7 +779,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.ignoreUpdatesUntilScrollingStopped = true } } - + @available(iOS 13.0, *) func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { if #available(iOS 14.5, *), navigationAction.shouldPerformDownload { @@ -798,11 +798,16 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU if let url = navigationAction.request.url?.absoluteString { if (navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == true) && (isTelegramMeLink(url) || url.hasPrefix("tg://")) && !url.contains("/auth/push?") && !self._state.url.contains("/auth/push?") { decisionHandler(.cancel, preferences) - self.minimize() + if !url.contains("domain=oauth") { + self.minimize() + } self.openAppUrl(url) } else { - if let scheme = navigationAction.request.url?.scheme, !["http", "https", "tonsite", "about"].contains(scheme.lowercased()) { + if let scheme = navigationAction.request.url?.scheme?.lowercased(), !["http", "https", "tonsite", "about"].contains(scheme) { decisionHandler(.cancel, preferences) + if ["facetime"].contains(scheme) { + return + } self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {}) } else { decisionHandler(.allow, preferences) diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index ab9ba5e3b8..194c3c97da 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -23,6 +23,20 @@ public enum CallListControllerMode { case navigation } +public enum CallListEntryTag: ItemListItemTag, Equatable { + case edit + case showTab + case missed + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? CallListEntryTag, self == other { + return true + } else { + return false + } + } +} + private final class DeleteAllButtonNode: ASDisplayNode { private let pressed: () -> Void @@ -69,6 +83,8 @@ private final class DeleteAllButtonNode: ASDisplayNode { } } + + public final class CallListController: TelegramBaseController { private var controllerNode: CallListControllerNode { return self.displayNode as! CallListControllerNode @@ -81,6 +97,7 @@ public final class CallListController: TelegramBaseController { private let context: AccountContext private let mode: CallListControllerMode + private let focusOnItemTag: CallListEntryTag? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? @@ -96,9 +113,15 @@ public final class CallListController: TelegramBaseController { private let clearDisposable = MetaDisposable() private var createConferenceCallDisposable: Disposable? - public init(context: AccountContext, mode: CallListControllerMode) { + public init( + context: AccountContext, + mode: CallListControllerMode, + focusOnItemTag: CallListEntryTag? = nil + ) { self.context = context self.mode = mode + self.focusOnItemTag = focusOnItemTag + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.segmentedTitleView = ItemListControllerSegmentedTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Calls_All, self.presentationData.strings.Calls_Missed], selectedIndex: 0) @@ -409,7 +432,7 @@ public final class CallListController: TelegramBaseController { if let strongSelf = self { strongSelf.callPressed() } - }) + }, focusOnItemTag: self.focusOnItemTag) if case .navigation = self.mode { self.controllerNode.navigationBar = self.navigationBar @@ -421,6 +444,18 @@ public final class CallListController: TelegramBaseController { } self._ready.set(self.controllerNode.ready) self.displayNodeDidLoad() + + switch self.focusOnItemTag { + case .edit: + self.editPressed() + case .missed: + Queue.mainQueue().after(0.1) { + self.segmentedTitleView.index = 1 + self.controllerNode.updateType(.missed) + } + default: + break + } } override public var navigationEdgeEffectExtension: CGFloat { diff --git a/submodules/CallListUI/Sources/CallListControllerNode.swift b/submodules/CallListUI/Sources/CallListControllerNode.swift index 412302fde3..9d25ad482b 100644 --- a/submodules/CallListUI/Sources/CallListControllerNode.swift +++ b/submodules/CallListUI/Sources/CallListControllerNode.swift @@ -125,7 +125,7 @@ private func mappedInsertEntries(context: AccountContext, presentationData: Item case let .displayTab(_, text, value): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in nodeInteraction.updateShowCallsTab(value) - }), directionHint: entry.directionHint) + }, tag: CallListEntryTag.showTab), directionHint: entry.directionHint) case let .displayTabInfo(_, text): return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case .openNewCall: @@ -149,7 +149,7 @@ private func mappedUpdateEntries(context: AccountContext, presentationData: Item case let .displayTab(_, text, value): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: true, noCorners: false, sectionId: 0, style: .blocks, updated: { value in nodeInteraction.updateShowCallsTab(value) - }), directionHint: entry.directionHint) + }, tag: CallListEntryTag.showTab), directionHint: entry.directionHint) case let .displayTabInfo(_, text): return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: 0), directionHint: entry.directionHint) case .openNewCall: @@ -184,6 +184,7 @@ final class CallListControllerNode: ASDisplayNode { private let context: AccountContext private let mode: CallListControllerMode private var presentationData: PresentationData + private var focusOnItemTag: CallListEntryTag? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -241,7 +242,7 @@ final class CallListControllerNode: ASDisplayNode { private var previousContentOffset: ListViewVisibleContentOffset? - init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, openNewCall: @escaping () -> Void) { + init(controller: CallListController, context: AccountContext, mode: CallListControllerMode, presentationData: PresentationData, call: @escaping (EngineMessage) -> Void, joinGroupCall: @escaping (EnginePeer.Id, EngineGroupCallDescription) -> Void, openInfo: @escaping (EnginePeer.Id, [EngineMessage]) -> Void, emptyStateUpdated: @escaping (Bool) -> Void, openNewCall: @escaping () -> Void, focusOnItemTag: CallListEntryTag?) { self.controller = controller self.context = context self.mode = mode @@ -253,6 +254,7 @@ final class CallListControllerNode: ASDisplayNode { self.openNewCall = openNewCall self.currentState = CallListNodeState(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, disableAnimations: true, editing: false, messageIdWithRevealedOptions: nil) self.statePromise = ValuePromise(self.currentState, ignoreRepeated: true) + self.focusOnItemTag = focusOnItemTag self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor @@ -861,6 +863,16 @@ final class CallListControllerNode: ASDisplayNode { strongSelf._ready.set(true) } + if let focusOnItemTag = strongSelf.focusOnItemTag { + strongSelf.focusOnItemTag = nil + + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + itemNode.displayHighlight() + } + } + } + completion() } } diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 87652ea35f..f57a9d888d 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -125,6 +125,10 @@ swift_library( "//submodules/TelegramUI/Components/LiveLocationHeaderPanelComponent", "//submodules/TelegramUI/Components/ChatList/ChatListSearchFiltersContainerNode", "//submodules/TelegramUI/Components/ChatList/ChatListHeaderNoticeComponent", + "//submodules/TelegramUI/Components/AlertComponent", + "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", + "//submodules/TelegramUI/Components/AvatarComponent", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index ff2710466b..f8dfbc50e1 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -144,7 +144,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var didSuggestLoginEmailSetup = false private var didSuggestLoginPasskeySetup = false - private var presentationData: PresentationData + private(set) var presentationData: PresentationData private let presentationDataValue = Promise() private var presentationDataDisposable: Disposable? @@ -3029,7 +3029,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(customTarget: nil, resumeLiveStream: hasLiveStream, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) + let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: hasLiveStream, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) coordinator?.animateIn() } } @@ -3511,7 +3511,17 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController super.navigationStackConfigurationUpdated(next: next) } - @objc fileprivate func editPressed() { + public func activateEdit() { + self.editPressed() + } + + public func openEmojiStatusSetup() { + if let navigationBarView = self.chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View { + navigationBarView.openEmojiStatusSetup() + } + } + + @objc func editPressed() { if self.secondaryContext == nil { if case .chatList(.root) = self.chatListDisplayNode.effectiveContainerNode.location { self.effectiveContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( @@ -5874,10 +5884,33 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) ], parseMarkdown: true), in: .window(.root)) } else { - completion(true) - self.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { - removed() - }) + let proceed = { + completion(true) + self.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { + removed() + }) + } + if case let .channel(channel) = peer.peer, channel.flags.contains(.isCreator) { + let _ = (self.context.engine.peers.getFutureCreatorAfterLeave(peerId: channel.id) + |> deliverOnMainQueue).start(next: { [weak self] nextCreator in + guard let self else { + return + } + if let nextCreator, let peer = peer.peer { + self.presentLeaveChannelConfirmation(peer: peer, nextCreator: nextCreator, completion: { commit in + if commit { + proceed() + } else { + completion(false) + } + }) + } else { + proceed() + } + }) + } else { + proceed() + } } } @@ -6425,7 +6458,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if let current = self.storyCameraTransitionInCoordinator { coordinator = current } else { - coordinator = rootController.openStoryCamera(customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in + coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { [weak self] target, _ in guard let self, let target else { return nil } diff --git a/submodules/ChatListUI/Sources/ChatListControllerLeaveChannelConfirmation.swift b/submodules/ChatListUI/Sources/ChatListControllerLeaveChannelConfirmation.swift new file mode 100644 index 0000000000..8175a859b8 --- /dev/null +++ b/submodules/ChatListUI/Sources/ChatListControllerLeaveChannelConfirmation.swift @@ -0,0 +1,157 @@ +import Foundation +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import AccountContext +import ComponentFlow +import AlertComponent +import AlertTransferHeaderComponent +import AvatarComponent +import PeerInfoUI +import OwnershipTransferController + +extension ChatListControllerImpl { + func presentLeaveChannelConfirmation(peer: EnginePeer, nextCreator: EnginePeer, completion: @escaping (Bool) -> Void) { + Task { @MainActor in + let accountPeer = await (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))).get() + + guard let accountPeer else { + completion(false) + return + } + + var content: [AnyComponentWithIdentity] = [] + content.append(AnyComponentWithIdentity( + id: "header", + component: AnyComponent( + AlertTransferHeaderComponent( + fromComponent: AnyComponentWithIdentity(id: "account", component: AnyComponent( + AvatarComponent( + context: self.context, + theme: self.presentationData.theme, + peer: accountPeer + ) + )), + toComponent: AnyComponentWithIdentity(id: "user", component: AnyComponent( + AvatarComponent( + context: self.context, + theme: self.presentationData.theme, + peer: nextCreator, + icon: AnyComponent( + AvatarComponent( + context: self.context, + theme: self.presentationData.theme, + peer: peer + ) + ) + ) + )), + type: .transfer + ) + ) + )) + //TODO:localize + content.append(AnyComponentWithIdentity( + id: "title", + component: AnyComponent( + AlertTitleComponent(title: "Leave \(peer.compactDisplayTitle)") + ) + )) + content.append(AnyComponentWithIdentity( + id: "text", + component: AnyComponent( + AlertTextComponent(content: .plain("If you leave, **\(nextCreator.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder))** will become the owner of **\(peer.compactDisplayTitle)** in **1 week**.")) + ) + )) + + let alertController = AlertScreen( + context: self.context, + configuration: .init(actionAlignment: .vertical), + content: content, + actions: [ + .init(title: "Appoint Another Owner", action: { [weak self] in + guard let self else { + return + } + self.presentOwnershipTransfer(chatPeer: peer) + }), + .init(title: "Cancel", action: { + completion(false) + }), + .init(title: "Leave Group", type: .destructive, action: { + completion(true) + }) + ] + ) + if let topController = self.navigationController?.topViewController as? ViewController { + topController.present(alertController, in: .window(.root)) + } + } + } + + func presentOwnershipTransfer(chatPeer: EnginePeer) { + let presentController: (ViewController) -> Void = { [weak self] c in + if let topController = self?.navigationController?.topViewController as? ViewController { + topController.present(c, in: .window(.root)) + } + } + let pushController: (ViewController) -> Void = { [weak self] c in + if let topController = self?.navigationController?.topViewController as? ViewController { + topController.push(c) + } + } + + var dismissController: (() -> Void)? + let controller = ChannelMembersSearchControllerImpl( + params: ChannelMembersSearchControllerParams( + context: self.context, + peerId: chatPeer.id, + mode: .ownershipTransfer, + filters: [], + openPeer: { [weak self] peer, participant in + guard let self else { + return + } + if peer.id == self.context.account.peerId { + return + } + if let participant { + switch participant.participant { + case .creator: + return + case let .member(_, _, adminInfo, _, _, _): + let _ = adminInfo + + let _ = (self.context.engine.peers.checkOwnershipTranfserAvailability(memberId: peer.id) |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self, case let .user(user) = peer else { + return + } + let controller = channelOwnershipTransferController( + context: self.context, + updatedPresentationData: nil, + peer: chatPeer, + member: user, + initialError: error, + present: { c, a in + presentController(c) + }, + push: { c in + pushController(c) + }, + completion: { _ in + dismissController?() + } + ) + presentController(controller) + }) + } + } + }) + ) + dismissController = { [weak controller] in + controller?.dismiss() + } + pushController(controller) + } +} diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift index 7c6a10aaf1..c880cf8fc9 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetController.swift @@ -1388,7 +1388,7 @@ private final class ChatListFilterPresetController: ItemListController { } } -func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { +public func chatListFilterPresetController(context: AccountContext, currentPreset initialPreset: ChatListFilter?, updated: @escaping ([ChatListFilter]) -> Void) -> ViewController { let initialName: ChatFolderTitle if let initialPreset { initialName = initialPreset.title diff --git a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift index 8750c6f8a5..d8dbb29e6a 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterPresetListController.swift @@ -45,6 +45,8 @@ private enum ChatListFilterPresetListSection: Int32 { } public enum ChatListFilterPresetListEntryTag: ItemListItemTag { + case edit + case addRecommended case displayTags public func isEqual(to other: ItemListItemTag) -> Bool { @@ -175,7 +177,7 @@ private enum ChatListFilterPresetListEntry: ItemListNodeEntry { case let .screenHeader(text): return ChatListFilterSettingsHeaderItem(context: arguments.context, theme: presentationData.theme, text: text, animation: .folders, sectionId: self.section) case let .suggestedListHeader(text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, multiline: true, sectionId: self.section, tag: ChatListFilterPresetListEntryTag.addRecommended) case let .suggestedPreset(_, title, label, preset): return ChatListFilterPresetListSuggestedItem(presentationData: presentationData, systemStyle: .glass, title: title.text, label: label, sectionId: self.section, style: .blocks, installAction: { arguments.addSuggestedPressed(title, preset) @@ -318,7 +320,7 @@ public enum ChatListFilterPresetListControllerMode { case modal } -public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, dismissed: (() -> Void)? = nil) -> ViewController { +public func chatListFilterPresetListController(context: AccountContext, mode: ChatListFilterPresetListControllerMode, scrollToTags: Bool = false, focusOnItemTag: ChatListFilterPresetListEntryTag? = nil, dismissed: (() -> Void)? = nil) -> ViewController { let initialState = ChatListFilterPresetListControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) @@ -326,6 +328,14 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch statePromise.set(stateValue.modify { f($0) }) } + if focusOnItemTag == .edit { + updateState { state in + var state = state + state.isEditing = true + return state + } + } + var dismissImpl: (() -> Void)? var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController) -> Void)? @@ -680,7 +690,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 entries = chatListFilterPresetListControllerEntries(presentationData: presentationData, state: state, filters: filtersWithCountsValue, updatedFilterOrder: updatedFilterOrderValue, suggestedFilters: suggestedFilters, displayTags: displayTags, isPremium: isPremium, limits: limits, premiumLimits: premiumLimits) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, initialScrollToItem: scrollToTags ? ListViewScrollToItem(index: entries.count - 1, position: .center(.bottom), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down) : nil, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -797,6 +807,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } }) }) + + var didFocusOnItem = false controller.afterTransactionCompleted = { [weak controller] in guard let toggleDirection = animateNextShowHideTagsTransition.swap(nil) else { return @@ -819,6 +831,15 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch } delay += 0.02 } + + if let focusOnItemTag, !didFocusOnItem { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } } return controller diff --git a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift index 92098851a4..bcec86cb1f 100644 --- a/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift +++ b/submodules/ComponentFlow/Source/Components/RoundedRectangle.swift @@ -193,7 +193,7 @@ public final class FilledRoundedRectangleComponent: Component { } } else { if component.smoothCorners { - let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0) + let size = CGSize(width: cornerRadius * 2.0 + 8.0, height: cornerRadius * 2.0 + 8.0) if let cornerImage = self.cornerImage, cornerImage.size == size { } else { self.cornerImage = generateImage(size, rotatedContext: { size, context in diff --git a/submodules/Components/ResizableSheetComponent/BUILD b/submodules/Components/ResizableSheetComponent/BUILD new file mode 100644 index 0000000000..cca9d7a5c5 --- /dev/null +++ b/submodules/Components/ResizableSheetComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ResizableSheetComponent", + module_name = "ResizableSheetComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ViewControllerComponent:ViewControllerComponent", + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/TelegramUI/Components/DynamicCornerRadiusView", + "//submodules/TelegramUI/Components/EdgeEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift new file mode 100644 index 0000000000..e638c51cb7 --- /dev/null +++ b/submodules/Components/ResizableSheetComponent/Sources/ResizableSheetComponent.swift @@ -0,0 +1,678 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import SwiftSignalKit +import DynamicCornerRadiusView +import TelegramPresentationData +import EdgeEffect + +public final class ResizableSheetComponentEnvironment: Equatable { + public let theme: PresentationTheme + public let statusBarHeight: CGFloat + public let safeInsets: UIEdgeInsets + public let metrics: LayoutMetrics + public let deviceMetrics: DeviceMetrics + public let isDisplaying: Bool + public let isCentered: Bool + public let screenSize: CGSize + public let regularMetricsSize: CGSize? + public let dismiss: (Bool) -> Void + + public init( + theme: PresentationTheme, + statusBarHeight: CGFloat, + safeInsets: UIEdgeInsets, + metrics: LayoutMetrics, + deviceMetrics: DeviceMetrics, + isDisplaying: Bool, + isCentered: Bool, + screenSize: CGSize, + regularMetricsSize: CGSize?, + dismiss: @escaping (Bool) -> Void + ) { + self.theme = theme + self.statusBarHeight = statusBarHeight + self.safeInsets = safeInsets + self.metrics = metrics + self.deviceMetrics = deviceMetrics + self.isDisplaying = isDisplaying + self.isCentered = isCentered + self.screenSize = screenSize + self.regularMetricsSize = regularMetricsSize + self.dismiss = dismiss + } + + public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + if lhs.statusBarHeight != rhs.statusBarHeight { + return false + } + if lhs.safeInsets != rhs.safeInsets { + return false + } + if lhs.metrics != rhs.metrics { + return false + } + if lhs.deviceMetrics != rhs.deviceMetrics { + return false + } + if lhs.isDisplaying != rhs.isDisplaying { + return false + } + if lhs.isCentered != rhs.isCentered { + return false + } + if lhs.screenSize != rhs.screenSize { + return false + } + if lhs.regularMetricsSize != rhs.regularMetricsSize { + return false + } + return true + } +} + +public final class ResizableSheetComponent: Component { + public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) + + public class ExternalState { + public fileprivate(set) var contentHeight: CGFloat + + public init() { + self.contentHeight = 0.0 + } + } + + public enum BackgroundColor: Equatable { + case color(UIColor) + } + + public let content: AnyComponent + public let titleItem: AnyComponent? + public let leftItem: AnyComponent? + public let rightItem: AnyComponent? + public let hasTopEdgeEffect: Bool + public let bottomItem: AnyComponent? + public let backgroundColor: BackgroundColor + public let isFullscreen: Bool + public let externalState: ExternalState? + public let animateOut: ActionSlot> + + public init( + content: AnyComponent, + titleItem: AnyComponent? = nil, + leftItem: AnyComponent? = nil, + rightItem: AnyComponent? = nil, + hasTopEdgeEffect: Bool = true, + bottomItem: AnyComponent? = nil, + backgroundColor: BackgroundColor, + isFullscreen: Bool = false, + externalState: ExternalState? = nil, + animateOut: ActionSlot>, + ) { + self.content = content + self.titleItem = titleItem + self.leftItem = leftItem + self.rightItem = rightItem + self.hasTopEdgeEffect = hasTopEdgeEffect + self.bottomItem = bottomItem + self.backgroundColor = backgroundColor + self.isFullscreen = isFullscreen + self.externalState = externalState + self.animateOut = animateOut + } + + public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.titleItem != rhs.titleItem { + return false + } + if lhs.leftItem != rhs.leftItem { + return false + } + if lhs.rightItem != rhs.rightItem { + return false + } + if lhs.hasTopEdgeEffect != rhs.hasTopEdgeEffect { + return false + } + if lhs.bottomItem != rhs.bottomItem { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.isFullscreen != rhs.isFullscreen { + return false + } + if lhs.animateOut != rhs.animateOut { + return false + } + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var containerCornerRadius: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { + public final class Tag { + public init() { + } + } + + public func matches(tag: Any) -> Bool { + if let _ = tag as? Tag { + return true + } + return false + } + + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let bottomContainer: SparseContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let topEdgeEffectView: EdgeEffectView + private let bottomEdgeEffectView: EdgeEffectView + private let contentView: ComponentView + + private var titleItemView: ComponentView? + private var leftItemView: ComponentView? + private var rightItemView: ComponentView? + private var bottomItemView: ComponentView? + + private let backgroundHandleView: UIImageView + + private var ignoreScrolling: Bool = false + + private var component: ResizableSheetComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ResizableSheetComponentEnvironment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + self.bottomContainer = SparseContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + self.topEdgeEffectView = EdgeEffectView() + self.topEdgeEffectView.clipsToBounds = true + self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.topEdgeEffectView.layer.cornerRadius = 40.0 + + self.bottomEdgeEffectView = EdgeEffectView() + self.bottomEdgeEffectView.clipsToBounds = true + self.bottomEdgeEffectView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + self.bottomEdgeEffectView.layer.cornerRadius = 40.0 + + self.contentView = ComponentView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + self.scrollView.contentInsetAdjustmentBehavior = .never + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + self.containerView.addSubview(self.bottomContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + if let result = self.bottomContainer.hitTest(self.convert(point, to: self.bottomContainer), with: event) { + return result + } + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.dismissAnimated() + } + } + + public func dismissAnimated() { + guard let environment = self.environment else { + return + } + self.endEditing(true) + environment.dismiss(true) + } + + private func updateScrolling(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout, let component = self.component else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + if component.isFullscreen { + topOffsetFraction = 1.0 + } + + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width + let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 + let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) + + if component.isFullscreen { + transition.setBounds(view: self.scrollView, bounds: CGRect(origin: .zero, size: self.scrollView.bounds.size)) + self.scrollView.isScrollEnabled = false + } else { + self.scrollView.isScrollEnabled = true + } + } + + private var didPlayAppearanceAnimation = false + func animateIn() { + self.didPlayAppearanceAnimation = true + + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.bottomContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.bottomContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + func update(component: ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value + component.animateOut.connect { [weak self] completion in + guard let self else { + return + } + self.animateOut { + completion(Void()) + } + } + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = sheetEnvironment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0 + } + let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + + self.component = component + self.state = state + self.environment = sheetEnvironment + + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + + let backgroundColor: UIColor + switch component.backgroundColor { + case let .color(color): + backgroundColor = color + self.backgroundLayer.backgroundColor = backgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var containerSize: CGSize + if !"".isEmpty, sheetEnvironment.isCentered { + let verticalInset: CGFloat = 44.0 + let maxSide = max(availableSize.width, availableSize.height) + let minSide = min(availableSize.width, availableSize.height) + containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0) + if let regularMetricsSize = sheetEnvironment.regularMetricsSize { + containerSize = regularMetricsSize + } + } else { + containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude) + } + + var containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0 + if component.isFullscreen { + containerInset = 0.0 + } + let clippingY: CGFloat + + self.contentView.parentState = state + let contentViewSize = self.contentView.update( + transition: transition, + component: component.content, + environment: { + environment[ChildEnvironmentType.self] + }, + containerSize: containerSize + ) + component.externalState?.contentHeight = contentViewSize.height + + if let contentView = self.contentView.view { + if contentView.superview == nil { + self.scrollContentView.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize)) + } + + let contentHeight = contentViewSize.height + let initialContentHeight = contentHeight + + let edgeEffectHeight: CGFloat = 80.0 + let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) + transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) + self.topEdgeEffectView.update(content: backgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) + if self.topEdgeEffectView.superview == nil { + self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) + } + self.topEdgeEffectView.isHidden = !component.hasTopEdgeEffect + + if let titleItem = component.titleItem { + let titleItemView: ComponentView + if let current = self.titleItemView { + titleItemView = current + } else { + titleItemView = ComponentView() + self.titleItemView = titleItemView + } + + let titleItemSize = titleItemView.update( + transition: transition, + component: titleItem, + environment: {}, + containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0) + ) + let titleItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(36.0 - titleItemSize.height * 0.5)), size: titleItemSize) + if let view = titleItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + transition.setFrame(view: view, frame: titleItemFrame) + } + } else if let titleItemView = self.titleItemView { + self.titleItemView = nil + titleItemView.view?.removeFromSuperview() + } + + + if let leftItem = component.leftItem { + var leftItemTransition = transition + let leftItemView: ComponentView + if let current = self.leftItemView { + leftItemView = current + } else { + leftItemTransition = .immediate + leftItemView = ComponentView() + self.leftItemView = leftItemView + } + + let leftItemSize = leftItemView.update( + transition: leftItemTransition, + component: leftItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let leftItemFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: leftItemSize) + if let view = leftItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + } + leftItemTransition.setFrame(view: view, frame: leftItemFrame) + } + } else if let leftItemView = self.leftItemView { + self.leftItemView = nil + if !transition.animation.isImmediate { + leftItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + leftItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + leftItemView.view?.removeFromSuperview() + }) + } else { + leftItemView.view?.removeFromSuperview() + } + } + + if let rightItem = component.rightItem { + var rightItemTransition = transition + let rightItemView: ComponentView + if let current = self.rightItemView { + rightItemView = current + } else { + rightItemTransition = .immediate + rightItemView = ComponentView() + self.rightItemView = rightItemView + } + + let rightItemSize = rightItemView.update( + transition: rightItemTransition, + component: rightItem, + environment: {}, + containerSize: CGSize(width: 66.0, height: 66.0) + ) + let rightItemFrame = CGRect(origin: CGPoint(x: availableSize.width - rawSideInset - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize) + if let view = rightItemView.view { + if view.superview == nil { + self.navigationBarContainer.addSubview(view) + + if !transition.animation.isImmediate { + view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + rightItemTransition.setFrame(view: view, frame: rightItemFrame) + } + } else if let rightItemView = self.rightItemView { + self.rightItemView = nil + if !transition.animation.isImmediate { + rightItemView.view?.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + rightItemView.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + rightItemView.view?.removeFromSuperview() + }) + } else { + rightItemView.view?.removeFromSuperview() + } + } + + if let bottomItem = component.bottomItem { + let bottomItemView: ComponentView + if let current = self.bottomItemView { + bottomItemView = current + } else { + bottomItemView = ComponentView() + self.bottomItemView = bottomItemView + } + + let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: sheetEnvironment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let bottomItemSize = bottomItemView.update( + transition: transition, + component: bottomItem, + environment: {}, + containerSize: CGSize(width: containerSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) + ) + let bottomItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - bottomItemSize.width)) / 2.0, y: availableSize.height - bottomItemSize.height - bottomInsets.bottom), size: bottomItemSize) + if let view = bottomItemView.view { + if view.superview == nil { + self.bottomContainer.addSubview(view) + } + transition.setFrame(view: view, frame: bottomItemFrame) + } + } else if let bottomItemView = self.bottomItemView { + self.bottomItemView = nil + bottomItemView.view?.removeFromSuperview() + } + + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: availableSize.height - edgeEffectHeight), size: CGSize(width: fillingSize, height: edgeEffectHeight)) + transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + self.bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: transition) + if self.bottomEdgeEffectView.superview == nil { + self.bottomContainer.insertSubview(self.bottomEdgeEffectView, at: 0) + } + transition.setAlpha(view: self.bottomContainer, alpha: component.bottomItem != nil ? 1.0 : 0.0) + + + clippingY = availableSize.height + + var topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + if component.isFullscreen { + topInset = 0.0 + } + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation { + self.animateIn() + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index d69affeae4..be63c37d30 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -575,7 +575,7 @@ public class ContactsController: ViewController { } } - @objc private func sortPressed() { + @objc public func sortPressed() { self.sortButton.contextAction?(self.sortButton.containerNode, nil) } diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 680428ae45..363ddfa3ea 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -551,7 +551,7 @@ private final class ContactContextExtractedContentSource: ContextExtractedConten } } -private func presentContactAccessPicker(context: AccountContext) { +public func presentContactAccessPicker(context: AccountContext) { if #available(iOS 18.0, *), let rootViewController = context.sharedContext.mainWindow?.viewController?.view.window?.rootViewController { var dismissImpl: (() -> Void)? let pickerView = ContactAccessPickerHostingView(completionHandler: { [weak rootViewController] ids in diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 2253b63daa..6b0d5b9a50 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -835,6 +835,17 @@ private func makeLayerSubtreeSnapshotAsView(layer: CALayer) -> UIView? { } +public func findParentScrollView(view: UIView?) -> UIScrollView? { + if let view = view { + if let view = view as? UIScrollView { + return view + } + return findParentScrollView(view: view.superview) + } else { + return nil + } +} + public extension UIView { func snapshotContentTree(unhide: Bool = false, keepPortals: Bool = false, keepTransform: Bool = false) -> UIView? { let wasHidden = self.isHidden diff --git a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift index 3f8776710c..860398c94c 100644 --- a/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift +++ b/submodules/ItemListPeerActionItem/Sources/ItemListPeerActionItem.swift @@ -35,8 +35,9 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { let noInsets: Bool public let sectionId: ItemListSectionId public let action: (() -> Void)? + public let tag: Any? - public init(presentationData: ItemListPresentationData, style: ItemListStyle = .blocks, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage?, iconSignal: Signal? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?) { + public init(presentationData: ItemListPresentationData, style: ItemListStyle = .blocks, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage?, iconSignal: Signal? = nil, title: String, additionalBadgeIcon: UIImage? = nil, alwaysPlain: Bool = false, hasSeparator: Bool = true, sectionId: ItemListSectionId, height: ItemListPeerActionItemHeight = .peerList, color: ItemListPeerActionItemColor = .accent, noInsets: Bool = false, editing: Bool = false, action: (() -> Void)?, tag: Any? = nil) { self.presentationData = presentationData self.style = style self.systemStyle = systemStyle @@ -52,6 +53,7 @@ public class ItemListPeerActionItem: ListViewItem, ItemListItem { self.color = color self.sectionId = sectionId self.action = action + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -133,6 +135,10 @@ public final class ItemListPeerActionItemNode: ListViewItemNode { private let iconDisposable = MetaDisposable() + public var tag: ItemListItemTag? { + return self.item?.tag as? ItemListItemTag + } + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/ItemListUI/Sources/ItemListItem.swift b/submodules/ItemListUI/Sources/ItemListItem.swift index 44d4897eef..bbf501135c 100644 --- a/submodules/ItemListUI/Sources/ItemListItem.swift +++ b/submodules/ItemListUI/Sources/ItemListItem.swift @@ -33,6 +33,13 @@ public extension ItemListItem { public protocol ItemListItemNode { var tag: ItemListItemTag? { get } + + func displayHighlight() +} + +public extension ItemListItemNode { + func displayHighlight() { + } } public protocol ItemListItemFocusableNode { diff --git a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift index 8743226e96..5017682d7b 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListCheckboxItem.swift @@ -42,8 +42,9 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let action: () -> Void let deleteAction: (() -> Void)? + public let tag: Any? - public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, enabled: Bool = true, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil) { + public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, iconSize: CGSize? = nil, iconPlacement: IconPlacement = .default, title: String, subtitle: String? = nil, style: ItemListCheckboxItemStyle, color: ItemListCheckboxItemColor = .accent, textColor: TextColor = .primary, checked: Bool, enabled: Bool = true, zeroSeparatorInsets: Bool, sectionId: ItemListSectionId, action: @escaping () -> Void, deleteAction: (() -> Void)? = nil, tag: Any? = nil) { self.presentationData = presentationData self.systemStyle = systemStyle self.icon = icon @@ -60,6 +61,7 @@ public class ItemListCheckboxItem: ListViewItem, ItemListItem { self.sectionId = sectionId self.action = action self.deleteAction = deleteAction + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -127,6 +129,10 @@ public class ItemListCheckboxItemNode: ItemListRevealOptionsItemNode { return self.contentParentNode } + public var tag: ItemListItemTag? { + return self.item?.tag as? ItemListItemTag + } + public init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift index 5d518b5f14..fc62243b86 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListDisclosureItem.swift @@ -173,6 +173,7 @@ private let boldBadgeFont = Font.semibold(14.0) public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -215,6 +216,9 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true + self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false @@ -283,6 +287,20 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { transition.updateAlpha(node: self.arrowNode, alpha: hasContextMenu ? 0.5 : 1.0) } + public func displayHighlight() { + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightNode, at: 0) + } + + Queue.mainQueue().after(1.2, { + self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.highlightNode.removeFromSupernode() + }) + }) + } + public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode.textNode) let makeTitleWithEntitiesLayout = TextNodeWithEntities.asyncLayout(self.titleNode) @@ -600,6 +618,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor } if let titleWithEntitiesApply = titleWithEntitiesLayoutAndApply?.1, let context = item.context { @@ -671,6 +690,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.highlightNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift index f19047c2ea..b3b368ecfa 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSectionHeaderItem.swift @@ -59,10 +59,11 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { let actionText: String? let action: (() -> Void)? public let sectionId: ItemListSectionId + public let tag: ItemListItemTag? public let isAlwaysPlain: Bool = true - public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId) { + public init(presentationData: ItemListPresentationData, text: String, badge: String? = nil, badgeStyle: BadgeStyle? = nil, multiline: Bool = false, activityIndicator: ItemListSectionHeaderActivityIndicator = .none, accessoryText: ItemListSectionHeaderAccessoryText? = nil, actionText: String? = nil, action: (() -> Void)? = nil, sectionId: ItemListSectionId, tag: ItemListItemTag? = nil) { self.presentationData = presentationData self.text = text self.badge = badge @@ -73,6 +74,7 @@ public class ItemListSectionHeaderItem: ListViewItem, ItemListItem { self.actionText = actionText self.action = action self.sectionId = sectionId + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -125,6 +127,10 @@ public class ItemListSectionHeaderItemNode: ListViewItemNode { private let activateArea: AccessibilityAreaNode + public var tag: ItemListItemTag? { + return self.item?.tag + } + public init() { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index b9d897c106..52db0c1cc7 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -147,6 +147,7 @@ extension IconSwitchNode: ItemListSwitchNodeImpl { public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode + private let highlightNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode @@ -175,6 +176,9 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.backgroundColor = .white + + self.highlightNode = ASDisplayNode() + self.highlightNode.isLayerBacked = true self.maskNode = ASImageNode() self.maskNode.isUserInteractionEnabled = false @@ -234,6 +238,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { self.switchGestureNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } + public func displayHighlight() { + if self.backgroundNode.supernode != nil { + self.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode) + } else { + self.insertSubnode(self.highlightNode, at: 0) + } + + Queue.mainQueue().after(1.2, { + self.highlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.highlightNode.removeFromSupernode() + }) + }) + } + func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeTextLayout = TextNode.asyncLayout(self.textNode) @@ -400,14 +418,13 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor - strongSelf.switchNode.frameColor = item.presentationData.theme.list.itemSwitchColors.frameColor strongSelf.switchNode.contentColor = item.presentationData.theme.list.itemSwitchColors.contentColor strongSelf.switchNode.handleColor = item.presentationData.theme.list.itemSwitchColors.handleColor strongSelf.switchNode.positiveContentColor = item.presentationData.theme.list.itemSwitchColors.positiveColor strongSelf.switchNode.negativeContentColor = item.presentationData.theme.list.itemSwitchColors.negativeColor - strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor + strongSelf.highlightNode.backgroundColor = item.presentationData.theme.list.itemSearchHighlightColor } let _ = titleApply() @@ -465,6 +482,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: item.systemStyle == .glass) : nil transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))) + transition.updateFrame(node: strongSelf.highlightNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))) transition.updateFrame(node: strongSelf.maskNode, frame: strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight))) diff --git a/submodules/LegacyComponents/Sources/PGVideoMovie.m b/submodules/LegacyComponents/Sources/PGVideoMovie.m index c81b4c3def..94e41dcdff 100755 --- a/submodules/LegacyComponents/Sources/PGVideoMovie.m +++ b/submodules/LegacyComponents/Sources/PGVideoMovie.m @@ -121,7 +121,12 @@ NSString *const kYUVVideoRangeConversionForLAFragmentShaderString = SHADER_STRIN #pragma mark Initialization and teardown - (GPUImageRotationMode)rotationForTrack:(AVAsset *)asset { - AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo]; + if (tracks.count == 0) { + return kGPUImageNoRotation; + } + + AVAssetTrack *videoTrack = [tracks objectAtIndex:0]; CGAffineTransform t = [videoTrack preferredTransform]; if (t.a == -1 && t.d == -1) { diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m index 4efcfc1ef4..df7b150222 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerGalleryVideoItemView.m @@ -428,9 +428,9 @@ if (itemChanged) { [self _playerCleanup]; - if (!item.asFile) { - [_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]]; - } +// if (!item.asFile) { +// [_facesDisposable setDisposable:[[TGPaintFaceDetector detectFacesInItem:item.editableMediaItem editingContext:item.editingContext] startStrictWithNext:nil file:__FILE_NAME__ line:__LINE__]]; +// } } _scrubberView.allowsTrimming = false; diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 756a3d95f0..5a25b9755f 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -83,6 +83,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBackgroundComponent", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/CounterControllerTitleView", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift index d013511047..03f7048781 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchController.swift @@ -6,6 +6,7 @@ import SwiftSignalKit import TelegramPresentationData import AccountContext import SearchUI +import CounterControllerTitleView public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMembersSearchController { private let queue = Queue() @@ -49,7 +50,18 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.title = self.presentationData.strings.Channel_Members_Title + let title: String + switch params.mode { + case .ownershipTransfer: + //TODO:localize + title = "Appoint Another Owner" + let titleView = CounterControllerTitleView(theme: self.presentationData.theme) + titleView.title = CounterControllerTitle(title: title, counter: " ") + self.navigationItem.titleView = titleView + default: + title = self.presentationData.strings.Channel_Members_Title + self.title = title + } self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) @@ -74,16 +86,28 @@ public final class ChannelMembersSearchControllerImpl: ViewController, ChannelMe } strongSelf.presentationData = presentationData strongSelf.controllerNode.updatePresentationData(presentationData) + + if let titleView = strongSelf.navigationItem.titleView as? CounterControllerTitleView { + titleView.theme = presentationData.theme + } }) - let _ = (params.context.account.postbox.loadedPeerWithId(peerId) + let _ = (params.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.peerId)) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - strongSelf.title = strongSelf.presentationData.strings.Channel_Subscribers_Title + guard let self, let peer else { + return + } + switch self.mode { + case .ownershipTransfer: + if let titleView = self.navigationItem.titleView as? CounterControllerTitleView { + titleView.title = CounterControllerTitle(title: titleView.title.title, counter: peer.compactDisplayTitle) + } + default: + if case let .channel(channel) = peer, case .broadcast = channel.info { + self.title = self.presentationData.strings.Channel_Subscribers_Title } else { - strongSelf.title = strongSelf.presentationData.strings.Channel_Members_Title + self.title = self.presentationData.strings.Channel_Members_Title } } }) diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift index af8f419045..18b0ea2db7 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift @@ -390,6 +390,13 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { } } } + case .ownershipTransfer: + if peer.id == context.account.peerId { + continue + } + if let user = peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) { + continue + } } let renderedParticipant: RenderedChannelParticipant switch participant { @@ -531,7 +538,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode { var label: String? var enabled = true switch mode { - case .ban, .promote: + case .ban, .promote, .ownershipTransfer: if participant.peer.id == context.account.peerId { continue participantsLoop } diff --git a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift index a17a139642..29bacd3d7d 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift @@ -158,10 +158,10 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -172,7 +172,7 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { component.cancel(true) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) @@ -496,11 +496,11 @@ private final class PremiumGiftCodeSheetContent: CombinedComponent { ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 36.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0)) ) context.add(star - .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 + 6.0)) ) var originY: CGFloat = 0.0 diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index b8f679fe3b..783f4e1160 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -1,4 +1,5 @@ import Foundation +import Foundation import UIKit import Display import ComponentFlow @@ -38,6 +39,11 @@ import PremiumStarComponent import PremiumCoinComponent import EdgeEffect +public enum PremiumIntroEntryTag { + case doNotHideAds +} +private let doNotHideAdsTag = GenericComponentViewTag() + public enum PremiumSource: Equatable { public static func == (lhs: PremiumSource, rhs: PremiumSource) -> Bool { switch lhs { @@ -2677,7 +2683,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let _ = accountContext.engine.accountData.updateAdMessagesEnabled(enabled: value).startStandalone() state?.updated(transition: .immediate) })), - action: nil + action: nil, + tag: doNotHideAdsTag )))) let adsInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Business_AdsInfo, attributes: termsMarkdownAttributes, textAlignment: .natural @@ -3918,6 +3925,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { } private let screenContext: ScreenContext fileprivate let mode: Mode + private let focusOnItemTag: PremiumIntroEntryTag? private var didSetReady = false private let _ready = Promise() @@ -3932,13 +3940,14 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { private let overNavigationContainer: UIView - public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { - self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium) + public convenience init(context: AccountContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) { + self.init(screenContext: .accountContext(context), mode: mode, source: source, modal: modal, forceDark: forceDark, forceHasPremium: forceHasPremium, focusOnItemTag: focusOnItemTag) } - public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false) { + public init(screenContext: ScreenContext, mode: Mode = .premium, source: PremiumSource, modal: Bool = true, forceDark: Bool = false, forceHasPremium: Bool = false, focusOnItemTag: PremiumIntroEntryTag? = nil) { self.screenContext = screenContext self.mode = mode + self.focusOnItemTag = focusOnItemTag let presentationData = screenContext.presentationData @@ -4066,6 +4075,20 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { override public func viewDidLoad() { super.viewDidLoad() + + if let focusOnItemTag = self.focusOnItemTag { + Queue.mainQueue().after(0.1, { + switch focusOnItemTag { + case .doNotHideAds: + if let view = self.node.hostView.findTaggedView(tag: doNotHideAdsTag) as? ListActionItemComponent.View { + if let scrollView = findParentScrollView(view: view) { + view.displayHighlight() + scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.bounds.height), animated: true) + } + } + } + }) + } } public override func viewWillDisappear(_ animated: Bool) { diff --git a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift index 447f9b2f52..e9de0a0ac6 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitScreen.swift @@ -857,10 +857,10 @@ private final class LimitSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -872,7 +872,7 @@ private final class LimitSheetContent: CombinedComponent { component.cancel() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton @@ -1519,7 +1519,7 @@ private final class LimitSheetContent: CombinedComponent { } context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 36.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0)) ) var textSize: CGSize diff --git a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift index 7b357651ab..79f2737703 100644 --- a/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumPrivacyScreen.swift @@ -163,10 +163,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -177,7 +177,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/QrCodeUI/Sources/QrCodeScreen.swift b/submodules/QrCodeUI/Sources/QrCodeScreen.swift index 9c975cc28c..2f522a62dc 100644 --- a/submodules/QrCodeUI/Sources/QrCodeScreen.swift +++ b/submodules/QrCodeUI/Sources/QrCodeScreen.swift @@ -159,25 +159,25 @@ private final class SheetContent: CombinedComponent { textString = "" } - var contentSize = CGSize(width: context.availableSize.width, height: 36.0) + var contentSize = CGSize(width: context.availableSize.width, height: 38.0) let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + tintColor: theme.chat.inputPanel.panelControlColor ) )), action: { _ in component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/SettingsUI/BUILD b/submodules/SettingsUI/BUILD index 06cc3d6d12..8a40750407 100644 --- a/submodules/SettingsUI/BUILD +++ b/submodules/SettingsUI/BUILD @@ -135,6 +135,10 @@ swift_library( "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/AvatarEditorScreen", + "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift index 286ea150dc..91024939fa 100644 --- a/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift +++ b/submodules/SettingsUI/Sources/CachedFaqInstantPage.swift @@ -40,13 +40,22 @@ func faqSearchableItems(context: AccountContext, resolvedUrl: Signal Bool { + if let other = other as? AutodownloadMediaCategoryEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { case master(PresentationTheme, String, Bool) case dataUsageHeader(PresentationTheme, String) @@ -155,31 +172,31 @@ private enum AutodownloadMediaCategoryEntry: ItemListNodeEntry { case let .master(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleMaster(value) - }) + }, tag: AutodownloadMediaCategoryEntryTag.master) case let .dataUsageHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .dataUsageItem(theme, strings, value, customPosition, enabled): return AutodownloadDataUsagePickerItem(theme: theme, strings: strings, systemStyle: .glass, value: value, customPosition: customPosition, enabled: enabled, sectionId: self.section, updated: { preset in arguments.changePreset(preset) - }) + }, tag: AutodownloadMediaCategoryEntryTag.usage) case let .typesHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .photos(_, text, value, enabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Photos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.photo) - }) + }, tag: AutodownloadMediaCategoryEntryTag.photos) case let .stories(_, text, value, enabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Stories")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.story) - }) + }, tag: AutodownloadMediaCategoryEntryTag.stories) case let .videos(_, text, value, enabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Videos")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.video) - }) + }, tag: AutodownloadMediaCategoryEntryTag.videos) case let .files(_, text, value, enabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Files")?.precomposed(), title: text, enabled: enabled, label: value, labelStyle: .detailText, sectionId: self.section, style: .blocks, action: { arguments.customize(.file) - }) + }, tag: AutodownloadMediaCategoryEntryTag.files) case let .voiceMessagesInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } @@ -300,7 +317,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData: return entries } -func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType) -> ViewController { +func autodownloadMediaConnectionTypeController(context: AccountContext, connectionType: AutomaticDownloadConnectionType, focusOnItemTag: AutodownloadMediaCategoryEntryTag? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let arguments = AutodownloadMediaConnectionTypeControllerArguments(toggleMaster: { value in @@ -368,7 +385,7 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: autodownloadMediaConnectionTypeControllerEntries(presentationData: presentationData, connectionType: connectionType, settings: automaticMediaDownloadSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -379,5 +396,20 @@ func autodownloadMediaConnectionTypeController(context: AccountContext, connecti (controller.navigationController as? NavigationController)?.pushViewController(c) } } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift index acb2d8f720..82129ad887 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadDataUsagePickerItem.swift @@ -40,8 +40,9 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem { let enabled: Bool let sectionId: ItemListSectionId let updated: (AutomaticDownloadDataUsage) -> Void + let tag: ItemListItemTag? - init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: AutomaticDownloadDataUsage, customPosition: Int?, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (AutomaticDownloadDataUsage) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, value: AutomaticDownloadDataUsage, customPosition: Int?, enabled: Bool, sectionId: ItemListSectionId, updated: @escaping (AutomaticDownloadDataUsage) -> Void, tag: ItemListItemTag? = nil) { self.theme = theme self.strings = strings self.systemStyle = systemStyle @@ -50,6 +51,7 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem { self.enabled = enabled self.sectionId = sectionId self.updated = updated + self.tag = tag } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -86,7 +88,7 @@ final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem { } } -private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode { +private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -103,6 +105,10 @@ private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode { private var item: AutodownloadDataUsagePickerItem? private var layoutParams: ListViewItemLayoutParams? + public var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift index 280d73655c..53816700d3 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadSizeLimitItem.swift @@ -58,8 +58,9 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem { let range: Range? let sectionId: ItemListSectionId let updated: (Int64) -> Void + let tag: ItemListItemTag? - init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, decimalSeparator: String, text: String, value: Int64, range: Range?, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, decimalSeparator: String, text: String, value: Int64, range: Range?, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void, tag: ItemListItemTag? = nil) { self.theme = theme self.strings = strings self.systemStyle = systemStyle @@ -69,6 +70,7 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem { self.range = range self.sectionId = sectionId self.updated = updated + self.tag = tag } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -105,7 +107,7 @@ final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem { } } -private final class AutodownloadSizeLimitItemNode: ListViewItemNode { +private final class AutodownloadSizeLimitItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -119,6 +121,10 @@ private final class AutodownloadSizeLimitItemNode: ListViewItemNode { private var item: AutodownloadSizeLimitItem? private var layoutParams: ListViewItemLayoutParams? + public var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift index 717d16872d..22484f4a11 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/DataAndStorageSettingsController.swift @@ -90,6 +90,7 @@ public enum DataAndStorageEntryTag: ItemListItemTag, Equatable { case raiseToListen case autoSave(AutomaticSaveIncomingPeerType) case sensitiveContent + case useLessVoiceData public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? DataAndStorageEntryTag, self == other { @@ -409,7 +410,7 @@ private enum DataAndStorageEntry: ItemListNodeEntry { case let .useLessVoiceData(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleVoiceUseLessData(value) - }, tag: nil) + }, tag: DataAndStorageEntryTag.useLessVoiceData) case let .useLessVoiceDataInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .otherHeader(_, text): @@ -1012,6 +1013,20 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da update() }) } - + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Data and Storage/EnergySavingSettingsScreen.swift b/submodules/SettingsUI/Sources/Data and Storage/EnergySavingSettingsScreen.swift index 255af4d950..865fe18f0c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/EnergySavingSettingsScreen.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/EnergySavingSettingsScreen.swift @@ -10,7 +10,7 @@ import PresentationDataUtils import AccountContext import UndoUI -enum ItemType: CaseIterable { +public enum EnergySavingItemType: CaseIterable { case autoplayVideo case autoplayGif case loopStickers @@ -88,10 +88,10 @@ enum ItemType: CaseIterable { private final class EnergeSavingSettingsScreenArguments { let updateThreshold: (Int32) -> Void - let toggleItem: (ItemType) -> Void + let toggleItem: (EnergySavingItemType) -> Void let displayDisabledTooltip: () -> Void - init(updateThreshold: @escaping (Int32) -> Void, toggleItem: @escaping (ItemType) -> Void, displayDisabledTooltip: @escaping () -> Void) { + init(updateThreshold: @escaping (Int32) -> Void, toggleItem: @escaping (EnergySavingItemType) -> Void, displayDisabledTooltip: @escaping () -> Void) { self.updateThreshold = updateThreshold self.toggleItem = toggleItem self.displayDisabledTooltip = displayDisabledTooltip @@ -103,19 +103,31 @@ private enum EnergeSavingSettingsScreenSection: Int32 { case items } +public enum EnergySavingEntryTag: ItemListItemTag, Equatable { + case item(EnergySavingItemType) + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? EnergySavingEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry { enum StableId: Hashable { case allHeader case all case allFooter case itemsHeader - case item(ItemType) + case item(EnergySavingItemType) } case allHeader(Bool?) case all(Int32) case allFooter(String) - case item(index: Int, type: ItemType, value: Bool, enabled: Bool) + case item(index: Int, type: EnergySavingItemType, value: Bool, enabled: Bool) case itemsHeader var section: ItemListSectionId { @@ -197,7 +209,7 @@ private enum EnergeSavingSettingsScreenEntry: ItemListNodeEntry { arguments.toggleItem(type) }, activatedWhileDisabled: { arguments.displayDisabledTooltip() - }) + }, tag: EnergySavingEntryTag.item(type)) } } } @@ -241,14 +253,14 @@ private func energeSavingSettingsScreenEntries( } entries.append(.itemsHeader) - for type in ItemType.allCases { + for type in EnergySavingItemType.allCases { entries.append(.item(index: entries.count, type: type, value: settings.energyUsageSettings[keyPath: type.settingsKeyPath] && itemsEnabled, enabled: itemsEnabled)) } return entries } -public func energySavingSettingsScreen(context: AccountContext) -> ViewController { +public func energySavingSettingsScreen(context: AccountContext, focusOnItemTag: EnergySavingEntryTag? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? let _ = pushControllerImpl @@ -300,7 +312,7 @@ public func energySavingSettingsScreen(context: AccountContext) -> ViewControlle backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false ) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, emptyStateItem: nil, animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: energeSavingSettingsScreenEntries(presentationData: presentationData, settings: automaticMediaDownloadSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: true) return (controllerState, (listState, arguments)) } @@ -317,5 +329,20 @@ public func energySavingSettingsScreen(context: AccountContext) -> ViewControlle controller.present(UndoOverlayController(presentationData: presentationData, content: c, elevatedLayout: false, action: { _ in return false }), in: .current) } } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift index 1aa9295dac..e935359544 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/IntentsSettingsController.swift @@ -12,6 +12,20 @@ import AccountContext import TelegramIntents import AccountUtils +public enum IntentsEntryTag: ItemListItemTag, Equatable { + case suggested + case suggestBy + case reset + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? IntentsEntryTag, self == other { + return true + } else { + return false + } + } +} + private final class IntentsSettingsControllerArguments { let context: AccountContext let updateSettings: (@escaping (IntentsSettings) -> IntentsSettings) -> Void @@ -197,7 +211,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry { case let .contacts(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.updateSettings { $0.withUpdatedContacts(value) } - }) + }, tag: IntentsEntryTag.suggested) case let .savedMessages(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.updateSettings { $0.withUpdatedSavedMessages(value) } @@ -217,7 +231,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry { case let .suggestAll(_, text, value): return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSettings { $0.withUpdatedOnlyShared(false) } - }) + }, tag: IntentsEntryTag.suggestBy) case let .suggestOnlyShared(_, text, value): return ItemListCheckboxItem(presentationData: presentationData, systemStyle: .glass, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateSettings { $0.withUpdatedOnlyShared(true) } @@ -226,7 +240,7 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry { case let .resetAll(_, text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.resetAll() - }) + }, tag: IntentsEntryTag.reset) } } } @@ -261,7 +275,7 @@ private func intentsSettingsControllerEntries(context: AccountContext, presentat return entries } -public func intentsSettingsController(context: AccountContext) -> ViewController { +public func intentsSettingsController(context: AccountContext, focusOnItemTag: IntentsEntryTag? = nil) -> ViewController { var presentControllerImpl: ((ViewController) -> Void)? let arguments = IntentsSettingsControllerArguments(context: context, updateSettings: { f in @@ -317,5 +331,20 @@ public func intentsSettingsController(context: AccountContext) -> ViewController presentControllerImpl = { [weak controller] c in controller?.present(c, in: .window(.root)) } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 1ecf2768de..606fbfaf99 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -58,6 +58,21 @@ private enum ProxySettingsControllerEntryId: Equatable, Hashable { case server(String, Int32, ProxyServerConnection) } +public enum ProxySettingsEntryTag: ItemListItemTag, Equatable { + case edit + case useProxy + case shareList + case useForCalls + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ProxySettingsEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum ProxySettingsControllerEntry: ItemListNodeEntry { case enabled(PresentationTheme, String, Bool, Bool) case serversHeader(PresentationTheme, String) @@ -207,7 +222,7 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { } else { arguments.toggleEnabled(value) } - }) + }, tag: ProxySettingsEntryTag.useProxy) case let .serversHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .addServer(_, text, _): @@ -227,11 +242,11 @@ private enum ProxySettingsControllerEntry: ItemListNodeEntry { case let .shareProxyList(_, text): return ProxySettingsActionItem(presentationData: presentationData, systemStyle: .glass, title: text, sectionId: self.section, editing: false, action: { arguments.shareProxyList() - }) + }, tag: ProxySettingsEntryTag.shareList) case let .useForCalls(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: true, enabled: true, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleUseForCalls(value) - }) + }, tag: ProxySettingsEntryTag.useForCalls) case let .useForCallsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } @@ -308,12 +323,12 @@ public enum ProxySettingsControllerMode { case modal } -public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default) -> ViewController { +public func proxySettingsController(context: AccountContext, mode: ProxySettingsControllerMode = .default, focusOnItemTag: ProxySettingsEntryTag? = nil) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - return proxySettingsController(accountManager: context.sharedContext.accountManager, sharedContext: context.sharedContext, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData) + return proxySettingsController(accountManager: context.sharedContext.accountManager, sharedContext: context.sharedContext, context: context, postbox: context.account.postbox, network: context.account.network, mode: mode, presentationData: presentationData, updatedPresentationData: context.sharedContext.presentationData, focusOnItemTag: focusOnItemTag) } -public func proxySettingsController(accountManager: AccountManager, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal) -> ViewController { +public func proxySettingsController(accountManager: AccountManager, sharedContext: SharedAccountContext, context: AccountContext? = nil, postbox: Postbox, network: Network, mode: ProxySettingsControllerMode, presentationData: PresentationData, updatedPresentationData: Signal, focusOnItemTag: ProxySettingsEntryTag? = nil) -> ViewController { var pushControllerImpl: ((ViewController) -> Void)? var dismissImpl: (() -> Void)? let stateValue = Atomic(value: ProxySettingsControllerState()) @@ -332,6 +347,14 @@ public func proxySettingsController(accountManager: AccountManager Void)? let arguments = ProxySettingsControllerArguments(toggleEnabled: { value in @@ -431,7 +454,7 @@ public func proxySettingsController(accountManager: AccountManager Void + let tag: ItemListItemTag? - init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void) { + init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, title: String, icon: ProxySettingsActionIcon = .none, sectionId: ItemListSectionId, editing: Bool, action: @escaping () -> Void, tag: ItemListItemTag? = nil) { self.presentationData = presentationData self.systemStyle = systemStyle self.title = title @@ -29,6 +30,7 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem { self.editing = editing self.sectionId = sectionId self.action = action + self.tag = tag } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -77,7 +79,7 @@ final class ProxySettingsActionItem: ListViewItem, ItemListItem { } } -private final class ProxySettingsActionItemNode: ListViewItemNode { +private final class ProxySettingsActionItemNode: ListViewItemNode, ItemListItemNode { private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -89,6 +91,10 @@ private final class ProxySettingsActionItemNode: ListViewItemNode { private var item: ProxySettingsActionItem? + public var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift index 84f7b552cd..d45446860c 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/SaveIncomingMediaController.swift @@ -48,6 +48,20 @@ enum SaveIncomingMediaSection: ItemListSectionId { case deleteAllExceptions } +public enum SaveIncomingMediaEntryTag: ItemListItemTag, Equatable { + case maxVideoSize + case addException + case deleteExceptions + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? SaveIncomingMediaEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum SaveIncomingMediaEntry: ItemListNodeEntry { enum StableId: Hashable { case peer @@ -200,7 +214,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry { case let .videoSize(decimalSeparator, text, size): return AutodownloadSizeLimitItem(theme: presentationData.theme, strings: presentationData.strings, systemStyle: .glass, decimalSeparator: decimalSeparator, text: text, value: size, range: nil/*2 * 1024 * 1024 ..< (4 * 1024 * 1024 * 1024)*/, sectionId: self.section, updated: { value in arguments.updateMaximumVideoSize(value) - }) + }, tag: SaveIncomingMediaEntryTag.maxVideoSize) case let .videoInfo(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section) case let .exceptionsHeader(title): @@ -209,7 +223,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry { let icon: UIImage? = PresentationResourcesItemList.createGroupIcon(presentationData.theme) return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: icon, title: title, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { arguments.openAddException() - }) + }, tag: SaveIncomingMediaEntryTag.addException) case let .exceptionItem(_, peer, label): return ItemListPeerItem( presentationData: presentationData, @@ -248,7 +262,7 @@ private enum SaveIncomingMediaEntry: ItemListNodeEntry { case let .deleteAllExceptions(title): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.deleteAllExceptions() - }) + }, tag: SaveIncomingMediaEntryTag.deleteExceptions) } } } @@ -352,7 +366,7 @@ private func saveIncomingMediaControllerEntries(presentationData: PresentationDa return entries } -enum SaveIncomingMediaScope { +public enum SaveIncomingMediaScope { case peer(EnginePeer.Id) case addPeer(id: EnginePeer.Id, completion: (MediaAutoSaveConfiguration) -> Void) case peerType(AutomaticSaveIncomingPeerType) @@ -363,7 +377,7 @@ private struct SaveIncomingMediaControllerState: Equatable { var peerIdWithOptions: EnginePeer.Id? } -func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope) -> ViewController { +public func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMediaScope, focusOnItemTag: SaveIncomingMediaEntryTag? = nil) -> ViewController { let stateValue = Atomic(value: SaveIncomingMediaControllerState()) let statePromise = ValuePromise(stateValue.with { $0 }) let updateState: ((SaveIncomingMediaControllerState) -> SaveIncomingMediaControllerState) -> Void = { f in @@ -719,7 +733,7 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -743,6 +757,20 @@ func saveIncomingMediaController(context: AccountContext, scope: SaveIncomingMed controller?.dismiss() } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift index 63ec3ccf98..6ef26ec66f 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListController.swift @@ -8,9 +8,24 @@ import TelegramCore import TelegramPresentationData import AccountContext import SearchUI +import ItemListUI + +public enum LocalizationListEntryTag: ItemListItemTag, Equatable { + case showButton + case translateChats + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? LocalizationListEntryTag, self == other { + return true + } else { + return false + } + } +} public class LocalizationListController: ViewController { private let context: AccountContext + private let focusOnItemTag: LocalizationListEntryTag? private var controllerNode: LocalizationListControllerNode { return self.displayNode as! LocalizationListControllerNode @@ -31,8 +46,9 @@ public class LocalizationListController: ViewController { private var previousContentOffset: ListViewVisibleContentOffset? - public init(context: AccountContext) { + public init(context: AccountContext, focusOnItemTag: LocalizationListEntryTag? = nil) { self.context = context + self.focusOnItemTag = focusOnItemTag self.presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -125,7 +141,7 @@ public class LocalizationListController: ViewController { self?.present(c, in: .window(.root), with: a) }, push: { [weak self] c in self?.push(c) - }) + }, focusOnItemTag: self.focusOnItemTag) self.controllerNode.listNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { diff --git a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift index ec93cca612..6fbc28259b 100644 --- a/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift +++ b/submodules/SettingsUI/Sources/Language Selection/LocalizationListControllerNode.swift @@ -95,7 +95,7 @@ private enum LanguageListEntry: Comparable, Identifiable { case let .translate(text, value): return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, value: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in toggleShowTranslate(value) - }) + }, tag: LocalizationListEntryTag.showButton) case let .translateEntire(text, value, locked): return ItemListSwitchItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !locked, displayLocked: locked, sectionId: LanguageListSection.translate.rawValue, style: .blocks, updated: { value in if !locked { @@ -103,7 +103,7 @@ private enum LanguageListEntry: Comparable, Identifiable { } }, activatedWhileDisabled: { showPremiumInfo() - }) + }, tag: LocalizationListEntryTag.translateChats) case let .doNotTranslate(text, value): return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, title: text, label: value, sectionId: LanguageListSection.translate.rawValue, style: .blocks, action: { openDoNotTranslate() @@ -342,6 +342,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { private let requestDeactivateSearch: () -> Void private let present: (ViewController, Any?) -> Void private let push: (ViewController) -> Void + private var focusOnItemTag: LocalizationListEntryTag? private var didSetReady = false let _ready = ValuePromise() @@ -367,7 +368,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { } } - init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void) { + init(context: AccountContext, presentationData: PresentationData, navigationBar: NavigationBar, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void, push: @escaping (ViewController) -> Void, focusOnItemTag: LocalizationListEntryTag?) { self.context = context self.presentationData = presentationData self.presentationDataValue.set(.single(presentationData)) @@ -376,6 +377,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { self.requestDeactivateSearch = requestDeactivateSearch self.present = present self.push = push + self.focusOnItemTag = focusOnItemTag self.listNode = ListView() self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.list.blocksBackgroundColor, direction: true) @@ -741,6 +743,16 @@ final class LocalizationListControllerNode: ViewControllerTracingNode { if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(true) + + if let focusOnItemTag = strongSelf.focusOnItemTag { + strongSelf.focusOnItemTag = nil + + strongSelf.listNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + itemNode.displayHighlight() + } + } + } } } }) diff --git a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift index fe38fae279..5259d56163 100644 --- a/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift +++ b/submodules/SettingsUI/Sources/Notifications/NotificationsAndSoundsController.swift @@ -897,5 +897,20 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift index a0c2b9e619..ff09f55252 100644 --- a/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift +++ b/submodules/SettingsUI/Sources/NotificationsPeerCategoryController.swift @@ -77,9 +77,12 @@ private enum NotificationsPeerCategorySection: Int32 { } public enum NotificationsPeerCategoryEntryTag: ItemListItemTag { + case edit case enable case previews case sound + case important + case deleteExceptions public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? NotificationsPeerCategoryEntryTag, self == other { @@ -284,7 +287,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { case let .enableImportant(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateEnabledImportant(updatedValue) - }, tag: self.tag) + }, tag: NotificationsPeerCategoryEntryTag.important) case let .importantInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .optionsHeader(_, text): @@ -314,7 +317,7 @@ private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { case let .removeAllExceptions(theme, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { arguments.removeAllExceptions() - }) + }, tag: NotificationsPeerCategoryEntryTag.deleteExceptions) } } } @@ -617,6 +620,12 @@ public func notificationsPeerCategoryController(context: AccountContext, categor updatedMode(result.mode) } + if focusOnItemTag == .edit { + updateState { + $0.withUpdatedEditing(true) + } + } + let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal = { peerId, sound in return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound) |> deliverOnMainQueue } @@ -1096,5 +1105,20 @@ public func notificationsPeerCategoryController(context: AccountContext, categor pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift index f9669b2aa9..0e01ee3e27 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift @@ -203,13 +203,19 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s return entries } -public func blockedPeersController(context: AccountContext, blockedPeersContext: BlockedPeersContext) -> ViewController { +public func blockedPeersController(context: AccountContext, blockedPeersContext: BlockedPeersContext, forceEdit: Bool = false) -> ViewController { let statePromise = ValuePromise(BlockedPeersControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: BlockedPeersControllerState()) let updateState: ((BlockedPeersControllerState) -> BlockedPeersControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + if forceEdit { + updateState { + $0.withUpdatedEditing(true) + } + } + var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() diff --git a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift index cbde827577..9d290f5328 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/DataPrivacySettingsController.swift @@ -36,7 +36,7 @@ private final class DataPrivacyControllerArguments { } } -private enum PrivacyAndSecuritySection: Int32 { +private enum DataPrivacySection: Int32 { case contacts case frequentContacts case chats @@ -45,7 +45,24 @@ private enum PrivacyAndSecuritySection: Int32 { case bots } -private enum PrivacyAndSecurityEntry: ItemListNodeEntry { +public enum DataPrivacyEntryTag: ItemListItemTag, Equatable { + case deleteSynced + case syncContacts + case suggestContacts + case deleteCloudDrafts + case clearPaymentInfo + case linkPreviews + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? DataPrivacyEntryTag, self == other { + return true + } else { + return false + } + } +} + +private enum DataPrivacyEntry: ItemListNodeEntry { case contactsHeader(PresentationTheme, String) case deleteContacts(PresentationTheme, String, Bool) case syncContacts(PresentationTheme, String, Bool) @@ -70,17 +87,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { case .contactsHeader, .deleteContacts, .syncContacts, .syncContactsInfo: - return PrivacyAndSecuritySection.contacts.rawValue + return DataPrivacySection.contacts.rawValue case .frequentContacts, .frequentContactsInfo: - return PrivacyAndSecuritySection.frequentContacts.rawValue + return DataPrivacySection.frequentContacts.rawValue case .chatsHeader, .deleteCloudDrafts: - return PrivacyAndSecuritySection.chats.rawValue + return DataPrivacySection.chats.rawValue case .paymentHeader, .clearPaymentInfo, .paymentInfo: - return PrivacyAndSecuritySection.payments.rawValue + return DataPrivacySection.payments.rawValue case .secretChatLinkPreviewsHeader, .secretChatLinkPreviews, .secretChatLinkPreviewsInfo: - return PrivacyAndSecuritySection.secretChats.rawValue + return DataPrivacySection.secretChats.rawValue case .botList: - return PrivacyAndSecuritySection.bots.rawValue + return DataPrivacySection.bots.rawValue } } @@ -124,7 +141,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } } - static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { + static func ==(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool { switch lhs { case let .contactsHeader(lhsTheme, lhsText): if case let .contactsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { @@ -219,7 +236,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { } } - static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { + static func <(lhs: DataPrivacyEntry, rhs: DataPrivacyEntry) -> Bool { return lhs.stableId < rhs.stableId } @@ -231,17 +248,17 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case let .deleteContacts(_, text, value): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteContacts() - }) + }, tag: DataPrivacyEntryTag.deleteSynced) case let .syncContacts(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSyncContacts(updatedValue) - }) + }, tag: DataPrivacyEntryTag.syncContacts) case let .syncContactsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .frequentContacts(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSuggestFrequentContacts(updatedValue) - }) + }, tag: DataPrivacyEntryTag.suggestContacts) case let .frequentContactsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .chatsHeader(_, text): @@ -249,13 +266,13 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case let .deleteCloudDrafts(_, text, value): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: value ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.deleteCloudDrafts() - }) + }, tag: DataPrivacyEntryTag.deleteCloudDrafts) case let .paymentHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .clearPaymentInfo(_, text, enabled): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.clearPaymentInfo() - }) + }, tag: DataPrivacyEntryTag.clearPaymentInfo) case let .paymentInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .secretChatLinkPreviewsHeader(_, text): @@ -263,7 +280,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { case let .secretChatLinkPreviews(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateSecretChatLinkPreviews(updatedValue) - }) + }, tag: DataPrivacyEntryTag.linkPreviews) case let .secretChatLinkPreviewsInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case .botList: @@ -289,8 +306,8 @@ private struct DataPrivacyControllerState: Equatable { var deletingCloudDrafts: Bool = false } -private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [PrivacyAndSecurityEntry] { - var entries: [PrivacyAndSecurityEntry] = [] +private func dataPrivacyControllerEntries(presentationData: PresentationData, state: DataPrivacyControllerState, secretChatLinkPreviews: Bool?, synchronizeDeviceContacts: Bool, frequentContacts: Bool, hasBotSettings: Bool) -> [DataPrivacyEntry] { + var entries: [DataPrivacyEntry] = [] entries.append(.contactsHeader(presentationData.theme, presentationData.strings.Privacy_ContactsTitle)) entries.append(.deleteContacts(presentationData.theme, presentationData.strings.Privacy_ContactsReset, !state.deletingContacts)) @@ -317,7 +334,7 @@ private func dataPrivacyControllerEntries(presentationData: PresentationData, st return entries } -public func dataPrivacyController(context: AccountContext) -> ViewController { +public func dataPrivacyController(context: AccountContext, focusOnItemTag: DataPrivacyEntryTag? = nil) -> ViewController { let statePromise = ValuePromise(DataPrivacyControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: DataPrivacyControllerState()) let updateState: ((DataPrivacyControllerState) -> DataPrivacyControllerState) -> Void = { f in @@ -434,7 +451,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { } if canBegin { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_ContactsResetConfirmation, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultDestructiveAction, title: presentationData.strings.Common_Delete, action: { var begin = false updateState { state in var state = state @@ -461,7 +478,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.Privacy_ContactsReset_ContactsDeleted, timeout: nil, customUndoText: nil), elevatedLayout: false, action: { _ in return false })) })) - }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) + })])) } }, updateSyncContacts: { value in let _ = context.engine.contacts.updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: value).start() @@ -556,7 +573,7 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { let animateChanges = false - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: dataPrivacyControllerEntries(presentationData: presentationData, state: state, secretChatLinkPreviews: secretChatLinkPreviews, synchronizeDeviceContacts: synchronizeDeviceContacts, frequentContacts: suggestRecentPeers, hasBotSettings: hasBotSettings), style: .blocks, ensureVisibleItemTag: focusOnItemTag, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } @@ -572,5 +589,19 @@ public func dataPrivacyController(context: AccountContext) -> ViewController { controller?.push(c) } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift index 6b4d5aae1b..7ccb1d9560 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/GlobalAutoremoveScreen.swift @@ -38,6 +38,18 @@ private enum GlobalAutoremoveSection: Int32 { case general } +public enum GlobalAutoremoveEntryTag: ItemListItemTag, Equatable { + case setCustom + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? GlobalAutoremoveEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum GlobalAutoremoveEntry: ItemListNodeEntry { case header case sectionHeader(String) @@ -126,7 +138,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { case let .customAction(text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openCustomValue() - }) + }, tag: GlobalAutoremoveEntryTag.setCustom) case let .info(text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in arguments.infoLinkAction() @@ -188,7 +200,7 @@ private func globalAutoremoveScreenEntries(presentationData: PresentationData, s return entries } -public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void) -> ViewController { +public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void, focusOnItemTag: GlobalAutoremoveEntryTag? = nil) -> ViewController { let initialState = GlobalAutoremoveScreenState( additionalValues: Set([initialValue]), updatedValue: initialValue @@ -430,7 +442,7 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, let animateChanges = false let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true) return (controllerState, (listState, arguments)) } @@ -461,5 +473,19 @@ public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, controller?.dismiss() } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index 52ec1c3c80..6f5a54cde6 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -48,6 +48,19 @@ private enum IncomingMessagePrivacySection: Int32 { case exceptions } +public enum IncomingMessagePrivacyEntryTag: ItemListItemTag, Equatable { + case setPrice + case removeFee + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? IncomingMessagePrivacyEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum GlobalAutoremoveEntry: ItemListNodeEntry { case header case optionEverybody(value: GlobalPrivacySettings.NonContactChatsPrivacy) @@ -166,7 +179,7 @@ private enum GlobalAutoremoveEntry: ItemListNodeEntry { case let .exceptions(count): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.Privacy_Messages_RemoveFee, label: count > 0 ? "\(count)" : "", sectionId: self.section, style: .blocks, action: { arguments.openExceptions() - }) + }, tag: IncomingMessagePrivacyEntryTag.removeFee) case .exceptionsInfo: return ItemListTextItem(presentationData: presentationData, text: .markdown(presentationData.strings.Privacy_Messages_RemoveFeeInfo), sectionId: self.section) } @@ -212,7 +225,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD return entries } -public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController { +public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void, focusOnItemTag: IncomingMessagePrivacyEntryTag? = nil) -> ViewController { var disableFor: [EnginePeer.Id: SelectivePrivacyPeer] = [:] if case let .enableContacts(value, _, _, _) = exceptions { disableFor = value @@ -424,7 +437,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP let animateChanges = false let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true) return (controllerState, (listState, arguments)) } @@ -468,5 +481,19 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP controller?.dismiss() } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift index f5344b55f0..7ce3e638f5 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PasscodeOptionsController.swift @@ -35,6 +35,21 @@ private enum PasscodeOptionsSection: Int32 { case options } +public enum PasscodeOptionsEntryTag: ItemListItemTag, Equatable { + case togglePasscode + case changePasscode + case autolock + case touchId + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? PasscodeOptionsEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum PasscodeOptionsEntry: ItemListNodeEntry { case togglePasscode(PresentationTheme, String, Bool) case changePasscode(PresentationTheme, String) @@ -114,21 +129,21 @@ private enum PasscodeOptionsEntry: ItemListNodeEntry { if value { arguments.turnPasscodeOff() } - }) + }, tag: PasscodeOptionsEntryTag.togglePasscode) case let .changePasscode(_, title): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.changePasscode() - }) + }, tag: PasscodeOptionsEntryTag.changePasscode) case let .settingInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .autoLock(_, title, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.changePasscodeTimeout() - }) + }, tag: PasscodeOptionsEntryTag.autolock) case let .touchId(_, title, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.changeTouchId(value) - }) + }, tag: PasscodeOptionsEntryTag.touchId) } } } @@ -206,7 +221,7 @@ private func passcodeOptionsControllerEntries(presentationData: PresentationData return entries } -func passcodeOptionsController(context: AccountContext) -> ViewController { +func passcodeOptionsController(context: AccountContext, focusOnItemTag: PasscodeOptionsEntryTag? = nil) -> ViewController { let initialState = PasscodeOptionsControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -360,7 +375,7 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { |> map { presentationData, state, passcodeOptionsData -> (ItemListControllerState, (ItemListNodeState, Any)) in let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.PasscodeSettings_Title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, emptyStateItem: nil, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: passcodeOptionsControllerEntries(presentationData: presentationData, state: state, passcodeOptionsData: passcodeOptionsData), style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -383,6 +398,20 @@ func passcodeOptionsController(context: AccountContext) -> ViewController { (controller?.navigationController as? NavigationController)?.replaceTopController(c, animated: animated) } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 7fe1edbcac..ef55a10ce0 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -1629,5 +1629,19 @@ public func privacyAndSecurityController( pushControllerImpl?(controller, true) } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift index 7ff33d6e67..d7d5c84346 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/Recent Sessions/RecentSessionsController.swift @@ -89,6 +89,20 @@ private struct SortIndex: Comparable { } } +public enum RecentSessionsEntryTag: ItemListItemTag, Equatable { + case edit + case terminateOtherSessions + case autoTerminate + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? RecentSessionsEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum RecentSessionsEntry: ItemListNodeEntry { case header(SortIndex, String) case currentSessionHeader(SortIndex, String) @@ -341,7 +355,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case let .terminateOtherSessions(_, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { arguments.terminateOtherSessions() - }) + }, tag: RecentSessionsEntryTag.terminateOtherSessions) case let .terminateAllWebSessions(_, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.blockDestructiveIcon(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { arguments.terminateAllWebSessions() @@ -403,7 +417,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry { case let .ttlTimeout(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.setupAuthorizationTTL() - }, tag: PrivacyAndSecurityEntryTag.accountTimeout) + }, tag: RecentSessionsEntryTag.autoTerminate) } } } @@ -560,13 +574,19 @@ private func recentSessionsControllerEntries(presentationData: PresentationData, private final class RecentSessionsControllerImpl: ItemListController, RecentSessionsController { } -public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool) -> ViewController & RecentSessionsController { +public func recentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext, webSessionsContext: WebSessionsContext, websitesOnly: Bool, focusOnItemTag: RecentSessionsEntryTag? = nil) -> ViewController & RecentSessionsController { let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + if focusOnItemTag == .edit { + updateState { + $0.withUpdatedEditing(true) + } + } + activeSessionsContext.loadMore() webSessionsContext.loadMore() @@ -865,7 +885,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag, emptyStateItem: emptyStateItem, crossfadeState: crossfadeState, animateChanges: animateChanges, scrollEnabled: emptyStateItem == nil) return (controllerState, (listState, arguments)) } |> afterDisposed { @@ -891,5 +911,19 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont controller?.dismiss() } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift index 6913f3f3fe..ea4cafbc9f 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/SelectivePrivacySettingsController.swift @@ -136,6 +136,30 @@ private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], } } +public enum SelectivePrivacyEntryTag: ItemListItemTag, Equatable { + case neverAllow + case alwaysAllow + case lastSeenHideReadTime + case birthdaySetup + case giftsShowButton + case giftsAcceptedTypes + case photoSetPublic + case photoUpdatePublic + case photoRemovePublic + case callsP2PNeverAllow + case callsP2PAlwaysAllow + case callsP2P + case callsIntegration + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? SelectivePrivacyEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case forwardsPreviewHeader(PresentationTheme, String) case forwardsPreview(PresentationTheme, TelegramWallpaper, PresentationFontSize, PresentationChatBubbleCorners, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, String, Bool, String) @@ -606,11 +630,11 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case let .disableFor(_, title, value, isEnabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.main, false) - }) + }, tag: SelectivePrivacyEntryTag.neverAllow) case let .enableFor(_, title, value, isEnabled): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, enabled: isEnabled, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.main, true) - }) + }, tag: SelectivePrivacyEntryTag.alwaysAllow) case let .peersInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .callsP2PHeader(_, text): @@ -632,17 +656,17 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case let .callsP2PDisableFor(_, title, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.callP2P, false) - }) + }, tag: SelectivePrivacyEntryTag.callsP2PNeverAllow) case let .callsP2PEnableFor(_, title, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: title, label: value, sectionId: self.section, style: .blocks, action: { arguments.openSelective(.callP2P, true) - }) + }, tag: SelectivePrivacyEntryTag.callsP2PAlwaysAllow) case let .callsP2PPeersInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .callsIntegrationEnabled(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateCallIntegrationEnabled?(value) - }) + }, tag: SelectivePrivacyEntryTag.callsIntegration) case let .callsIntegrationInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .phoneDiscoveryHeader(_, text): @@ -662,18 +686,18 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case let .setPublicPhoto(theme, text): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.addPhotoIcon(theme), title: text, sectionId: self.section, height: .generic, color: .accent, editing: false, action: { arguments.setPublicPhoto?() - }) + }, tag: SelectivePrivacyEntryTag.photoSetPublic) case let .removePublicPhoto(_, text, peer, image, completeImage): return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: completeImage, iconSignal: completeImage == nil ? peerAvatarCompleteImage(account: arguments.context.account, peer: peer, forceProvidedRepresentation: true, representation: image?.representationForDisplayAtSize(PixelDimensions(width: 28, height: 28)), size: CGSize(width: 28.0, height: 28.0)) : nil, title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { arguments.removePublicPhoto?() - }) + }, tag: SelectivePrivacyEntryTag.photoRemovePublic) case let .publicPhotoInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in }) case let .hideReadTime(_, text, enabled, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.updateHideReadTime?(value) - }) + }, tag: SelectivePrivacyEntryTag.lastSeenHideReadTime) case let .hideReadTimeInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .subscribeToPremium(_, text): @@ -683,7 +707,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { case let .subscribeToPremiumInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .disallowedGiftsHeader(_, text): - return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section, tag: SelectivePrivacyEntryTag.giftsAcceptedTypes) case let .disallowedGiftsUnlimited(_, text, isLocked, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { updatedValue in if !isLocked { @@ -747,7 +771,7 @@ private enum SelectivePrivacySettingsEntry: ItemListNodeEntry { if available { arguments.displayLockedGiftsInfo() } - }) + }, tag: SelectivePrivacyEntryTag.giftsShowButton) case let .showGiftButtonInfo(_, text): let attributedString = NSMutableAttributedString(string: text, font: Font.regular(presentationData.fontSize.itemListBaseHeaderFontSize), textColor: presentationData.theme.list.freeTextColor) if let range = attributedString.string.range(of: "#") { @@ -1216,6 +1240,7 @@ public func selectivePrivacySettingsController( requestPublicPhotoSetup: ((@escaping (UIImage?) -> Void) -> Void)? = nil, requestPublicPhotoRemove: ((@escaping () -> Void) -> Void)? = nil, openedFromBirthdayScreen: Bool = false, + focusOnItemTag: SelectivePrivacyEntryTag? = nil, updated: @escaping (SelectivePrivacySettings, (SelectivePrivacySettings, VoiceCallSettings)?, Bool?, GlobalPrivacySettings?) -> Void ) -> ViewController { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings @@ -1910,5 +1935,20 @@ public func selectivePrivacySettingsController( dismissImpl = { [weak controller] in controller?.dismiss() } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift index 59b1af8a77..97c2ab8bca 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/TwoStepVerificationUnlockController.swift @@ -50,19 +50,15 @@ private enum TwoStepVerificationUnlockSettingsSection: Int32 { case email } -private enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag { +public enum TwoStepVerificationUnlockSettingsEntryTag: ItemListItemTag { case password + case change + case disable + case changeEmail - func isEqual(to other: ItemListItemTag) -> Bool { - if let other = other as? TwoStepVerificationUnlockSettingsEntryTag { - switch self { - case .password: - if case .password = other { - return true - } else { - return false - } - } + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? TwoStepVerificationUnlockSettingsEntryTag, self == other { + return true } else { return false } @@ -159,15 +155,15 @@ private enum TwoStepVerificationUnlockSettingsEntry: ItemListNodeEntry { case let .changePassword(_, text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupPassword() - }) + }, tag: TwoStepVerificationUnlockSettingsEntryTag.change) case let .turnPasswordOff(_, text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openDisablePassword() - }) + }, tag: TwoStepVerificationUnlockSettingsEntryTag.disable) case let .setupRecoveryEmail(_, text): return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.openSetupEmail() - }) + }, tag: TwoStepVerificationUnlockSettingsEntryTag.changeEmail) case let .passwordInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .pendingEmailConfirmInfo(_, text): @@ -283,7 +279,7 @@ public enum TwoStepVerificationUnlockSettingsControllerData: Equatable { case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) } -public func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false) -> ViewController { +public func twoStepVerificationUnlockSettingsController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, openSetupPasswordImmediately: Bool = false, focusOnItemTag: TwoStepVerificationUnlockSettingsEntryTag? = nil) -> ViewController { let initialState = TwoStepVerificationUnlockSettingsControllerState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -463,7 +459,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, return state } - replaceControllerImpl?(twoStepVerificationUnlockSettingsController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil)), true) + replaceControllerImpl?(twoStepVerificationUnlockSettingsController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil), focusOnItemTag: focusOnItemTag), true) }, error: { error in updateState { state in var state = state @@ -969,7 +965,7 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil, emptyStateItem: emptyStateItem, animateChanges: false) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: focusOnItemTag ?? (didAppear ? TwoStepVerificationUnlockSettingsEntryTag.password : nil), emptyStateItem: emptyStateItem, animateChanges: false) return (controllerState, (listState, arguments)) } @@ -1041,5 +1037,19 @@ public func twoStepVerificationUnlockSettingsController(context: AccountContext, })) } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift index 87c6c60f32..0463ea0ad6 100644 --- a/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift +++ b/submodules/SettingsUI/Sources/ReactionNotificationSettingsController.swift @@ -53,6 +53,22 @@ private enum ReactionNotificationSettingsSection: Int32 { case options } +public enum ReactionNotificationSettingsEntryTag: ItemListItemTag { + case messages + case stories + case showSender + case sound + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ReactionNotificationSettingsEntryTag, self == other { + return true + } else { + return false + } + } +} + + private enum ReactionNotificationSettingsEntry: ItemListNodeEntry { enum StableId: Hashable { case categoriesHeader @@ -169,23 +185,23 @@ private enum ReactionNotificationSettingsEntry: ItemListNodeEntry { arguments.toggleMessages(value) }, action: { arguments.openMessages() - }) + }, tag: ReactionNotificationSettingsEntryTag.messages) case let .stories(title, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, text: text, textColor: .accent, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleStories(value) }, action: { arguments.openStories() - }) + }, tag: ReactionNotificationSettingsEntryTag.stories) case let .optionsHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .previews(text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updatePreviews(value) - }) + }, tag: ReactionNotificationSettingsEntryTag.showSender) case let .sound(text, value, sound): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openSound(sound) - }, tag: self.tag) + }, tag: ReactionNotificationSettingsEntryTag.sound) } } } @@ -255,7 +271,8 @@ private struct ReactionNotificationSettingsState: Equatable { } public func reactionNotificationSettingsController( - context: AccountContext + context: AccountContext, + focusOnItemTag: ReactionNotificationSettingsEntryTag? = nil ) -> ViewController { var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? @@ -399,7 +416,7 @@ public func reactionNotificationSettingsController( let title: String = presentationData.strings.Notifications_Reactions_Title let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: focusOnItemTag) return (controllerState, (listState, arguments)) } @@ -411,5 +428,20 @@ public func reactionNotificationSettingsController( pushControllerImpl = { [weak controller] c in (controller?.navigationController as? NavigationController)?.pushViewController(c) } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift index 076f187448..ce0598ba31 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchItem.swift @@ -48,6 +48,8 @@ extension SettingsSearchableItemIcon { return PresentationResourcesSettings.support case .faq: return PresentationResourcesSettings.faq + case .tips: + return PresentationResourcesSettings.tips case .chatFolders: return PresentationResourcesSettings.chatFolders case .deleteAccount: @@ -56,24 +58,42 @@ extension SettingsSearchableItemIcon { return PresentationResourcesSettings.devices case .premium: return PresentationResourcesSettings.premium + case .business: + return PresentationResourcesSettings.business + case .stars: + return PresentationResourcesSettings.stars + case .ton: + return PresentationResourcesSettings.ton case .stories: return PresentationResourcesSettings.stories + case .myProfile: + return PresentationResourcesSettings.myProfile + case .gift: + return PresentationResourcesSettings.premiumGift + case .powerSaving: + return PresentationResourcesSettings.powerSaving } } } final class SettingsSearchInteraction { let openItem: (SettingsSearchableItem) -> Void - let deleteRecentItem: (SettingsSearchableItemId) -> Void + let openItemContextMenu: (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> Void + let deleteRecentItem: (AnyHashable) -> Void - init(openItem: @escaping (SettingsSearchableItem) -> Void, deleteRecentItem: @escaping (SettingsSearchableItemId) -> Void) { + init( + openItem: @escaping (SettingsSearchableItem) -> Void, + openItemContextMenu: @escaping (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> Void, + deleteRecentItem: @escaping (AnyHashable) -> Void + ) { self.openItem = openItem + self.openItemContextMenu = openItemContextMenu self.deleteRecentItem = deleteRecentItem } } private enum SettingsSearchEntryStableId: Hashable { - case result(SettingsSearchableItemId) + case result(AnyHashable) } private enum SettingsSearchEntry: Comparable, Identifiable { @@ -132,7 +152,7 @@ private func preparedSettingsSearchContainerTransition(theme: PresentationTheme, } private enum SettingsSearchRecentEntryStableId: Hashable { - case recent(SettingsSearchableItemId) + case recent(AnyHashable) } private enum SettingsSearchRecentEntry: Comparable, Identifiable { @@ -192,7 +212,11 @@ private enum SettingsSearchRecentEntry: Comparable, Identifiable { func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: SettingsSearchInteraction) -> ListViewItem { switch self { case let .recent(_, item, header): - return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: item.title, breadcrumbs: item.breadcrumbs, isFaq: false, action: { + var title = item.title + if title.isEmpty, let id = item.id.base as? String { + title = id + } + return SettingsSearchRecentItem(account: account, theme: theme, strings: strings, title: title, breadcrumbs: item.breadcrumbs, isFaq: false, action: { interaction.openItem(item) }, deleted: { interaction.deleteRecentItem(item.id) @@ -244,7 +268,19 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo private var presentationDataDisposable: Disposable? private let presentationDataPromise: Promise - public init(context: AccountContext, openResult: @escaping (SettingsSearchableItem) -> Void, resolvedFaqUrl: Signal, exceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasTwoStepAuth: Signal, twoStepAuthData: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) { + public init( + context: AccountContext, + openResult: @escaping (SettingsSearchableItem) -> Void, + openContextMenu: @escaping (SettingsSearchableItem, ContextExtractedContentContainingNode, CGRect, UIGestureRecognizer?) -> Void, + resolvedFaqUrl: Signal, + exceptionsList: Signal, + archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, + privacySettings: Signal, + hasTwoStepAuth: Signal, + twoStepAuthData: Signal, + activeSessionsContext: Signal, + webSessionsContext: Signal + ) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData self.presentationDataPromise = Promise(self.presentationData) @@ -273,15 +309,30 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo self.addSubnode(self.listNode) self.view.addSubview(self.edgeEffectView) - let interaction = SettingsSearchInteraction(openItem: { result in - addRecentSettingsSearchItem(engine: context.engine, item: result.id) - openResult(result) - }, deleteRecentItem: { id in - removeRecentSettingsSearchItem(engine: context.engine, item: id) - }) + let interaction = SettingsSearchInteraction( + openItem: { item in + addRecentSettingsSearchItem(engine: context.engine, item: item.id) + openResult(item) + }, + openItemContextMenu: { item, node, rect, gesture in + openContextMenu(item, node, rect, gesture) + }, + deleteRecentItem: { id in + removeRecentSettingsSearchItem(engine: context.engine, item: id) + } + ) let searchableItems = Promise<[SettingsSearchableItem]>() - searchableItems.set(settingsSearchableItems(context: context, notificationExceptionsList: exceptionsList, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings, hasTwoStepAuth: hasTwoStepAuth, twoStepAuthData: twoStepAuthData, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext)) + searchableItems.set(settingsSearchableItems( + context: context, + notificationExceptionsList: exceptionsList, + archivedStickerPacks: archivedStickerPacks, + privacySettings: privacySettings, + hasTwoStepAuth: hasTwoStepAuth, + twoStepAuthData: twoStepAuthData, + activeSessionsContext: activeSessionsContext, + webSessionsContext: webSessionsContext + )) let faqItems = Promise<[SettingsSearchableItem]>() faqItems.set(faqSearchableItems(context: context, resolvedUrl: resolvedFaqUrl, suggestAccountDeletion: false)) @@ -294,11 +345,11 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo let results = searchSettingsItems(items: searchableItems, query: query) let faqResults = searchSettingsItems(items: faqSearchableItems, query: query) let finalResults: [SettingsSearchableItem] - if faqResults.first?.id == .faq(1) { - finalResults = faqResults + results - } else { + //if faqResults.first?.id == .faq(1) { + // finalResults = faqResults + results + //} else { finalResults = results + faqResults - } + //} return .single((query, finalResults)) } else { return .single(nil) @@ -308,12 +359,12 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo self.recentListNode.isHidden = false - let previousRecentlySearchedItemOrder = Atomic<[SettingsSearchableItemId]>(value: []) + let previousRecentlySearchedItemOrder = Atomic<[AnyHashable]>(value: []) let fixedRecentlySearchedItems = settingsSearchRecentItems(engine: context.engine) - |> map { recentIds -> [SettingsSearchableItemId] in - var result: [SettingsSearchableItemId] = [] + |> map { recentIds -> [AnyHashable] in + var result: [AnyHashable] = [] let _ = previousRecentlySearchedItemOrder.modify { current in - var updated: [SettingsSearchableItemId] = [] + var updated: [AnyHashable] = [] for id in current { inner: for recentId in recentIds { if recentId == id { @@ -336,7 +387,7 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo let recentSearchItems = combineLatest(searchableItems.get(), fixedRecentlySearchedItems) |> map { searchableItems, recentItems -> [SettingsSearchableItem] in - let searchableItemsMap = searchableItems.reduce([SettingsSearchableItemId : SettingsSearchableItem]()) { (map, item) -> [SettingsSearchableItemId: SettingsSearchableItem] in + let searchableItemsMap = searchableItems.reduce([AnyHashable : SettingsSearchableItem]()) { (map, item) -> [AnyHashable: SettingsSearchableItem] in var map = map map[item.id] = item return map @@ -344,10 +395,10 @@ public final class SettingsSearchContainerNode: SearchDisplayControllerContentNo var result: [SettingsSearchableItem] = [] for itemId in recentItems { if let searchItem = searchableItemsMap[itemId] { - if case let .language(id) = searchItem.id, id > 0 { - } else { + //if case let .language(id) = searchItem.id, id > 0 { + //} else { result.append(searchItem) - } + //} } } return result @@ -620,7 +671,7 @@ private final class SettingsSearchItemNode: ItemListControllerSearchNode { } }) } - }, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, twoStepAuthData: self.twoStepAuthData, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in + }, openContextMenu: { _, _, _, _ in }, resolvedFaqUrl: self.resolvedFaqUrl, exceptionsList: self.exceptionsList, archivedStickerPacks: self.archivedStickerPacks, privacySettings: self.privacySettings, hasTwoStepAuth: self.hasTwoStepAuth, twoStepAuthData: self.twoStepAuthData, activeSessionsContext: self.activeSessionsContext, webSessionsContext: self.webSessionsContext), cancel: { [weak self] in self?.cancel() }, fieldStyle: placeholderNode.fieldStyle) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift index f2863b3b75..65d87903ed 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentItem.swift @@ -187,14 +187,14 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 16.0 - rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - var height = titleLayout.size.height - if subtitle.isEmpty { - height += 22.0 - } else { - height += 39.0 + var height: CGFloat = 30.0 + titleLayout.size.height + if !subtitle.isEmpty { + height += 9.0 } + let contentSize = CGSize(width: params.width, height: height) let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + return (nodeLayout, { [weak self] in var updatedTheme: PresentationTheme? if currentItem?.theme !== item.theme { @@ -215,7 +215,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { let _ = titleApply() let _ = subtitleApply() - let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0 + let titleY: CGFloat = subtitle.isEmpty ? 16.0 - UIScreenPixel : 11.0 strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size) strongSelf.subtitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size) @@ -223,7 +223,7 @@ class SettingsSearchRecentItemNode: ItemListRevealOptionsItemNode { let topHighlightInset: CGFloat = (firstWithHeader || !nodeLayout.insets.top.isZero) ? 0.0 : separatorHeight strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.contentSize.height + topHighlightInset)) strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) strongSelf.separatorNode.isHidden = last diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentQueries.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentQueries.swift index 38c7327b7f..d150d523ec 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchRecentQueries.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchRecentQueries.swift @@ -38,28 +38,34 @@ public final class RecentSettingsSearchQueryItem: Codable { } } -func addRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) { - let itemId = SettingsSearchRecentQueryItemId(item.index) - let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue, item: RecentSettingsSearchQueryItem(), removeTailIfCountExceeds: 100).start() +func addRecentSettingsSearchItem(engine: TelegramEngine, item: AnyHashable) { + guard let id = item.base as? String, let data = id.data(using: .ascii) else { + return + } + let itemId = MemoryBuffer(data: data) + let _ = engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId, item: RecentSettingsSearchQueryItem(), removeTailIfCountExceeds: 100).start() } -func removeRecentSettingsSearchItem(engine: TelegramEngine, item: SettingsSearchableItemId) { - let itemId = SettingsSearchRecentQueryItemId(item.index) - let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId.rawValue).start() +func removeRecentSettingsSearchItem(engine: TelegramEngine, item: AnyHashable) { + guard let id = item.base as? String, let data = id.data(using: .ascii) else { + return + } + let itemId = MemoryBuffer(data: data) + let _ = engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, id: itemId).start() } func clearRecentSettingsSearchItems(engine: TelegramEngine) { let _ = engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems).start() } -func settingsSearchRecentItems(engine: TelegramEngine) -> Signal<[SettingsSearchableItemId], NoError> { +func settingsSearchRecentItems(engine: TelegramEngine) -> Signal<[AnyHashable], NoError> { return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems)) - |> map { items -> [SettingsSearchableItemId] in - var result: [SettingsSearchableItemId] = [] + |> map { items -> [AnyHashable] in + var result: [AnyHashable] = [] for item in items { - let index = SettingsSearchRecentQueryItemId(item.id).value - if let itemId = SettingsSearchableItemId(index: index) { - result.append(itemId) + let data = item.id.makeData() + if let id = String(data: data, encoding: .utf8) { + result.append(id) } } return result diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift index c04b38af70..3b11d40df5 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchResultItem.swift @@ -77,6 +77,15 @@ private let titleFont = Font.regular(17.0) private let subtitleFont = Font.regular(13.0) class SettingsSearchResultItemNode: ListViewItemNode { + private let contextSourceNode: ContextExtractedContentContainingNode + private let containerNode: ContextControllerSourceNode + private let extractedBackgroundImageNode: ASImageNode + + private var extractedRect: CGRect? + private var nonExtractedRect: CGRect? + + private let offsetContainerNode: ASDisplayNode + private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode @@ -90,6 +99,15 @@ class SettingsSearchResultItemNode: ListViewItemNode { private var layoutParams: (ListViewItemLayoutParams, ItemListNeighbors)? init() { + self.contextSourceNode = ContextExtractedContentContainingNode() + self.containerNode = ContextControllerSourceNode() + + self.extractedBackgroundImageNode = ASImageNode() + self.extractedBackgroundImageNode.displaysAsynchronously = false + self.extractedBackgroundImageNode.alpha = 0.0 + + self.offsetContainerNode = ASDisplayNode() + self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -119,9 +137,48 @@ class SettingsSearchResultItemNode: ListViewItemNode { super.init(layerBacked: false, rotated: false, seeThrough: false) - self.addSubnode(self.iconNode) - self.addSubnode(self.titleNode) - self.addSubnode(self.subtitleNode) + self.containerNode.addSubnode(self.contextSourceNode) + self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode + self.addSubnode(self.containerNode) + + self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode) + self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode) + + self.offsetContainerNode.addSubnode(self.iconNode) + self.offsetContainerNode.addSubnode(self.titleNode) + self.offsetContainerNode.addSubnode(self.subtitleNode) + + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + + cancelParentGestures(view: strongSelf.view) + + item.interaction.openItemContextMenu(item.item, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture) + } + + self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in + guard let strongSelf = self, let item = strongSelf.item else { + return + } + + if isExtracted { + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 52.0, color: item.theme.list.plainBackgroundColor) + } + + if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { + let rect = isExtracted ? extractedRect : nonExtractedRect + transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect) + } + + transition.updateSublayerTransformOffset(layer: strongSelf.offsetContainerNode.layer, offset: CGPoint(x: isExtracted ? 12.0 : 0.0, y: 0.0)) + transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in + if !isExtracted { + self?.extractedBackgroundImageNode.image = nil + } + }) + } } func asyncLayout() -> (_ item: SettingsSearchResultItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { @@ -132,7 +189,7 @@ class SettingsSearchResultItemNode: ListViewItemNode { return { item, params, neighbors in var leftInset: CGFloat = params.leftInset - let contentInset: CGFloat = 58.0 + let contentInset: CGFloat = 60.0 let insets = itemListNeighborsGroupedInsets(neighbors, params) @@ -157,12 +214,11 @@ class SettingsSearchResultItemNode: ListViewItemNode { updateIconImage = item.icon } - var height = titleLayout.size.height - if subtitle.isEmpty { - height += 22.0 - } else { - height += 39.0 + var height: CGFloat = 30.0 + titleLayout.size.height + if !subtitle.isEmpty { + height += 9.0 } + let contentSize = CGSize(width: params.width, height: height) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) return (layout, { [weak self] animated in @@ -219,11 +275,29 @@ class SettingsSearchResultItemNode: ListViewItemNode { default: bottomStripeInset = 0.0 } + + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.offsetContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize) + + let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.contentSize.width - 16.0, height: layout.contentSize.height)) + let extractedRect = CGRect(origin: CGPoint(), size: layout.contentSize).insetBy(dx: 16.0 + params.leftInset, dy: 0.0) + strongSelf.extractedRect = extractedRect + strongSelf.nonExtractedRect = nonExtractedRect + + if strongSelf.contextSourceNode.isExtractedToContextPreview { + strongSelf.extractedBackgroundImageNode.frame = extractedRect + } else { + strongSelf.extractedBackgroundImageNode.frame = nonExtractedRect + } + strongSelf.contextSourceNode.contentRect = extractedRect + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight)) - let titleY: CGFloat = subtitle.isEmpty ? 11.0 : 11.0 + let titleY: CGFloat = subtitle.isEmpty ? 16.0 - UIScreenPixel : 11.0 transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY), size: titleLayout.size)) transition.updateFrame(node: strongSelf.subtitleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: titleY + titleLayout.size.height + 1.0), size: subtitleLayout.size)) diff --git a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift index 6ad46eadc0..6635834096 100644 --- a/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift +++ b/submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift @@ -23,6 +23,13 @@ import PremiumUI import StorageUsageScreen import PeerInfoStoryGridScreen import WallpaperGridScreen +import PeerNameColorScreen +import UndoUI +import PasskeysScreen +import ContextUI +import QuickReactionSetupController +import AvatarEditorScreen +import PeerSelectionScreen enum SettingsSearchableItemIcon { case profile @@ -39,152 +46,18 @@ enum SettingsSearchableItemIcon { case passport case support case faq + case tips case chatFolders case deleteAccount case devices case premium + case business + case stars + case ton case stories -} - -public enum SettingsSearchableItemId: Hashable { - case profile(Int32) - case proxy(Int32) - case savedMessages(Int32) - case calls(Int32) - case stickers(Int32) - case notifications(Int32) - case privacy(Int32) - case data(Int32) - case appearance(Int32) - case language(Int32) - case watch(Int32) - case passport(Int32) - case support(Int32) - case faq(Int32) - case chatFolders(Int32) - case deleteAccount(Int32) - case devices(Int32) - case premium(Int32) - case stories(Int32) - - private var namespace: Int32 { - switch self { - case .profile: - return 1 - case .proxy: - return 2 - case .savedMessages: - return 3 - case .calls: - return 4 - case .stickers: - return 5 - case .notifications: - return 6 - case .privacy: - return 7 - case .data: - return 8 - case .appearance: - return 9 - case .language: - return 10 - case .watch: - return 11 - case .passport: - return 12 - case .support: - return 14 - case .faq: - return 15 - case .chatFolders: - return 16 - case .deleteAccount: - return 17 - case .devices: - return 18 - case .premium: - return 19 - case .stories: - return 20 - } - } - - private var id: Int32 { - switch self { - case let .profile(id), - let .proxy(id), - let .savedMessages(id), - let .calls(id), - let .stickers(id), - let .notifications(id), - let .privacy(id), - let .data(id), - let .appearance(id), - let .language(id), - let .watch(id), - let .passport(id), - let .support(id), - let .faq(id), - let .chatFolders(id), - let .deleteAccount(id), - let .devices(id), - let .premium(id), - let .stories(id): - return id - } - } - - var index: Int64 { - return (Int64(self.namespace) << 32) | Int64(self.id) - } - - init?(index: Int64) { - let namespace = Int32((index >> 32) & 0x7fffffff) - let id = Int32(bitPattern: UInt32(index & 0xffffffff)) - switch namespace { - case 1: - self = .profile(id) - case 2: - self = .proxy(id) - case 3: - self = .savedMessages(id) - case 4: - self = .calls(id) - case 5: - self = .stickers(id) - case 6: - self = .notifications(id) - case 7: - self = .privacy(id) - case 8: - self = .data(id) - case 9: - self = .appearance(id) - case 10: - self = .language(id) - case 11: - self = .watch(id) - case 12: - self = .passport(id) - case 14: - self = .support(id) - case 15: - self = .faq(id) - case 16: - self = .chatFolders(id) - case 17: - self = .deleteAccount(id) - case 18: - self = .devices(id) - case 19: - self = .premium(id) - case 20: - self = .stories(id) - default: - return nil - } - } + case myProfile + case gift + case powerSaving } public enum SettingsSearchableItemPresentation { @@ -195,12 +68,35 @@ public enum SettingsSearchableItemPresentation { } public struct SettingsSearchableItem { - public let id: SettingsSearchableItemId + public let id: AnyHashable let title: String let alternate: [String] let icon: SettingsSearchableItemIcon let breadcrumbs: [String] + let isVisible: Bool public let present: (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void + + init( + id: AnyHashable, + title: String = "", + alternate: [String] = [], + icon: SettingsSearchableItemIcon = .privacy, + breadcrumbs: [String] = [], + isVisible: Bool = true, + present: @escaping (AccountContext, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void + ) { + self.id = id + self.title = title + self.alternate = alternate + self.icon = icon + self.breadcrumbs = breadcrumbs + self.isVisible = isVisible + self.present = present + } + + func withUpdatedTitle(_ title: String) -> SettingsSearchableItem { + return SettingsSearchableItem(id: self.id, title: title, alternate: self.alternate, icon: self.icon, breadcrumbs: self.breadcrumbs, isVisible: self.isVisible, present: self.present) + } } private func synonyms(_ string: String?) -> [String] { @@ -211,44 +107,350 @@ private func synonyms(_ string: String?) -> [String] { } } -private func profileSearchableItems(context: AccountContext, canAddAccount: Bool) -> [SettingsSearchableItem] { +private func profileSearchableItems( + context: AccountContext, + canAddAccount: Bool +) -> [SettingsSearchableItem] { let icon: SettingsSearchableItemIcon = .profile let strings = context.sharedContext.currentPresentationData.with { $0 }.strings var items: [SettingsSearchableItem] = [] - items.append(SettingsSearchableItem(id: .profile(2), title: strings.Settings_PhoneNumber, alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_PhoneNumber), icon: icon, breadcrumbs: [strings.EditProfile_Title], present: { context, _, present in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - var phoneNumber: String? - if case let .user(user) = peer { - phoneNumber = user.phone + + items.append( + SettingsSearchableItem( + id: "search", + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: false) + Queue.mainQueue().after(0.1) { + rootController.getSettingsController()?.tabBarActivateSearch() + } + } } - present(.push, PrivacyIntroController(context: context, mode: .changePhoneNumber(phoneNumber ?? ""), proceedAction: { - present(.push, ChangePhoneNumberController(context: context)) - })) - }) - })) - items.append(SettingsSearchableItem(id: .profile(3), title: strings.Settings_Username, alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_Username), icon: icon, breadcrumbs: [strings.EditProfile_Title], present: { context, _, present in - present(.modal, usernameSetupController(context: context)) - })) + ) + ) + items.append( + SettingsSearchableItem( + id: "edit", + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: true) + } + } + ) + ) + //TODO:highlight + items.append( + SettingsSearchableItem( + id: "edit/first-name", + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: true) + } + } + ) + ) + + //TODO:highlight + items.append( + SettingsSearchableItem( + id: "edit/last-name", + icon: .profile, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: true) + } + } + ) + ) + + //TODO:highlight + items.append( + SettingsSearchableItem( + id: "edit/bio", + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: true) + } + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "edit/change-number", + title: strings.Settings_PhoneNumber, + alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_PhoneNumber), + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + var phoneNumber: String? + if case let .user(user) = peer { + phoneNumber = user.phone + } + present(.push, PrivacyIntroController(context: context, mode: .changePhoneNumber(phoneNumber ?? ""), proceedAction: { + present(.push, ChangePhoneNumberController(context: context)) + })) + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "edit/username", + title: strings.Settings_Username, + alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_Username), + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + let controller = usernameSetupController(context: context) + present(.modal, controller) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "edit/your-color", + title: strings.Settings_YourColor, + alternate: [], + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + let controller = UserAppearanceScreen(context: context) + present(.push, controller) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "edit/channel", + title: strings.Settings_PersonalChannelItem, + alternate: [], + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let _ = (context.engine.peers.adminedPublicChannels(scope: .forPersonalProfile) + |> deliverOnMainQueue).start(next: { personalChannels in + let _ = (PeerSelectionScreen.initialData(context: context, channels: personalChannels) + |> deliverOnMainQueue).start(next: { initialData in + present(.push, PeerSelectionScreen(context: context, initialData: initialData, updatedPresentationData: nil, completion: { channel in + if initialData.channelId == channel?.peer.id { + return + } + + let toastText: String + var mappedChannel: TelegramPersonalChannel? + if let channel { + mappedChannel = TelegramPersonalChannel(peerId: channel.peer.id, subscriberCount: channel.subscriberCount.flatMap(Int32.init(clamping:)), topMessageId: nil) + if initialData.channelId != nil { + toastText = presentationData.strings.Settings_PersonalChannelUpdatedToast + } else { + toastText = presentationData.strings.Settings_PersonalChannelAddedToast + } + } else { + toastText = presentationData.strings.Settings_PersonalChannelRemovedToast + } + let _ = context.engine.accountData.updatePersonalChannel(personalChannel: mappedChannel).startStandalone() + + present(.immediate, UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: toastText, cancel: nil, destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) + })) + }) + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "edit/birthday", + title: strings.Settings_Birthday, + alternate: [], + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + presentSetupBirthday(context: context, present: present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "emoji-status", + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openSettings(edit: false) + Queue.mainQueue().justDispatch { + if let settingsScreen = rootController.getSettingsController() as? PeerInfoScreen { + settingsScreen.openEmojiStatusSetup() + } + } + } + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "profile-photo", + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.openPhotoSetup(completedWithUploadingImage: { [weak rootController] _, _ in + rootController?.openSettings(edit: false) + return nil + }) + } + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-photo/use-emoji", + icon: icon, + isVisible: false, + present: { context, _, present in + let controller = AvatarEditorScreen(context: context, inputData: AvatarEditorScreen.inputData(context: context, isGroup: false), peerType: .user, markup: nil) + controller.imageCompletion = { image, commit in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let settingsController = rootController.getSettingsController() as? PeerInfoScreen { + settingsController.updateProfilePhoto(image) + commit() + } + } + controller.videoCompletion = { image, url, values, markup, commit in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let settingsController = rootController.getSettingsController() as? PeerInfoScreen { + settingsController.updateProfileVideo(image, video: nil, values: nil, markup: markup) + commit() + } + } + present(.push, controller) + } + ) + ) + if canAddAccount { - items.append(SettingsSearchableItem(id: .profile(4), title: strings.Settings_AddAccount, alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_AddAccount), icon: icon, breadcrumbs: [strings.EditProfile_Title], present: { context, _, present in - let isTestingEnvironment = context.account.testingEnvironment - context.sharedContext.beginNewAuth(testingEnvironment: isTestingEnvironment) - })) + items.append( + SettingsSearchableItem( + id: "edit/add-account", + title: strings.Settings_AddAccount, + alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_AddAccount), + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, _, present in + let isTestingEnvironment = context.account.testingEnvironment + context.sharedContext.beginNewAuth(testingEnvironment: isTestingEnvironment) + } + ) + ) } - items.append(SettingsSearchableItem(id: .profile(5), title: strings.Settings_Logout, alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_Logout), icon: icon, breadcrumbs: [strings.EditProfile_Title], present: { context, navigationController, present in - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> deliverOnMainQueue).start(next: { peer in - var phoneNumber: String? - if case let .user(user) = peer { - phoneNumber = user.phone + items.append( + SettingsSearchableItem( + id: "edit/log-out", + title: strings.Settings_Logout, + alternate: synonyms(strings.SettingsSearch_Synonyms_EditProfile_Logout), + icon: icon, + breadcrumbs: [strings.EditProfile_Title], + present: { context, navigationController, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + var phoneNumber: String? + if case let .user(user) = peer { + phoneNumber = user.phone + } + if let navigationController { + present(.modal, logoutOptionsController(context: context, navigationController: navigationController, canAddAccounts: canAddAccount, phoneNumber: phoneNumber ?? "")) + } + }) } - if let navigationController = navigationController { - present(.modal, logoutOptionsController(context: context, navigationController: navigationController, canAddAccounts: canAddAccount, phoneNumber: phoneNumber ?? "")) + ) + ) + + items.append( + SettingsSearchableItem( + id: "profile-color", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context) + present(.push, controller) } - }) - })) + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/profile", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/profile/add-icons", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context, focusOnItemTag: .profileAddIcons) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/profile/use-gift", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context, focusOnItemTag: .profileUseGift) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/name", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context, focusOnItemTag: .name) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/name/add-icons", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context, focusOnItemTag: .nameAddIcons) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "profile-color/name/use-gift", + isVisible: false, + present: { context, _, present in + let controller = UserAppearanceScreen(context: context, focusOnItemTag: .nameUseGift) + present(.push, controller) + } + ) + ) + return items } @@ -256,31 +458,86 @@ private func devicesSearchableItems(context: AccountContext, activeSessionsConte let icon: SettingsSearchableItemIcon = .devices let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - var result: [SettingsSearchableItem] = [] + var items: [SettingsSearchableItem] = [] if let activeSessionsContext = activeSessionsContext { - result.append(SettingsSearchableItem(id: .devices(0), title: strings.Settings_Devices, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions) + [strings.PrivacySettings_AuthSessions], icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false)) - })) - result.append(SettingsSearchableItem(id: .devices(1), title: strings.AuthSessions_TerminateOtherSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Devices_TerminateOtherSessions), icon: icon, breadcrumbs: [strings.Settings_Devices], present: { context, _, present in - present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false)) - })) - result.append(SettingsSearchableItem(id: .devices(2), title: strings.AuthSessions_LinkDesktopDevice, alternate: synonyms(strings.SettingsSearch_Synonyms_Devices_LinkDesktopDevice), icon: icon, breadcrumbs: [strings.Settings_Devices], present: { context, _, present in - - present(.push, QrCodeScanScreen(context: context, subject: .authTransfer(activeSessionsContext: activeSessionsContext))) - })) + items.append( + SettingsSearchableItem( + id: "devices", + title: strings.Settings_Devices, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions) + [strings.PrivacySettings_AuthSessions], + icon: icon, + breadcrumbs: [], + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "devices/edit", + icon: icon, + isVisible: false, + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false, focusOnItemTag: .edit)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "devices/terminate-sessions", + title: strings.AuthSessions_TerminateOtherSessions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Devices_TerminateOtherSessions), + icon: icon, + breadcrumbs: [strings.Settings_Devices], + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false, focusOnItemTag: .terminateOtherSessions)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "devices/link-desktop", + title: strings.AuthSessions_LinkDesktopDevice, + alternate: synonyms(strings.SettingsSearch_Synonyms_Devices_LinkDesktopDevice), + icon: icon, + breadcrumbs: [strings.Settings_Devices], + present: { context, _, present in + present(.push, QrCodeScanScreen(context: context, subject: .authTransfer(activeSessionsContext: activeSessionsContext))) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "devices/auto-terminate", + icon: icon, + isVisible: false, + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: false, focusOnItemTag: .autoTerminate)) + } + ) + ) } - return result + return items } private func premiumSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { let icon: SettingsSearchableItemIcon = .premium let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - var result: [SettingsSearchableItem] = [] + var items: [SettingsSearchableItem] = [] - result.append(SettingsSearchableItem(id: .premium(0), title: strings.Settings_Premium, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium), icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, PremiumIntroScreen(context: context, source: .settings, modal: false)) - })) + items.append( + SettingsSearchableItem( + id: "premium", + title: strings.Settings_Premium, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + present(.push, PremiumIntroScreen(context: context, source: .settings, modal: false)) + } + ) + ) let presentDemo: (PremiumDemoScreen.Subject, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { subject, present in var replaceImpl: ((ViewController) -> Void)? @@ -294,76 +551,599 @@ private func premiumSearchableItems(context: AccountContext) -> [SettingsSearcha present(.push, controller) } - result.append(SettingsSearchableItem(id: .premium(1), title: strings.Premium_DoubledLimits, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_DoubledLimits), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { _, _, present in - presentDemo(.doubleLimits, present) - })) + let openResolvedUrl: (ResolvedUrl, NavigationController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { resolvedUrl, navigationController, present in + context.sharedContext.openResolvedUrl( + resolvedUrl, + context: context, + urlContext: .generic, + navigationController: navigationController, + forceExternal: false, + forceUpdate: false, + openPeer: { peer, navigation in }, + sendFile: nil, + sendSticker: nil, + sendEmoji: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { controller, arguments in + present(.push, controller) + }, + dismissInput: {}, + contentContext: nil, + progress: nil, + completion: nil + ) + } - result.append(SettingsSearchableItem(id: .premium(2), title: strings.Premium_UploadSize, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_UploadSize), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.moreUpload, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/doubled-limits", + title: strings.Premium_DoubledLimits, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_DoubledLimits), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { _, _, present in + presentDemo(.doubleLimits, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(3), title: strings.Premium_FasterSpeed, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_FasterSpeed), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.fasterDownload, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/unlimited-cloud-storage", + title: strings.Premium_UploadSize, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_UploadSize), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.moreUpload, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(4), title: strings.Premium_VoiceToText, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_VoiceToText), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.voiceToText, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/faster-download-speed", + title: strings.Premium_FasterSpeed, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_FasterSpeed), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.fasterDownload, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(5), title: strings.Premium_NoAds, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_NoAds), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.noAds, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/voice-to-text", + title: strings.Premium_VoiceToText, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_VoiceToText), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.voiceToText, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(6), title: strings.Premium_EmojiStatus, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_EmojiStatus), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.emojiStatus, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/no-ads", + title: strings.Premium_NoAds, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_NoAds), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.noAds, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(7), title: strings.Premium_Reactions, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Reactions), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.uniqueReactions, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/emoji-statuses", + title: strings.Premium_EmojiStatus, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_EmojiStatus), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.emojiStatus, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(8), title: strings.Premium_Stickers, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Stickers), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.premiumStickers, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/unique-reactions", + title: strings.Premium_Reactions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Reactions), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.uniqueReactions, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(9), title: strings.Premium_AnimatedEmoji, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_AnimatedEmoji), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.animatedEmoji, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/premium-stickers", + title: strings.Premium_Stickers, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Stickers), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.premiumStickers, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(10), title: strings.Premium_ChatManagement, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_ChatManagement), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.advancedChatManagement, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/animated-emoji", + title: strings.Premium_AnimatedEmoji, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_AnimatedEmoji), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.animatedEmoji, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(11), title: strings.Premium_Badge, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Badge), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.profileBadge, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/advanced-chat-management", + title: strings.Premium_ChatManagement, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_ChatManagement), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.advancedChatManagement, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(12), title: strings.Premium_Avatar, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Avatar), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.animatedUserpics, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/profile-badge", + title: strings.Premium_Badge, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Badge), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.profileBadge, present) + } + ) + ) - result.append(SettingsSearchableItem(id: .premium(13), title: strings.Premium_AppIcon, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_AppIcon), icon: icon, breadcrumbs: [strings.Settings_Premium], present: { context, _, present in - presentDemo(.appIcons, present) - })) + items.append( + SettingsSearchableItem( + id: "premium/animated-profile-pictures", + title: strings.Premium_Avatar, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_Avatar), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.animatedUserpics, present) + } + ) + ) - return result + items.append( + SettingsSearchableItem( + id: "premium/app-icons", + title: strings.Premium_AppIcon, + alternate: synonyms(strings.SettingsSearch_Synonyms_Premium_AppIcon), + icon: icon, + breadcrumbs: [strings.Settings_Premium], + present: { context, _, present in + presentDemo(.appIcons, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "business", + title: strings.Settings_Business, + alternate: [], + icon: .business, + breadcrumbs: [], + present: { context, _, present in + present(.push, PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "business/do-not-hide-ads", + title: strings.Business_DontHideAds, + alternate: [], + icon: .business, + breadcrumbs: [strings.Settings_Business], + present: { context, _, present in + present(.push, PremiumIntroScreen(context: context, mode: .business, source: .settings, modal: false, focusOnItemTag: .doNotHideAds)) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "stars", + title: strings.Settings_Stars, + alternate: [], + icon: .stars, + breadcrumbs: [], + present: { context, _, present in + guard let starsContext = context.starsContext else { + return + } + let controller = context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext) + present(.push, controller) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "stars/top-up", + icon: .stars, + isVisible: false, + present: { context, navigationController, present in + openResolvedUrl(.starsTopup(amount: nil, purpose: nil), navigationController, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "stars/stats", + icon: .stars, + isVisible: false, + present: { context, navigationController, present in + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: context.account.peerId, ton: false) + let controller = context.sharedContext.makeStarsStatisticsScreen(context: context, peerId: context.account.peerId, revenueContext: starsRevenueStatsContext) + present(.push, controller) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "stars/gift", + icon: .stars, + isVisible: false, + present: { context, navigationController, present in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let _ = combineLatest(queue: Queue.mainQueue(), + context.engine.payments.starsTopUpOptions(), + context.account.stateManager.contactBirthdays |> take(1) + ).start(next: { [weak navigationController] options, birthdays in + guard let starsContext = context.starsContext else { + return + } + let controller = context.sharedContext.makeStarsGiftController(context: context, birthdays: birthdays, completion: { [weak navigationController] peerIds in + guard let peerId = peerIds.first else { + return + } + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options, + purpose: .gift(peerId: peerId), + targetPeerId: nil, + customTheme: nil, + completion: { [weak navigationController] stars in + if let navigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactSelectionController) } + navigationController.setViewControllers(controllers, animated: true) + } + + Queue.mainQueue().after(2.0) { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .universal( + animation: "StarsSend", + scale: 0.066, + colors: [:], + title: nil, + text: presentationData.strings.Stars_Intro_StarsSent(Int32(stars)), + customUndoText: presentationData.strings.Stars_Intro_StarsSent_ViewChat, + timeout: nil + ), + elevatedLayout: false, + action: { [weak navigationController] action in + if case .undo = action, let navigationController { + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + return true + } + ) + present(.immediate, resultController) + } + } + ) + present(.push, purchaseController) + }) + present(.push, controller) + }) + } + ) + ) + + var canJoinRefProgram = false + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] { + if let value = value as? Double { + canJoinRefProgram = value != 0.0 + } else if let value = value as? Bool { + canJoinRefProgram = value + } + } + if canJoinRefProgram { + items.append( + SettingsSearchableItem( + id: "stars/earn", + title: strings.Monetization_EarnStarsInfo_Title, + alternate: [], + icon: .stars, + breadcrumbs: [strings.Settings_Stars], + present: { context, navigationController, present in + let _ = (context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: context, peerId: context.account.peerId, mode: .connectedPrograms) + |> deliverOnMainQueue).startStandalone(next: { initialData in + let controller = context.sharedContext.makeAffiliateProgramSetupScreen(context: context, initialData: initialData) + present(.push, controller) + }) + } + ) + ) + } + + items.append( + SettingsSearchableItem( + id: "ton", + title: strings.Settings_MyTon, + alternate: [], + icon: .ton, + breadcrumbs: [], + present: { context, _, present in + guard let tonContext = context.tonContext else { + return + } + let controller = context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: tonContext) + present(.push, controller) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "send-gift", + title: strings.Settings_SendGift, + alternate: [], + icon: .gift, + breadcrumbs: [], + present: { context, navigationController, present in + openResolvedUrl(.sendGift(peerId: nil), navigationController, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "send-gift/self", + title: strings.Settings_SendGift, + alternate: [], + icon: .gift, + breadcrumbs: [], + isVisible: false, + present: { context, navigationController, present in + openResolvedUrl(.sendGift(peerId: context.account.peerId), navigationController, present) + } + ) + ) + + return items } -private func storiesSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { - let icon: SettingsSearchableItemIcon = .stories +private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - var result: [SettingsSearchableItem] = [] - - result.append(SettingsSearchableItem(id: .stories(0), title: strings.Settings_MyStories, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium), icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .saved)) - })) + var items: [SettingsSearchableItem] = [] - result.append(SettingsSearchableItem(id: .stories(1), title: strings.Settings_StoriesArchive, alternate: synonyms(strings.SettingsSearch_Synonyms_Premium), icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .archive)) - })) + items.append( + SettingsSearchableItem( + id: "qr-code", + isVisible: false, + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } + ) + ) + + //TODO:fix + items.append( + SettingsSearchableItem( + id: "qr-code/share", + isVisible: false, + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false) + present(.push, controller) + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "qr-code/scan", + isVisible: false, + present: { context, _, present in + let scanController = QrCodeScanScreen(context: context, subject: .peer) + present(.push, scanController) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile", + title: strings.Settings_MyProfile, + alternate: [], + icon: .myProfile, + breadcrumbs: [], + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) + present(.push, controller) + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile/edit", + icon: .myProfile, + isVisible: false, + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) + present(.push, controller) + + Queue.mainQueue().justDispatch { + if let controller = controller as? PeerInfoScreen { + controller.activateEdit() + } + } + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile/gifts", + title: strings.Gift_Options_Gift_Filter_MyGifts, + alternate: [], + icon: .myProfile, + breadcrumbs: [strings.Settings_MyProfile], + present: { context, _, present in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer?._asPeer() else { + return + } + let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer, + mode: .myProfileGifts, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) + present(.push, controller) + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile/posts", + title: strings.Settings_MyStories, + alternate: [], + icon: .stories, + breadcrumbs: [strings.Settings_MyProfile], + present: { context, _, present in + present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .saved)) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile/posts/all-stories", + icon: .stories, + isVisible: false, + present: { context, _, present in + present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .saved)) + } + ) + ) + + //TODO:fix + items.append( + SettingsSearchableItem( + id: "my-profile/posts/add-album", + icon: .stories, + isVisible: false, + present: { context, _, present in + present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .saved)) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "my-profile/archived-posts", + title: strings.Settings_StoriesArchive, + alternate: [], + icon: .stories, + breadcrumbs: [strings.Settings_MyProfile], + present: { context, _, present in + present(.push, PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: .archive)) + } + ) + ) - return result + return items } @@ -371,50 +1151,383 @@ private func callSearchableItems(context: AccountContext) -> [SettingsSearchable let icon: SettingsSearchableItemIcon = .calls let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in - present(.push, CallListController(context: context, mode: .navigation)) + let presentCallSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, CallListEntryTag?) -> Void = { context, present, itemTag in + present(.push, CallListController(context: context, mode: .navigation, focusOnItemTag: itemTag)) } - return [ - SettingsSearchableItem(id: .calls(0), title: strings.CallSettings_RecentCalls, alternate: synonyms(strings.SettingsSearch_Synonyms_Calls_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentCallSettings(context, present) - }), - SettingsSearchableItem(id: .calls(1), title: strings.CallSettings_TabIcon, alternate: synonyms(strings.SettingsSearch_Synonyms_Calls_CallTab), icon: icon, breadcrumbs: [strings.CallSettings_RecentCalls], present: { context, _, present in - presentCallSettings(context, present) - }) + SettingsSearchableItem( + id: "calls", + title: strings.CallSettings_RecentCalls, + alternate: synonyms(strings.SettingsSearch_Synonyms_Calls_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentCallSettings(context, present, nil) + } + ), + SettingsSearchableItem( + id: "calls/all", + title: strings.Calls_All, + icon: icon, + breadcrumbs: [strings.CallSettings_RecentCalls], + isVisible: false, + present: { context, _, present in + presentCallSettings(context, present, nil) + } + ), + SettingsSearchableItem( + id: "calls/missed", + title: strings.Calls_Missed, + icon: icon, + breadcrumbs: [strings.CallSettings_RecentCalls], + isVisible: false, + present: { context, _, present in + presentCallSettings(context, present, .missed) + } + ), + SettingsSearchableItem( + id: "calls/edit", + icon: icon, + breadcrumbs: [strings.CallSettings_RecentCalls], + isVisible: false, + present: { context, _, present in + presentCallSettings(context, present, .edit) + } + ), + SettingsSearchableItem( + id: "calls/show-tab", + title: strings.CallSettings_TabIcon, + alternate: synonyms(strings.SettingsSearch_Synonyms_Calls_CallTab), + icon: icon, + breadcrumbs: [strings.CallSettings_RecentCalls], + present: { context, _, present in + presentCallSettings(context, present, .showTab) + } + ), + SettingsSearchableItem( + id: "calls/start-call", + title: strings.Calls_StartNewCall, + icon: icon, + isVisible: false, + present: { context, _, present in + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.startNewCall() + } + } + ) ] } +private func chatFoldersSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .chatFolders + let strings = context.sharedContext.currentPresentationData.with { $0 }.strings + + let presentChatFoldersSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, ChatListFilterPresetListEntryTag?) -> Void = { context, present, itemTag in + present(.push, chatListFilterPresetListController(context: context, mode: .default, focusOnItemTag: itemTag)) + } + + var items: [SettingsSearchableItem] = [] + + items.append( + SettingsSearchableItem( + id: "folders", + title: strings.Settings_ChatFolders, + alternate: synonyms(strings.SettingsSearch_Synonyms_ChatFolders), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentChatFoldersSettings(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "folders/edit", + icon: icon, + breadcrumbs: [strings.Settings_ChatFolders], + isVisible: false, + present: { context, _, present in + presentChatFoldersSettings(context, present, .edit) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "folders/create", + title: strings.ChatListFolderSettings_NewFolder, + icon: icon, + breadcrumbs: [strings.Settings_ChatFolders], + isVisible: false, + present: { context, _, present in + let filtersWithCounts = context.engine.peers.updatedChatListFilters() + |> distinctUntilChanged + |> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in + return .single(filters.map { filter -> (ChatListFilter, Int) in + return (filter, 0) + }) + } + + let _ = combineLatest( + queue: Queue.mainQueue(), + 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) + ), + filtersWithCounts |> take(1) + ).start(next: { result, filters in + let (accountPeer, limits, premiumLimits) = result + let isPremium = accountPeer?.isPremium ?? false + + let filters = filters.filter { filter in + if case .allChats = filter.0 { + return false + } + return true + } + + let limit = limits.maxFoldersCount + let premiumLimit = premiumLimits.maxFoldersCount + if filters.count >= premiumLimit { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { + return true + }) + present(.push, controller) + return + } else if filters.count >= limit && !isPremium { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(filters.count), action: { + let controller = PremiumIntroScreen(context: context, source: .folders) + replaceImpl?(controller) + return true + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + present(.push, controller) + return + } + present(.push, chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in })) + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "folders/add-recommended", + title: strings.ChatListFolderSettings_RecommendedFoldersSection.capitalized, + icon: icon, + breadcrumbs: [strings.Settings_ChatFolders], + isVisible: false, + present: { context, _, present in + presentChatFoldersSettings(context, present, .addRecommended) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "folders/show-tags", + title: strings.ChatListFilterList_ShowTags, + icon: icon, + breadcrumbs: [strings.Settings_ChatFolders], + isVisible: false, + present: { context, _, present in + presentChatFoldersSettings(context, present, .displayTags) + } + ) + ) + + return items +} + private func stickerSearchableItems(context: AccountContext, archivedStickerPacks: [ArchivedStickerPackItem]?) -> [SettingsSearchableItem] { let icon: SettingsSearchableItemIcon = .stickers let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in - present(.push, installedStickerPacksController(context: context, mode: .general, archivedPacks: archivedStickerPacks, updatedPacks: { _ in }, focusOnItemTag: itemTag)) + let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, InstalledStickerPacksControllerMode, InstalledStickerPacksEntryTag?) -> Void = { context, present, mode, itemTag in + present(.push, installedStickerPacksController(context: context, mode: mode, archivedPacks: archivedStickerPacks, updatedPacks: { _ in }, focusOnItemTag: itemTag)) } var items: [SettingsSearchableItem] = [] - - items.append(SettingsSearchableItem(id: .stickers(0), title: strings.ChatSettings_Stickers, alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentStickerSettings(context, present, nil) - })) - items.append(SettingsSearchableItem(id: .stickers(1), title: strings.Stickers_SuggestStickers, alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_SuggestStickers), icon: icon, breadcrumbs: [strings.ChatSettings_Stickers], present: { context, _, present in - presentStickerSettings(context, present, .suggestOptions) - })) - /*items.append(SettingsSearchableItem(id: .stickers(2), title: strings.StickerPacksSettings_AnimatedStickers, alternate: synonyms(strings.StickerPacksSettings_AnimatedStickers), icon: icon, breadcrumbs: [strings.ChatSettings_Stickers], present: { context, _, present in - presentStickerSettings(context, present, .loopAnimatedStickers) - }))*/ - items.append(SettingsSearchableItem(id: .stickers(3), title: strings.StickerPacksSettings_FeaturedPacks, alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_FeaturedPacks), icon: icon, breadcrumbs: [strings.ChatSettings_Stickers], present: { context, _, present in - present(.push, featuredStickerPacksController(context: context)) - })) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji", + title: strings.ChatSettings_Stickers, + alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentStickerSettings(context, present, .general, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/edit", + icon: icon, + isVisible: false, + present: { context, _, present in + presentStickerSettings(context, present, .general, .edit) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/suggest-by-emoji", + title: strings.Stickers_SuggestStickers, + alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_SuggestStickers), + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + presentStickerSettings(context, present, .general, .suggestOptions) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/trending", + title: strings.StickerPacksSettings_FeaturedPacks, + alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_FeaturedPacks), + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + present(.push, featuredStickerPacksController(context: context)) + } + ) + ) if !(archivedStickerPacks?.isEmpty ?? true) { - items.append(SettingsSearchableItem(id: .stickers(4), title: strings.StickerPacksSettings_ArchivedPacks, alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_ArchivedPacks), icon: icon, breadcrumbs: [strings.ChatSettings_Stickers], present: { context, _, present in - present(.push, archivedStickerPacksController(context: context, mode: .stickers, archived: archivedStickerPacks, updatedPacks: { _ in })) - })) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/archived", + title: strings.StickerPacksSettings_ArchivedPacks, + alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_ArchivedPacks), + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + present(.push, archivedStickerPacksController(context: context, mode: .stickers, archived: archivedStickerPacks, updatedPacks: { _ in })) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/archived/edit", + icon: icon, + isVisible: false, + present: { context, _, present in + present(.push, archivedStickerPacksController(context: context, mode: .stickers, archived: archivedStickerPacks, updatedPacks: { _ in }, forceEdit: true)) + } + ) + ) } - items.append(SettingsSearchableItem(id: .stickers(5), title: strings.MaskStickerSettings_Title, alternate: synonyms(strings.SettingsSearch_Synonyms_Stickers_Masks), icon: icon, breadcrumbs: [strings.ChatSettings_Stickers], present: { context, _, present in - present(.push, installedStickerPacksController(context: context, mode: .masks, archivedPacks: nil, updatedPacks: { _ in })) - })) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/large", + title: strings.Appearance_LargeEmoji, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_LargeEmoji), + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + presentStickerSettings(context, present, .general, .largeEmoji) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/dynamic-order", + title: strings.StickerPacksSettings_DynamicOrder, + alternate: [], + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + presentStickerSettings(context, present, .general, .dynamicOrder) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji", + title: strings.StickerPacksSettings_Emoji, + alternate: [], + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + presentStickerSettings(context, present, .emoji, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/edit", + icon: icon, + breadcrumbs: [strings.StickerPacksSettings_Emoji], + isVisible: false, + present: { context, _, present in + presentStickerSettings(context, present, .emoji, .edit) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/suggest", + icon: icon, + breadcrumbs: [strings.StickerPacksSettings_Emoji], + present: { context, _, present in + presentStickerSettings(context, present, .emoji, .suggestOptions) + } + ) + ) + if !(archivedStickerPacks?.isEmpty ?? true) { + //TODO:fix + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/archived", + title: strings.StickerPacksSettings_ArchivedPacks, + alternate: [], + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + isVisible: false, + present: { context, _, present in + present(.push, archivedStickerPacksController(context: context, mode: .emoji, archived: archivedStickerPacks, updatedPacks: { _ in })) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/archived/edit", + icon: icon, + isVisible: false, + present: { context, _, present in + present(.push, archivedStickerPacksController(context: context, mode: .emoji, archived: archivedStickerPacks, updatedPacks: { _ in }, forceEdit: true)) + } + ) + ) + } + + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/quick-reaction", + title: strings.Settings_QuickReactionSetup_Title, + alternate: [], + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers], + present: { context, _, present in + present(.push, quickReactionSetupController(context: context)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "appearance/stickers-and-emoji/emoji/quick-reaction/choose", + icon: icon, + breadcrumbs: [strings.ChatSettings_Stickers, strings.Settings_QuickReactionSetup_Title], + isVisible: false, + present: { context, _, present in + present(.push, quickReactionSetupController(context: context, focusOnItemTag: .choose)) + } + ) + ) + return items } @@ -425,14 +1538,21 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in present(.push, notificationsAndSoundsController(context: context, exceptionsList: exceptionsList, focusOnItemTag: itemTag)) } - - let exceptions = { () -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in + + let defaultStorySettings = PeerStoryNotificationSettings.default + let exceptions = { () -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in var users:[PeerId : NotificationExceptionWrapper] = [:] var groups: [PeerId : NotificationExceptionWrapper] = [:] - var channels:[PeerId : NotificationExceptionWrapper] = [:] + var channels: [PeerId : NotificationExceptionWrapper] = [:] + var stories: [PeerId : NotificationExceptionWrapper] = [:] + if let list = exceptionsList { for (key, value) in list.settings { if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId { + if value.storySettings != defaultStorySettings { + stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer)) + } + switch value.muteState { case .default: switch value.messageSound { @@ -465,7 +1585,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob } } } - return (.users(users), .groups(groups), .channels(channels)) + return (.users(users), .groups(groups), .channels(channels), .stories(stories)) } func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound { @@ -476,74 +1596,462 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob } } + let presentNotificationCategorySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, NotificationsPeerCategory, NotificationsPeerCategoryEntryTag?) -> Void = { context, present, category, itemTag in + let exceptionMode = exceptions() + let mode: NotificationExceptionMode + switch category { + case .privateChat: + mode = exceptionMode.0 + case .group: + mode = exceptionMode.1 + case .channel: + mode = exceptionMode.2 + case .stories: + mode = exceptionMode.3 + } + present(.push, notificationsPeerCategoryController(context: context, category: category, mode: mode, updatedMode: { _ in }, focusOnItemTag: itemTag)) + } + return [ - SettingsSearchableItem(id: .notifications(0), title: strings.Settings_NotificationsAndSounds, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentNotificationSettings(context, present, nil) - }), - SettingsSearchableItem(id: .notifications(3), title: strings.Notifications_MessageNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in - - let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.privateChats.sound), defaultSound: nil, completion: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.privateChats.sound = value - return settings - }).start() - }) - present(.modal, controller) - }), - SettingsSearchableItem(id: .notifications(4), title: strings.Notifications_MessageNotificationsExceptions, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications], present: { context, _, present in - present(.push, NotificationExceptionsController(context: context, mode: exceptions().0, updatedMode: { _ in})) - }), - SettingsSearchableItem(id: .notifications(7), title: strings.Notifications_GroupNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in - let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.groupChats.sound), defaultSound: nil, completion: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.groupChats.sound = value - return settings - }).start() - }) - present(.modal, controller) - }), - SettingsSearchableItem(id: .notifications(8), title: strings.Notifications_GroupNotificationsExceptions, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupNotifications], present: { context, _, present in - present(.push, NotificationExceptionsController(context: context, mode: exceptions().1, updatedMode: { _ in})) - }), - SettingsSearchableItem(id: .notifications(11), title: strings.Notifications_ChannelNotificationsSound, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in - let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.channels.sound), defaultSound: nil, completion: { value in - let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in - var settings = settings - settings.channels.sound = value - return settings - }).start() - }) - present(.modal, controller) - }), - SettingsSearchableItem(id: .notifications(12), title: strings.Notifications_MessageNotificationsExceptions, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_ChannelNotifications], present: { context, _, present in - present(.push, NotificationExceptionsController(context: context, mode: exceptions().2, updatedMode: { _ in})) - }), - SettingsSearchableItem(id: .notifications(13), title: strings.Notifications_InAppNotificationsSounds, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsSound), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .inAppSounds) - }), - SettingsSearchableItem(id: .notifications(14), title: strings.Notifications_InAppNotificationsVibrate, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .inAppVibrate) - }), - SettingsSearchableItem(id: .notifications(15), title: strings.Notifications_InAppNotificationsPreview, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications], present: { context, _, present in - presentNotificationSettings(context, present, .inAppPreviews) - }), - SettingsSearchableItem(id: .notifications(16), title: strings.Notifications_DisplayNamesOnLockScreen, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds], present: { context, _, present in - presentNotificationSettings(context, present, .displayNamesOnLockscreen) - }), - SettingsSearchableItem(id: .notifications(19), title: strings.Notifications_Badge_IncludeChannels, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Badge], present: { context, _, present in - presentNotificationSettings(context, present, .includeChannels) - }), - SettingsSearchableItem(id: .notifications(20), title: strings.Notifications_Badge_CountUnreadMessages, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Badge], present: { context, _, present in - presentNotificationSettings(context, present, .unreadCountCategory) - }), - SettingsSearchableItem(id: .notifications(21), title: strings.NotificationSettings_ContactJoined, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ContactJoined), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds], present: { context, _, present in - presentNotificationSettings(context, present, .joinedNotifications) - }), - SettingsSearchableItem(id: .notifications(22), title: strings.Notifications_ResetAllNotifications, alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ResetAllNotifications), icon: icon, breadcrumbs: [strings.Settings_NotificationsAndSounds], present: { context, _, present in - presentNotificationSettings(context, present, .reset) - }) + SettingsSearchableItem( + id: "notifications", + title: strings.Settings_NotificationsAndSounds, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentNotificationSettings(context, present, nil) + } + ), + + SettingsSearchableItem( + id: "notifications/accounts", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds], + isVisible: false, + present: { context, _, present in + presentNotificationSettings(context, present, .allAccounts) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats", + title: strings.Notifications_PrivateChats, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications.capitalized], + present: { context, _, present in + presentNotificationCategorySettings(context, present, .privateChat, nil) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/edit", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .privateChat, .edit) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/show", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .privateChat, .enable) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/preview", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .privateChat, .previews) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/sound", + title: strings.Notifications_MessageNotificationsSound, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + present: { context, _, present in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.privateChats.sound), defaultSound: nil, completion: { value in + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.privateChats.sound = value + return settings + }).start() + }) + present(.modal, controller) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/add-exception", + title: strings.Notifications_MessageNotificationsExceptions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + present: { context, _, present in + present(.push, NotificationExceptionsController(context: context, mode: exceptions().0, updatedMode: { _ in})) + } + ), + SettingsSearchableItem( + id: "notifications/private-chats/delete-exceptions", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .privateChat, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "notifications/groups", + title: strings.Notifications_GroupChats, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications.capitalized], + present: { context, _, present in + presentNotificationCategorySettings(context, present, .group, nil) + } + ), + SettingsSearchableItem( + id: "notifications/groups/edit", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .group, .edit) + } + ), + SettingsSearchableItem( + id: "notifications/groups/show", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .group, .enable) + } + ), + SettingsSearchableItem( + id: "notifications/groups/preview", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .group, .previews) + } + ), + SettingsSearchableItem( + id: "notifications/groups/sound", + title: strings.Notifications_GroupNotificationsSound, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + present: { context, _, present in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.groupChats.sound), defaultSound: nil, completion: { value in + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.groupChats.sound = value + return settings + }).start() + }) + present(.modal, controller) + } + ), + SettingsSearchableItem( + id: "notifications/groups/add-exception", + title: strings.Notifications_GroupNotificationsExceptions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_GroupNotificationsExceptions), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + present: { context, _, present in + present(.push, NotificationExceptionsController(context: context, mode: exceptions().1, updatedMode: { _ in})) + } + ), + SettingsSearchableItem( + id: "notifications/groups/delete-exceptions", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .group, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "notifications/channels", + title: strings.Notifications_Channels, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications.capitalized], + present: { context, _, present in + presentNotificationCategorySettings(context, present, .channel, nil) + } + ), + SettingsSearchableItem( + id: "notifications/channels/edit", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .channel, .edit) + } + ), + SettingsSearchableItem( + id: "notifications/channels/show", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .channel, .enable) + } + ), + SettingsSearchableItem( + id: "notifications/channels/preview", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .channel, .previews) + } + ), + SettingsSearchableItem( + id: "notifications/channels/sound", + title: strings.Notifications_ChannelNotificationsSound, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + present: { context, _, present in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.channels.sound), defaultSound: nil, completion: { value in + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.channels.sound = value + return settings + }).start() + }) + present(.modal, controller) + } + ), + SettingsSearchableItem( + id: "notifications/channels/add-exception", + title: strings.Notifications_MessageNotificationsExceptions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ChannelNotificationsExceptions), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + present: { context, _, present in + present(.push, NotificationExceptionsController(context: context, mode: exceptions().2, updatedMode: { _ in})) + } + ), + SettingsSearchableItem( + id: "notifications/channels/delete-exceptions", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .channel, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "notifications/stories", + title: strings.Notifications_Stories, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_MessageNotifications.capitalized], + present: { context, _, present in + presentNotificationCategorySettings(context, present, .stories, nil) + } + ), + SettingsSearchableItem( + id: "notifications/stories/new", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .stories, .enable) + } + ), + SettingsSearchableItem( + id: "notifications/stories/important", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .stories, .important) + } + ), + SettingsSearchableItem( + id: "notifications/stories/show-sender", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .stories, .previews) + } + ), + SettingsSearchableItem( + id: "notifications/stories/sound", + title: strings.Notifications_MessageNotificationsSound, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + present: { context, _, present in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: filteredGlobalSound(settings.privateChats.storySettings.sound), defaultSound: nil, completion: { value in + let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.privateChats.storySettings.sound = value + return settings + }).start() + }) + present(.modal, controller) + } + ), + SettingsSearchableItem( + id: "notifications/stories/add-exception", + title: strings.Notifications_MessageNotificationsExceptions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_MessageNotificationsExceptions), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + present: { context, _, present in + present(.push, NotificationExceptionsController(context: context, mode: exceptions().3, updatedMode: { _ in})) + } + ), + SettingsSearchableItem( + id: "notifications/stories/delete-exceptions", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Stories], + isVisible: false, + present: { context, _, present in + presentNotificationCategorySettings(context, present, .stories, .deleteExceptions) + } + ), + + SettingsSearchableItem( + id: "notifications/reactions", + title: strings.Notifications_Reactions, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds], + present: { context, _, present in + present(.push, reactionNotificationSettingsController(context: context)) + } + ), + SettingsSearchableItem( + id: "notifications/reactions/messages", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Reactions], + isVisible: false, + present: { context, _, present in + present(.push, reactionNotificationSettingsController(context: context, focusOnItemTag: .messages)) + } + ), + SettingsSearchableItem( + id: "notifications/reactions/stories", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Reactions], + isVisible: false, + present: { context, _, present in + present(.push, reactionNotificationSettingsController(context: context, focusOnItemTag: .stories)) + } + ), + SettingsSearchableItem( + id: "notifications/reactions/show-sender", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Reactions], + isVisible: false, + present: { context, _, present in + present(.push, reactionNotificationSettingsController(context: context, focusOnItemTag: .showSender)) + } + ), + SettingsSearchableItem( + id: "notifications/reactions/sound", + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Reactions], + isVisible: false, + present: { context, _, present in + present(.push, reactionNotificationSettingsController(context: context, focusOnItemTag: .sound)) + } + ), + SettingsSearchableItem( + id: "notifications/in-app-sounds", + title: strings.Notifications_InAppNotificationsSounds, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsSound), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications.capitalized], + present: { context, _, present in + presentNotificationSettings(context, present, .inAppSounds) + } + ), + SettingsSearchableItem( + id: "notifications/in-app-vibrate", + title: strings.Notifications_InAppNotificationsVibrate, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsVibrate), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications.capitalized], + present: { context, _, present in + presentNotificationSettings(context, present, .inAppVibrate) + } + ), + SettingsSearchableItem( + id: "notifications/in-app-preview", + title: strings.Notifications_InAppNotificationsPreview, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_InAppNotificationsPreview), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_InAppNotifications.capitalized], + present: { context, _, present in + presentNotificationSettings(context, present, .inAppPreviews) + } + ), + SettingsSearchableItem( + id: "notifications/lock-screen-names", + title: strings.Notifications_DisplayNamesOnLockScreen, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_DisplayNamesOnLockScreen), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds], + present: { context, _, present in + presentNotificationSettings(context, present, .displayNamesOnLockscreen) + } + ), + SettingsSearchableItem( + id: "notifications/include-channels", + title: strings.Notifications_Badge_IncludeChannels, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_BadgeIncludeMutedChannels), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Badge.capitalized], + present: { context, _, present in + presentNotificationSettings(context, present, .includeChannels) + } + ), + SettingsSearchableItem( + id: "notifications/count-unread-messages", + title: strings.Notifications_Badge_CountUnreadMessages, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_BadgeCountUnreadMessages), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds, strings.Notifications_Badge.capitalized], + present: { context, _, present in + presentNotificationSettings(context, present, .unreadCountCategory) + } + ), + SettingsSearchableItem( + id: "notifications/new-contacts", + title: strings.NotificationSettings_ContactJoined, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ContactJoined), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds], + present: { context, _, present in + presentNotificationSettings(context, present, .joinedNotifications) + } + ), + SettingsSearchableItem( + id: "notifications/reset", + title: strings.Notifications_ResetAllNotifications, + alternate: synonyms(strings.SettingsSearch_Synonyms_Notifications_ResetAllNotifications), + icon: icon, + breadcrumbs: [strings.Settings_NotificationsAndSounds], + present: { context, _, present in + presentNotificationSettings(context, present, .reset) + } + ) ] } @@ -555,7 +2063,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac present(.push, privacyAndSecurityController(context: context, focusOnItemTag: itemTag)) } - let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, kind, present in + let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, SelectivePrivacyEntryTag?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, kind, focusOnItemTag, present in let privacySignal: Signal if let privacySettings = privacySettings { privacySignal = .single(privacySettings) @@ -603,7 +2111,7 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac current = info.giftsAutoSave } - present(.push, selectivePrivacySettingsController(context: context, kind: kind, current: current, callSettings: callSettings != nil ? (info.voiceCallsP2P, callSettings!.0) : nil, voipConfiguration: callSettings?.1, callIntegrationAvailable: CallKitIntegration.isAvailable, updated: { updated, updatedCallSettings, _, _ in + present(.push, selectivePrivacySettingsController(context: context, kind: kind, current: current, callSettings: callSettings != nil ? (info.voiceCallsP2P, callSettings!.0) : nil, voipConfiguration: callSettings?.1, callIntegrationAvailable: CallKitIntegration.isAvailable, focusOnItemTag: focusOnItemTag, updated: { updated, updatedCallSettings, _, _ in if let (_, updatedCallSettings) = updatedCallSettings { let _ = updateVoiceCallSettingsSettingsInteractively(accountManager: context.sharedContext.accountManager, { _ in return updatedCallSettings @@ -613,8 +2121,65 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac }) } - let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in - present(.push, dataPrivacyController(context: context)) + let presentMessagesPrivacySettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, IncomingMessagePrivacyEntryTag?) -> Void = { context, present, itemTag in + let privacySignal: Signal + if let privacySettings = privacySettings { + privacySignal = .single(privacySettings) + } else { + privacySignal = context.engine.privacy.requestAccountPrivacySettings() + } + + let _ = (privacySignal + |> deliverOnMainQueue).start(next: { privacySettings in + let controller = incomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in + let _ = (context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue) + |> mapToSignal { _ -> Signal in } + |> deliverOnMainQueue).start() + }, focusOnItemTag: itemTag) + + present(.push, controller) + }) + } + + let presentMessageAutoRemove: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, GlobalAutoremoveEntryTag?) -> Void = { context, present, itemTag in + let privacySignal: Signal + if let privacySettings = privacySettings { + privacySignal = .single(privacySettings) + } else { + privacySignal = context.engine.privacy.requestAccountPrivacySettings() + } + let _ = (privacySignal + |> deliverOnMainQueue).start(next: { info in + let controller = globalAutoremoveScreen(context: context, initialValue: info.messageAutoremoveTimeout ?? 0, updated: { _ in }, focusOnItemTag: itemTag) + present(.push, controller) + }) + } + + let presentDataPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, DataPrivacyEntryTag?) -> Void = { context, present, itemTag in + present(.push, dataPrivacyController(context: context, focusOnItemTag: itemTag)) + } + + let presentBlockUser: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in + let blockedPeersContext = BlockedPeersContext(account: context.account, subject: .blocked) + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: [.onlyPrivateChats, .excludeSavedMessages, .removeSearchHeader, .excludeRecent, .doNotSearchMessages], title: presentationData.strings.BlockedUsers_SelectUserTitle)) + controller.peerSelected = { [weak controller] peer, _ in + let peerId = peer.id + + guard let strongController = controller else { + return + } + strongController.inProgress = true + let _ = (blockedPeersContext.add(peerId: peerId) + |> deliverOnMainQueue).start(completed: { + guard let strongController = controller else { + return + } + strongController.inProgress = false + strongController.dismiss() + }) + } + present(.push, controller) } let passcodeTitle: String @@ -633,75 +2198,1027 @@ private func privacySearchableItems(context: AccountContext, privacySettings: Ac passcodeAlternate = synonyms(strings.SettingsSearch_Synonyms_Privacy_Passcode) } - return ([ - SettingsSearchableItem(id: .privacy(0), title: strings.Settings_PrivacySettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentPrivacySettings(context, present, nil) - }), - SettingsSearchableItem(id: .privacy(1), title: strings.Settings_BlockedUsers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_BlockedUsers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - present(.push, blockedPeersController(context: context, blockedPeersContext: BlockedPeersContext(account: context.account, subject: .blocked))) - }), - SettingsSearchableItem(id: .privacy(2), title: strings.PrivacySettings_LastSeen, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_LastSeen), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentSelectivePrivacySettings(context, .presence, present) - }), - SettingsSearchableItem(id: .privacy(3), title: strings.Privacy_ProfilePhoto, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_ProfilePhoto), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentSelectivePrivacySettings(context, .profilePhoto, present) - }), - SettingsSearchableItem(id: .privacy(4), title: strings.Privacy_Forwards, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Forwards), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentSelectivePrivacySettings(context, .forwards, present) - }), - SettingsSearchableItem(id: .privacy(5), title: strings.Privacy_Calls, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Calls), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentSelectivePrivacySettings(context, .voiceCalls, present) - }), - SettingsSearchableItem(id: .privacy(6), title: strings.Privacy_GroupsAndChannels, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_GroupsAndChannels), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentSelectivePrivacySettings(context, .groupInvitations, present) - }), - SettingsSearchableItem(id: .privacy(7), title: passcodeTitle, alternate: passcodeAlternate, icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - let _ = passcodeOptionsAccessController(context: context, pushController: { c in - present(.push, c) - }, completion: { animated in - let controller = passcodeOptionsController(context: context) - if animated { + var items: [SettingsSearchableItem] = [] + items.append( + SettingsSearchableItem( + id: "privacy", + title: strings.Settings_PrivacySettings, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentPrivacySettings(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/blocked", + title: strings.Settings_BlockedUsers, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_BlockedUsers), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + present(.push, blockedPeersController(context: context, blockedPeersContext: BlockedPeersContext(account: context.account, subject: .blocked))) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/blocked/edit", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_BlockedUsers], + isVisible: false, + present: { context, _, present in + present(.push, blockedPeersController(context: context, blockedPeersContext: BlockedPeersContext(account: context.account, subject: .blocked), forceEdit: true)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/blocked/block-user", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_BlockedUsers], + isVisible: false, + present: { context, _, present in + presentBlockUser(context, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/blocked/block-user/chats", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_BlockedUsers], + isVisible: false, + present: { context, _, present in + presentBlockUser(context, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/blocked/block-user/contacts", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_BlockedUsers], + isVisible: false, + present: { context, _, present in + presentBlockUser(context, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/last-seen", + title: strings.PrivacySettings_LastSeen, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_LastSeen), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .presence, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/last-seen/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_LastSeen], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .presence, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/last-seen/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_LastSeen], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .presence, .alwaysAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/last-seen/hide-read-time", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_LastSeen], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .presence, .lastSeenHideReadTime, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos", + title: strings.Privacy_ProfilePhoto, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_ProfilePhoto), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_ProfilePhoto], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_ProfilePhoto], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, .alwaysAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos/set-public", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_ProfilePhoto], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, .photoSetPublic, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos/update-public", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_ProfilePhoto], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, .photoUpdatePublic, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/profile-photos/remove-public", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_ProfilePhoto], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, .photoRemovePublic, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/forwards", + title: strings.Privacy_Forwards, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Forwards), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .forwards, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/forwards/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Forwards], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .forwards, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/forwards/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Forwards], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .forwards, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/calls", + title: strings.Privacy_Calls, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Calls), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .alwaysAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/p2p", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .callsP2P, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/p2p/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .callsP2PNeverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/p2p/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .callsP2PAlwaysAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/calls/ios-integration", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Calls], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, .callsIntegration, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/invites", + title: strings.Privacy_GroupsAndChannels, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_GroupsAndChannels), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .groupInvitations, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/invites/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_GroupsAndChannels], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .groupInvitations, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/invites/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_GroupsAndChannels], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .groupInvitations, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/bio", + title: strings.Privacy_Bio, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .bio, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/bio/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Bio], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .bio, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/bio/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Bio], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .bio, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/birthday", + title: strings.Privacy_Birthday, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .birthday, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/birthday/add", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Birthday], + isVisible: false, + present: { context, _, present in + presentSetupBirthday(context: context, present: present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/birthday/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Birthday], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .birthday, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/birthday/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Birthday], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .birthday, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/gifts", + title: strings.Privacy_Gifts, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .giftsAutoSave, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/gifts/show-icon", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Gifts], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .giftsAutoSave, .giftsShowButton, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/gifts/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Gifts], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .giftsAutoSave, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/gifts/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Gifts], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .giftsAutoSave, .alwaysAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/gifts/accepted-types", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_Gifts], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .giftsAutoSave, .giftsAcceptedTypes, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/phone-number", + title: strings.Privacy_PhoneNumber, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .phoneNumber, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/phone-number/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_PhoneNumber], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .phoneNumber, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/phone-number/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_PhoneNumber], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .phoneNumber, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/saved-music", + title: strings.Privacy_SavedMusic, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .savedMusic, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/saved-music/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_SavedMusic], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .savedMusic, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/saved-music/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_SavedMusic], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .savedMusic, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/voice", + title: strings.Privacy_VoiceMessages, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceMessages, nil, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/voice/never", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_VoiceMessages], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceMessages, .neverAllow, present) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/voice/always", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Privacy_VoiceMessages], + isVisible: false, + present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceMessages, .alwaysAllow, present) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/messages", + title: strings.Settings_Privacy_Messages, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentMessagesPrivacySettings(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/messages/set-price", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_Privacy_Messages], + isVisible: false, + present: { context, _, present in + presentMessagesPrivacySettings(context, present, .setPrice) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/messages/remove-fee", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_Privacy_Messages], + isVisible: false, + present: { context, _, present in + presentMessagesPrivacySettings(context, present, .removeFee) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/passcode", + title: passcodeTitle, + alternate: passcodeAlternate, + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + let _ = passcodeOptionsAccessController(context: context, pushController: { c in + present(.push, c) + }, completion: { animated in + let controller = passcodeOptionsController(context: context) present(.push, controller) + }).start(next: { controller in + if let controller { + present(.push, controller) + } + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/passcode/disable", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, passcodeTitle], + isVisible: false, + present: { context, _, present in + let _ = passcodeOptionsAccessController(context: context, pushController: { c in + present(.push, c) + }, completion: { animated in + let controller = passcodeOptionsController(context: context, focusOnItemTag: .togglePasscode) + present(.push, controller) + }).start(next: { controller in + if let controller { + present(.push, controller) + } + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/passcode/change", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, passcodeTitle], + isVisible: false, + present: { context, _, present in + let _ = passcodeOptionsAccessController(context: context, pushController: { c in + present(.push, c) + }, completion: { animated in + let controller = passcodeOptionsController(context: context, focusOnItemTag: .changePasscode) + present(.push, controller) + }).start(next: { controller in + if let controller { + present(.push, controller) + } + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/passcode/auto-lock", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, passcodeTitle], + isVisible: false, + present: { context, _, present in + let _ = passcodeOptionsAccessController(context: context, pushController: { c in + present(.push, c) + }, completion: { animated in + let controller = passcodeOptionsController(context: context, focusOnItemTag: .autolock) + present(.push, controller) + }).start(next: { controller in + if let controller { + present(.push, controller) + } + }) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/passcode/face-id", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, passcodeTitle], + isVisible: false, + present: { context, _, present in + let _ = passcodeOptionsAccessController(context: context, pushController: { c in + present(.push, c) + }, completion: { animated in + let controller = passcodeOptionsController(context: context, focusOnItemTag: .touchId) + present(.push, controller) + }).start(next: { controller in + if let controller { + present(.push, controller) + } + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/2sv", + title: strings.PrivacySettings_TwoStepAuth, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_TwoStepAuth), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil))) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/2sv/change", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_TwoStepAuth], + isVisible: false, + present: { context, _, present in + present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil), focusOnItemTag: .change)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/2sv/disable", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_TwoStepAuth], + isVisible: false, + present: { context, _, present in + present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil), focusOnItemTag: .disable)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/2sv/change-email", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_TwoStepAuth], + isVisible: false, + present: { context, _, present in + present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil), focusOnItemTag: .changeEmail)) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/passkey", + title: strings.PrivacySettings_Passkey, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + Task { @MainActor in + let initialPasskeysData = await (context.engine.auth.passkeysData() |> take(1)).get() + let passkeysScreen = PasskeysScreen(context: context, displaySkip: false, initialPasskeysData: initialPasskeysData, passkeysDataUpdated: { _ in + }, completion: {}, cancel: {}) + present(.push, passkeysScreen) + } + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/passkey/create", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_Passkey], + isVisible: false, + present: { context, _, present in + Task { @MainActor in + let initialPasskeysData = await (context.engine.auth.passkeysData() |> take(1)).get() + let passkeysScreen = PasskeysScreen(context: context, displaySkip: false, initialPasskeysData: initialPasskeysData, forceCreate: true, passkeysDataUpdated: { _ in + }, completion: {}, cancel: {}) + present(.push, passkeysScreen) + } + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/auto-delete", + title: strings.Settings_AutoDeleteTitle, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentMessageAutoRemove(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/auto-delete/set-custom", + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.Settings_AutoDeleteTitle], + isVisible: false, + present: { context, _, present in + presentMessageAutoRemove(context, present, .setCustom) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/login-email", + title: strings.PrivacySettings_LoginEmail, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, navigationController, present in + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getTwoStepAuthData() { + settingsPromise = current } else { - present(.push, controller) + settingsPromise = Promise() + settingsPromise.set( + context.engine.auth.twoStepAuthData() + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + ) } - }).start(next: { controller in - if let controller = controller { - present(.push, controller) + + let _ = (settingsPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { twoStepAuthData in + let emailPattern = twoStepAuthData?.loginEmailPattern + let setupEmailImpl: (String?) -> Void = { emailPattern in + let controller = loginEmailSetupController(context: context, blocking: false, emailPattern: nil, navigationController: navigationController, completion: {}, dismiss: {}) + present(.push, controller) + } + if let emailPattern, !emailPattern.contains(" ") { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controller = textAlertController( + context: context, title: emailPattern, text: presentationData.strings.PrivacySettings_LoginEmailAlertText, actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.PrivacySettings_LoginEmailAlertChange, action: { + setupEmailImpl(emailPattern) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }) + ], actionLayout: .vertical + ) + present(.immediate, controller) + } else { + setupEmailImpl(nil) + } + }) + } + ) + ) + + if let webSessionsContext { + items.append( + SettingsSearchableItem( + id: "privacy/active-websites", + title: strings.PrivacySettings_WebSessions, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? context.engine.privacy.activeSessions(), webSessionsContext: webSessionsContext, websitesOnly: true)) } - }) - }), - SettingsSearchableItem(id: .privacy(8), title: strings.PrivacySettings_TwoStepAuth, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_TwoStepAuth), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - present(.push, twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: true, data: nil))) - }), - webSessionsContext == nil ? nil : SettingsSearchableItem(id: .privacy(10), title: strings.PrivacySettings_WebSessions, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_AuthSessions), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? context.engine.privacy.activeSessions(), webSessionsContext: webSessionsContext ?? context.engine.privacy.webSessions(), websitesOnly: true)) - }), - SettingsSearchableItem(id: .privacy(11), title: strings.PrivacySettings_DeleteAccountTitle, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentPrivacySettings(context, present, .accountTimeout) - }), - SettingsSearchableItem(id: .privacy(12), title: strings.PrivacySettings_DataSettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_Title), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(13), title: strings.Privacy_ContactsReset, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsReset), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(14), title: strings.Privacy_ContactsSync, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsSync), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(15), title: strings.Privacy_TopPeers, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_TopPeers), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(16), title: strings.Privacy_DeleteDrafts, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(17), title: strings.Privacy_PaymentsClearInfo, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], present: { context, _, present in - presentDataPrivacySettings(context, present) - }), - SettingsSearchableItem(id: .privacy(18), title: strings.Privacy_SecretChatsLinkPreviews, alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview), icon: icon, breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings, strings.Privacy_SecretChatsTitle], present: { context, _, present in - presentDataPrivacySettings(context, present) - }) - ] as [SettingsSearchableItem?]).compactMap { $0 } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/active-websites/edit", + isVisible: false, + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? context.engine.privacy.activeSessions(), webSessionsContext: webSessionsContext, websitesOnly: true, focusOnItemTag: .edit)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/active-websites/disconnect-all", + isVisible: false, + present: { context, _, present in + present(.push, recentSessionsController(context: context, activeSessionsContext: activeSessionsContext ?? context.engine.privacy.activeSessions(), webSessionsContext: webSessionsContext, websitesOnly: true, focusOnItemTag: .terminateOtherSessions)) + } + ) + ) + } + items.append( + SettingsSearchableItem( + id: "privacy/self-destruct", + title: strings.PrivacySettings_DeleteAccountTitle.capitalized, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_DeleteAccountIfAwayFor), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentPrivacySettings(context, present, .accountTimeout) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/archive-and-mute", + title: strings.PrivacySettings_AutoArchive, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentPrivacySettings(context, present, .autoArchive) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy/data-settings", + title: strings.PrivacySettings_DataSettings, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_Title), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/delete-synced", + title: strings.Privacy_ContactsReset, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsReset), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, .deleteSynced) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/sync-contacts", + title: strings.Privacy_ContactsSync, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ContactsSync), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, .syncContacts) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/suggest-contacts", + title: strings.Privacy_TopPeers, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_TopPeers), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, .suggestContacts) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/delete-cloud-drafts", + title: strings.Privacy_DeleteDrafts, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_DeleteDrafts), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, .deleteCloudDrafts) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/clear-payment-info", + title: strings.Privacy_PaymentsClearInfo, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_ClearPaymentsInfo), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + presentDataPrivacySettings(context, present, .clearPaymentInfo) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/link-previews", + title: strings.Privacy_SecretChatsLinkPreviews, + alternate: synonyms(strings.SettingsSearch_Synonyms_Privacy_Data_SecretChatLinkPreview), + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings, strings.Privacy_SecretChatsTitle.capitalized], + present: { context, _, present in + presentDataPrivacySettings(context, present, .linkPreviews) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "privacy/data-settings/bot-settings", + title: strings.Settings_BotListSettings, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_PrivacySettings, strings.PrivacySettings_DataSettings], + present: { context, _, present in + let controller = context.sharedContext.makeBotSettingsScreen(context: context, peerId: nil) + present(.push, controller) + } + ) + ) + return items } private func dataSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { @@ -712,81 +3229,494 @@ private func dataSearchableItems(context: AccountContext) -> [SettingsSearchable present(.push, dataAndStorageController(context: context, focusOnItemTag: itemTag)) } - return [ - SettingsSearchableItem(id: .data(0), title: strings.Settings_ChatSettings, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentDataSettings(context, present, nil) - }), - SettingsSearchableItem(id: .data(1), title: strings.ChatSettings_Cache, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_Title), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in - return storageUsageExceptionsScreen(context: context, category: category) - }) - present(.push, controller) - }), - SettingsSearchableItem(id: .data(2), title: strings.Cache_KeepMedia, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_KeepMedia), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], present: { context, _, present in - let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in - return storageUsageExceptionsScreen(context: context, category: category) - }) - present(.push, controller) - }), - SettingsSearchableItem(id: .data(3), title: strings.Cache_ClearCache, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_ClearCache), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], present: { context, _, present in - let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in - return storageUsageExceptionsScreen(context: context, category: category) - }) - present(.push, controller) - }), - SettingsSearchableItem(id: .data(4), title: strings.NetworkUsageSettings_Title, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_NetworkUsage), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - let mediaAutoDownloadSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) - |> map { sharedData -> MediaAutoDownloadSettings in - var automaticMediaDownloadSettings: MediaAutoDownloadSettings - if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { - automaticMediaDownloadSettings = value - } else { - automaticMediaDownloadSettings = .defaultSettings + let presentDataUsage: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, DataUsageEntryTag?) -> Void = { context, present, itemTag in + let mediaAutoDownloadSettings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]) + |> map { sharedData -> MediaAutoDownloadSettings in + var automaticMediaDownloadSettings: MediaAutoDownloadSettings + if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) { + automaticMediaDownloadSettings = value + } else { + automaticMediaDownloadSettings = .defaultSettings + } + return automaticMediaDownloadSettings + } + + let _ = (combineLatest( + accountNetworkUsageStats(account: context.account, reset: []), + mediaAutoDownloadSettings + ) + |> take(1) + |> deliverOnMainQueue).start(next: { stats, mediaAutoDownloadSettings in + var stats = stats + + if stats.resetWifiTimestamp == 0 { + var value = stat() + if stat(context.account.basePath, &value) == 0 { + stats.resetWifiTimestamp = Int32(value.st_ctimespec.tv_sec) } - return automaticMediaDownloadSettings } - let _ = (combineLatest( - accountNetworkUsageStats(account: context.account, reset: []), - mediaAutoDownloadSettings - ) - |> take(1) - |> deliverOnMainQueue).start(next: { stats, mediaAutoDownloadSettings in - var stats = stats - - if stats.resetWifiTimestamp == 0 { - var value = stat() - if stat(context.account.basePath, &value) == 0 { - stats.resetWifiTimestamp = Int32(value.st_ctimespec.tv_sec) - } - } - - present(.push, DataUsageScreen(context: context, stats: stats, mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: { isCellular in - return autodownloadMediaConnectionTypeController(context: context, connectionType: isCellular ? .cellular : .wifi) - })) - }) - }), - SettingsSearchableItem(id: .data(5), title: strings.ChatSettings_AutoDownloadUsingCellular, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle], present: { context, _, present in - present(.push, autodownloadMediaConnectionTypeController(context: context, connectionType: .cellular)) - }), - SettingsSearchableItem(id: .data(6), title: strings.ChatSettings_AutoDownloadUsingWiFi, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle], present: { context, _, present in - present(.push, autodownloadMediaConnectionTypeController(context: context, connectionType: .wifi)) - }), - SettingsSearchableItem(id: .data(7), title: strings.ChatSettings_AutoDownloadReset, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadReset), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - presentDataSettings(context, present, .automaticDownloadReset) - }), - SettingsSearchableItem(id: .data(10), title: strings.CallSettings_UseLessData, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_CallsUseLessData), icon: icon, breadcrumbs: [strings.Settings_ChatSettings, strings.Settings_CallSettings], present: { context, _, present in - present(.push, voiceCallDataSavingController(context: context)) - }), - SettingsSearchableItem(id: .data(12), title: strings.Settings_SaveEditedPhotos, alternate: synonyms(strings.SettingsSearch_Synonyms_Data_SaveEditedPhotos), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - presentDataSettings(context, present, .saveEditedPhotos) - }), - SettingsSearchableItem(id: .data(14), title: strings.ChatSettings_OpenLinksIn, alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_OpenLinksIn), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - present(.push, webBrowserSettingsController(context: context)) - }), - SettingsSearchableItem(id: .data(15), title: strings.ChatSettings_IntentsSettings, alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_IntentsSettings), icon: icon, breadcrumbs: [strings.Settings_ChatSettings], present: { context, _, present in - present(.push, intentsSettingsController(context: context)) - }), + present(.push, DataUsageScreen(context: context, stats: stats, mediaAutoDownloadSettings: mediaAutoDownloadSettings, makeAutodownloadSettingsController: { isCellular in + return autodownloadMediaConnectionTypeController(context: context, connectionType: isCellular ? .cellular : .wifi) + }, focusOnItemTag: itemTag)) + }) + } + + let presentStorageUsage: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, StorageUsageEntryTag?) -> Void = { context, present, itemTag in + let controller = StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in + return storageUsageExceptionsScreen(context: context, category: category) + }, focusOnItemTag: itemTag) + present(.push, controller) + } + + let presentSaveIncomingMediaSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, AutomaticSaveIncomingPeerType, SaveIncomingMediaEntryTag?) -> Void = { context, present, peerType, itemTag in + present(.push, saveIncomingMediaController(context: context, scope: .peerType(peerType), focusOnItemTag: itemTag)) + } + + let presentAutodownloadMediaSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, AutomaticDownloadConnectionType, AutodownloadMediaCategoryEntryTag?) -> Void = { context, present, connectionType, itemTag in + present(.push, autodownloadMediaConnectionTypeController(context: context, connectionType: connectionType, focusOnItemTag: itemTag)) + } + + return [ + SettingsSearchableItem( + id: "data", + title: strings.Settings_ChatSettings, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentDataSettings(context, present, nil) + } + ), + SettingsSearchableItem( + id: "data/storage", + title: strings.ChatSettings_Cache, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_Title), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentStorageUsage(context, present, nil) + } + ), + SettingsSearchableItem( + id: "data/storage/edit", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], + isVisible: false, + present: { context, _, present in + presentStorageUsage(context, present, .edit) + } + ), + SettingsSearchableItem( + id: "data/storage/auto-remove", + title: strings.Cache_KeepMedia, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_KeepMedia), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], + present: { context, _, present in + presentStorageUsage(context, present, .autoRemove) + } + ), + SettingsSearchableItem( + id: "data/storage/clear-cache", + title: strings.Cache_ClearCache, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_Storage_ClearCache), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], + present: { context, _, present in + presentStorageUsage(context, present, .clearCache) + } + ), + SettingsSearchableItem( + id: "data/max-cache", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_Cache], + isVisible: false, + present: { context, _, present in + presentStorageUsage(context, present, .maxCache) + } + ), + SettingsSearchableItem( + id: "data/usage", + title: strings.NetworkUsageSettings_Title, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_NetworkUsage), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentDataUsage(context, present, nil) + } + ), + SettingsSearchableItem( + id: "data/usage/mobile", + icon: icon, + isVisible: false, + present: { context, _, present in + presentDataUsage(context, present, .mobile) + } + ), + SettingsSearchableItem( + id: "data/usage/wifi", + icon: icon, + isVisible: false, + present: { context, _, present in + presentDataUsage(context, present, .wifi) + } + ), + SettingsSearchableItem( + id: "data/usage/reset", + icon: icon, + isVisible: false, + present: { context, _, present in + presentDataUsage(context, present, .reset) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile", + title: strings.ChatSettings_AutoDownloadUsingCellular, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadUsingCellular), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized], + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, nil) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/enable", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .master) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/usage", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .usage) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/photos", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .photos) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/stories", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .stories) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/videos", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .videos) + } + ), + SettingsSearchableItem( + id: "data/auto-download/mobile/files", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .cellular, .files) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi", + title: strings.ChatSettings_AutoDownloadUsingWiFi, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadUsingWifi), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized], + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, nil) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/enable", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .master) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/usage", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .usage) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/photos", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .photos) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/stories", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .stories) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/videos", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .videos) + } + ), + SettingsSearchableItem( + id: "data/auto-download/wifi/files", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_AutoDownloadTitle.capitalized, strings.ChatSettings_AutoDownloadUsingCellular], + isVisible: false, + present: { context, _, present in + presentAutodownloadMediaSettings(context, present, .wifi, .files) + } + ), + SettingsSearchableItem( + id: "data/auto-download/reset", + title: strings.ChatSettings_AutoDownloadReset, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_AutoDownloadReset), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentDataSettings(context, present, .automaticDownloadReset) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/chats", + title: strings.Notifications_PrivateChats, + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .privateChats, nil) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/chats/max-video-size", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .privateChats, .maxVideoSize) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/chats/add-exception", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .privateChats, .addException) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/chats/delete-all", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_PrivateChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .privateChats, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/groups", + title: strings.Notifications_GroupChats, + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .groups, nil) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/groups/max-video-size", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .groups, .maxVideoSize) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/groups/add-exception", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .groups, .addException) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/groups/delete-all", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_GroupChats], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .groups, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/channels", + title: strings.Notifications_Channels, + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .channels, nil) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/channels/max-video-size", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .channels, .maxVideoSize) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/channels/add-exception", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .channels, .addException) + } + ), + SettingsSearchableItem( + id: "data/save-to-photos/channels/delete-all", + icon: icon, + breadcrumbs: [strings.Settings_SaveToCameraRollSection, strings.Notifications_Channels], + isVisible: false, + present: { context, _, present in + presentSaveIncomingMediaSettings(context, present, .channels, .deleteExceptions) + } + ), + SettingsSearchableItem( + id: "data/use-less-data", + title: strings.CallSettings_UseLessData, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_CallsUseLessData), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.Settings_CallSettings], + present: { context, _, present in + presentDataSettings(context, present, .useLessVoiceData) + } + ), + SettingsSearchableItem( + id: "data/save-edited-photos", + title: strings.Settings_SaveEditedPhotos, + alternate: synonyms(strings.SettingsSearch_Synonyms_Data_SaveEditedPhotos), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentDataSettings(context, present, .saveEditedPhotos) + } + ), + SettingsSearchableItem( + id: "data/pause-music", + title: strings.Settings_PauseMusicOnRecording, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentDataSettings(context, present, .pauseMusicOnRecording) + } + ), + SettingsSearchableItem( + id: "data/raise-to-listen", + title: strings.Settings_RaiseToListen, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + presentDataSettings(context, present, .raiseToListen) + } + ), + SettingsSearchableItem( + id: "data/show-18-content", + title: strings.Settings_SensitiveContent, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + isVisible: false, + present: { context, _, present in + presentDataSettings(context, present, .sensitiveContent) + } + ), + SettingsSearchableItem( + id: "data/open-links", + title: strings.ChatSettings_OpenLinksIn, + alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_OpenLinksIn), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + present(.push, webBrowserSettingsController(context: context)) + } + ), + SettingsSearchableItem( + id: "data/share-sheet", + title: strings.ChatSettings_IntentsSettings, + alternate: synonyms(strings.SettingsSearch_Synonyms_ChatSettings_IntentsSettings), + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings], + present: { context, _, present in + present(.push, intentsSettingsController(context: context)) + } + ), + SettingsSearchableItem( + id: "data/share-sheet/suggested-chats", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_IntentsSettings], + isVisible: false, + present: { context, _, present in + present(.push, intentsSettingsController(context: context, focusOnItemTag: .suggested)) + } + ), + SettingsSearchableItem( + id: "data/share-sheet/suggest-by", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_IntentsSettings], + isVisible: false, + present: { context, _, present in + present(.push, intentsSettingsController(context: context, focusOnItemTag: .suggestBy)) + } + ), + SettingsSearchableItem( + id: "data/share-sheet/reset", + icon: icon, + breadcrumbs: [strings.Settings_ChatSettings, strings.ChatSettings_IntentsSettings], + isVisible: false, + present: { context, _, present in + present(.push, intentsSettingsController(context: context, focusOnItemTag: .reset)) + } + ) ] } @@ -794,18 +3724,66 @@ private func proxySearchableItems(context: AccountContext, servers: [ProxyServer let icon: SettingsSearchableItemIcon = .proxy let strings = context.sharedContext.currentPresentationData.with { $0 }.strings - let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in - present(.push, proxySettingsController(context: context)) + let presentProxySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, ProxySettingsEntryTag?) -> Void = { context, present, itemTag in + present(.push, proxySettingsController(context: context, focusOnItemTag: itemTag)) } var items: [SettingsSearchableItem] = [] - items.append(SettingsSearchableItem(id: .proxy(0), title: strings.Settings_Proxy, alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentProxySettings(context, present) - })) - items.append(SettingsSearchableItem(id: .proxy(1), title: strings.SocksProxySetup_AddProxy, alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_AddProxy), icon: icon, breadcrumbs: [strings.Settings_Proxy], present: { context, _, present in - present(.modal, proxyServerSettingsController(context: context)) - })) - + items.append( + SettingsSearchableItem( + id: "data/proxy", + title: strings.Settings_Proxy, + alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentProxySettings(context, present, nil) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "data/proxy/edit", + icon: icon, + isVisible: false, + present: { context, _, present in + presentProxySettings(context, present, .edit) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "data/use-proxy", + icon: icon, + isVisible: false, + present: { context, _, present in + presentProxySettings(context, present, .useProxy) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "data/proxy/add-proxy", + title: strings.SocksProxySetup_AddProxy, + alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_AddProxy), + icon: icon, + breadcrumbs: [strings.Settings_Proxy], + present: { context, _, present in + present(.modal, proxyServerSettingsController(context: context)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "data/proxy/share-list", + icon: icon, + isVisible: false, + present: { context, _, present in + presentProxySettings(context, present, .shareList) + } + ) + ) + var hasSocksServers = false for server in servers { if case .socks5 = server.connection { @@ -814,13 +3792,101 @@ private func proxySearchableItems(context: AccountContext, servers: [ProxyServer } } if hasSocksServers { - items.append(SettingsSearchableItem(id: .proxy(2), title: strings.SocksProxySetup_UseForCalls, alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_UseForCalls), icon: icon, breadcrumbs: [strings.Settings_Proxy], present: { context, _, present in - presentProxySettings(context, present) - })) + items.append( + SettingsSearchableItem( + id: "data/proxy/use-for-calls", + title: strings.SocksProxySetup_UseForCalls, + alternate: synonyms(strings.SettingsSearch_Synonyms_Proxy_UseForCalls), + icon: icon, + breadcrumbs: [strings.Settings_Proxy], + present: { context, _, present in + presentProxySettings(context, present, .useForCalls) + } + ) + ) } return items } +private func energySavingSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .powerSaving + let strings = context.sharedContext.currentPresentationData.with { $0 }.strings + + let presentEnergySaving: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, EnergySavingItemType?) -> Void = { context, present, item in + let controller = energySavingSettingsScreen(context: context, focusOnItemTag: item.flatMap { .item($0) }) + present(.push, controller) + } + + return [ + SettingsSearchableItem( + id: "power-saving", + title: strings.Settings_PowerSaving, + alternate: [], + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentEnergySaving(context, present, nil) + } + ), + SettingsSearchableItem( + id: "power-saving/videos", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .autoplayVideo) + } + ), + SettingsSearchableItem( + id: "power-saving/gifs", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .autoplayGif) + } + ), + SettingsSearchableItem( + id: "power-saving/stickers", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .loopStickers) + } + ), + SettingsSearchableItem( + id: "power-saving/emoji", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .loopEmoji) + } + ), + SettingsSearchableItem( + id: "power-saving/effects", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .fullTranslucency) + } + ), + SettingsSearchableItem( + id: "power-saving/preload", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .autodownloadInBackground) + } + ), + SettingsSearchableItem( + id: "power-saving/background", + icon: icon, + isVisible: false, + present: { context, _, present in + presentEnergySaving(context, present, .extendBackgroundWork) + } + ), + ] +} + private func appearanceSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { let icon: SettingsSearchableItemIcon = .appearance let strings = context.sharedContext.currentPresentationData.with { $0 }.strings @@ -830,31 +3896,211 @@ private func appearanceSearchableItems(context: AccountContext) -> [SettingsSear } return [ - SettingsSearchableItem(id: .appearance(0), title: strings.Settings_Appearance, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_Title), icon: icon, breadcrumbs: [], present: { context, _, present in - presentAppearanceSettings(context, present, nil) - }), - SettingsSearchableItem(id: .appearance(1), title: strings.Appearance_TextSizeSetting, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_TextSize), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in - presentAppearanceSettings(context, present, .fontSize) - }), - SettingsSearchableItem(id: .appearance(2), title: strings.Settings_ChatBackground, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in - present(.push, ThemeGridController(context: context)) - }), - SettingsSearchableItem(id: .appearance(3), title: strings.Wallpaper_SetColor, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor), icon: icon, breadcrumbs: [strings.Settings_Appearance, strings.Settings_ChatBackground], present: { context, _, present in - present(.push, ThemeColorsGridController(context: context)) - }), - SettingsSearchableItem(id: .appearance(4), title: strings.Wallpaper_SetCustomBackground, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground_Custom), icon: icon, breadcrumbs: [strings.Settings_Appearance, strings.Settings_ChatBackground], present: { context, _, present in - presentCustomWallpaperPicker(context: context, present: { controller in - present(.immediate, controller) - }, push: { controller in + SettingsSearchableItem( + id: "appearance", + title: strings.Settings_Appearance, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_Title), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + presentAppearanceSettings(context, present, nil) + } + ), + SettingsSearchableItem( + id: "appearance/wallpapers", + title: strings.Settings_ChatBackground, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground), + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + present(.push, ThemeGridController(context: context)) + } + ), + SettingsSearchableItem( + id: "appearance/wallpapers/edit", + icon: icon, + breadcrumbs: [strings.Settings_Appearance, strings.Settings_ChatBackground], + isVisible: false, + present: { context, _, present in + present(.push, ThemeGridController(context: context, forceEdit: true)) + } + ), + SettingsSearchableItem( + id: "appearance/wallpapers/set", + title: strings.Wallpaper_SetColor, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground_SetColor), + icon: icon, + breadcrumbs: [strings.Settings_Appearance, strings.Settings_ChatBackground], + present: { context, _, present in + + present(.push, ThemeColorsGridController(context: context)) + } + ), + SettingsSearchableItem( + id: "appearance/wallpapers/choose-photo", + title: strings.Wallpaper_SetCustomBackground, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ChatBackground_Custom), + icon: icon, + breadcrumbs: [strings.Settings_Appearance, strings.Settings_ChatBackground], + present: { context, _, present in + presentCustomWallpaperPicker(context: context, present: { controller in + present(.immediate, controller) + }, push: { controller in + present(.push, controller) + }) + } + ), + SettingsSearchableItem( + id: "appearance/night-mode", + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + isVisible: false, + present: { context, _, present in + presentAppearanceSettings(context, present, .nightMode) + } + ), + SettingsSearchableItem( + id: "appearance/auto-night-mode", + title: strings.Appearance_AutoNightTheme, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_AutoNightTheme), + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + present(.push, themeAutoNightSettingsController(context: context)) + } + ), + SettingsSearchableItem( + id: "appearance/themes", + title: strings.Themes_Title, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ColorTheme), + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + let controller = themePickerController(context: context) present(.push, controller) - }) - }), - SettingsSearchableItem(id: .appearance(5), title: strings.Appearance_AutoNightTheme, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_AutoNightTheme), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in - present(.push, themeAutoNightSettingsController(context: context)) - }), - SettingsSearchableItem(id: .appearance(6), title: strings.Appearance_ColorTheme, alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_ColorTheme), icon: icon, breadcrumbs: [strings.Settings_Appearance], present: { context, _, present in - presentAppearanceSettings(context, present, .accentColor) - }) + } + ), + SettingsSearchableItem( + id: "appearance/themes/edit", + title: strings.Themes_EditCurrentTheme, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_Appearance, strings.Themes_Title], + present: { context, _, present in + let controller = themePickerController(context: context) + present(.push, controller) + } + ), + SettingsSearchableItem( + id: "appearance/themes/create", + title: strings.Themes_CreateNewTheme, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_Appearance, strings.Themes_Title], + present: { context, navigationController, present in + let _ = (context.sharedContext.accountManager.transaction { transaction -> PresentationThemeReference in + let settings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings)?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + + let themeReference: PresentationThemeReference + let autoNightModeTriggered = context.sharedContext.currentPresentationData.with { $0 }.autoNightModeTriggered + if autoNightModeTriggered { + themeReference = settings.automaticThemeSwitchSetting.theme + } else { + themeReference = settings.theme + } + + return themeReference + } + |> deliverOnMainQueue).start(next: { [weak navigationController] themeReference in + let controller = editThemeController(context: context, mode: .create(nil, nil), navigateToChat: { [weak navigationController] peerId in + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + guard let peer else { + return + } + if let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + }) + }) + present(.push, controller) + }) + } + ), + SettingsSearchableItem( + id: "appearance/text-size", + title: strings.Appearance_TextSizeSetting, + alternate: synonyms(strings.SettingsSearch_Synonyms_Appearance_TextSize), + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + let _ = (context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationThemeSettings])) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let settings = view.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + present(.push, TextSizeSelectionController(context: context, presentationThemeSettings: settings)) + }) + } + ), + SettingsSearchableItem( + id: "appearance/text-size/use-system", + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + isVisible: false, + present: { context, _, present in + let _ = (context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationThemeSettings])) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let settings = view.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + present(.push, TextSizeSelectionController(context: context, presentationThemeSettings: settings, focusOnItemTag: .useSystem)) + }) + } + ), + SettingsSearchableItem( + id: "appearance/message-corners", + title: strings.Appearance_BubbleCornersSetting, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + let _ = (context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationThemeSettings])) + |> take(1) + |> deliverOnMainQueue).start(next: { view in + let settings = view.entries[ApplicationSpecificSharedDataKeys.presentationThemeSettings]?.get(PresentationThemeSettings.self) ?? PresentationThemeSettings.defaultSettings + present(.push, BubbleSettingsController(context: context, presentationThemeSettings: settings)) + }) + } + ), + SettingsSearchableItem( + id: "appearance/app-icon", + title: strings.Appearance_AppIcon.capitalized, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + presentAppearanceSettings(context, present, .icon) + } + ), + SettingsSearchableItem( + id: "appearance/animations", + title: strings.Appearance_Animations.capitalized, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + present: { context, _, present in + let controller = energySavingSettingsScreen(context: context) + present(.push, controller) + } + ), + SettingsSearchableItem( + id: "appearance/tap-for-next-media", + icon: icon, + breadcrumbs: [strings.Settings_Appearance], + isVisible: false, + present: { context, _, present in + presentAppearanceSettings(context, present, .tapForNextMedia) + } + ), ] } @@ -875,28 +4121,178 @@ private func languageSearchableItems(context: AccountContext, localizations: [Lo } var items: [SettingsSearchableItem] = [] - items.append(SettingsSearchableItem(id: .language(0), title: strings.Settings_AppLanguage, alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), icon: icon, breadcrumbs: [], present: { context, _, present in - present(.push, LocalizationListController(context: context)) - })) + items.append( + SettingsSearchableItem( + id: "language", + title: strings.Settings_AppLanguage, + alternate: synonyms(strings.SettingsSearch_Synonyms_AppLanguage), + icon: icon, + breadcrumbs: [], + present: { context, _, present in + present(.push, LocalizationListController(context: context)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "language/show-button", + title: strings.Localization_ShowTranslate, + alternate: synonyms(strings.SettingsSearch_Synonyms_Language_ShowTranslateButton), + icon: icon, + breadcrumbs: [strings.Settings_AppLanguage], + present: { context, _, present in + present(.push, LocalizationListController(context: context, focusOnItemTag: .showButton)) + } + ) + ) + items.append( + SettingsSearchableItem( + id: "language/translate-chats", + title: strings.Localization_TranslateEntireChat, + alternate: [], + icon: icon, + breadcrumbs: [strings.Settings_AppLanguage], + present: { context, _, present in + present(.push, LocalizationListController(context: context, focusOnItemTag: .translateChats)) + }) + ) + items.append( + SettingsSearchableItem( + id: "language/do-not-translate", + title: strings.Localization_DoNotTranslate, + alternate: synonyms(strings.SettingsSearch_Synonyms_Language_DoNotTranslate), + icon: icon, + breadcrumbs: [strings.Settings_AppLanguage], + present: { context, _, present in + present(.push, translationSettingsController(context: context)) + }) + ) + var index: Int32 = 1 for localization in localizations { - items.append(SettingsSearchableItem(id: .language(index), title: localization.localizedTitle, alternate: [localization.title], icon: icon, breadcrumbs: [strings.Settings_AppLanguage], present: { context, _, present in - applyLocalization(context, present, localization.languageCode) - })) + items.append( + SettingsSearchableItem( + id: "language/set/\(localization.languageCode)", + title: localization.localizedTitle, + alternate: [localization.title], + icon: icon, + breadcrumbs: [strings.Settings_AppLanguage], + present: { context, _, present in + applyLocalization(context, present, localization.languageCode) + } + ) + ) index += 1 } - items.append(SettingsSearchableItem(id: .language(1000), title: strings.Localization_ShowTranslate, alternate: synonyms(strings.SettingsSearch_Synonyms_Language_ShowTranslateButton), icon: icon, breadcrumbs: [strings.Settings_AppLanguage], present: { context, _, present in - present(.push, LocalizationListController(context: context)) - })) - items.append(SettingsSearchableItem(id: .language(1001), title: strings.Localization_DoNotTranslate, alternate: synonyms(strings.SettingsSearch_Synonyms_Language_DoNotTranslate), icon: icon, breadcrumbs: [strings.Settings_AppLanguage], present: { context, _, present in - present(.push, LocalizationListController(context: context)) - })) - return items } -func settingsSearchableItems(context: AccountContext, notificationExceptionsList: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal, hasTwoStepAuth: Signal, twoStepAuthData: Signal, activeSessionsContext: Signal, webSessionsContext: Signal) -> Signal<[SettingsSearchableItem], NoError> { +private func helpSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + var items: [SettingsSearchableItem] = [] + + items.append( + SettingsSearchableItem( + id: "ask-question", + title: strings.Settings_Support, + alternate: synonyms(strings.SettingsSearch_Synonyms_Support), + icon: .support, + breadcrumbs: [], + present: { context, _, present in + let _ = (context.engine.peers.supportPeerId() + |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) + } + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "faq", + title: strings.Settings_FAQ, + alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), + icon: .faq, + breadcrumbs: [], + present: { context, navigationController, present in + let _ = (cachedFaqInstantPage(context: context) + |> take(1) + |> deliverOnMainQueue).start(next: { resolvedUrl in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in + present(.push, controller) + }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "features", + title: strings.Settings_Tips, + alternate: [], + icon: .tips, + breadcrumbs: [], + present: { context, navigationController, present in + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) + present(.immediate, controller) + + let _ = (context.engine.peers.resolvePeerByName(name: strings.Settings_TipsUsername, referrer: nil) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).startStandalone(next: { [weak controller] peer in + controller?.dismiss() + if let peer, let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer))) + } + }) + } + ) + ) + + items.append( + SettingsSearchableItem( + id: "privacy-policy", + title: strings.Permissions_PrivacyPolicy, + alternate: [], + icon: .tips, + breadcrumbs: [], + present: { context, navigationController, present in + let _ = (cachedPrivacyPage(context: context) + |> take(1) + |> deliverOnMainQueue).start(next: { resolvedUrl in + context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in + }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { c, arguments in + present(.push, c) + }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) + }) + } + ) + ) + + return items +} + +func settingsSearchableItems( + context: AccountContext, + notificationExceptionsList: Signal = .single(nil), + archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError> = .single(nil), + privacySettings: Signal = .single(nil), + hasTwoStepAuth: Signal = .single(nil), + twoStepAuthData: Signal = .single(nil), + activeSessionsContext: Signal = .single(nil), + webSessionsContext: Signal = .single(nil) +) -> Signal<[SettingsSearchableItem], NoError> { let canAddAccount = activeAccountsAndPeers(context: context) |> take(1) |> map { accountsAndPeers -> Bool in @@ -990,8 +4386,31 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList } } - return combineLatest(canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasTwoStepAuth, twoStepAuthData, activeSessionsContext, activeWebSessionsContext) - |> map { canAddAccount, localizations, notificationSettings, notificationExceptionsList, archivedStickerPacks, proxyServers, privacySettings, hasTwoStepAuth, twoStepAuthData, activeSessionsContext, activeWebSessionsContext in + return combineLatest( + canAddAccount, + localizations, + notificationSettings, + notificationExceptionsList, + archivedStickerPacks, + proxyServers, + privacySettings, + hasTwoStepAuth, + twoStepAuthData, + activeSessionsContext, + activeWebSessionsContext + ) + |> map { + canAddAccount, + localizations, + notificationSettings, + notificationExceptionsList, + archivedStickerPacks, + proxyServers, + privacySettings, + hasTwoStepAuth, + twoStepAuthData, + activeSessionsContext, + activeWebSessionsContext in let strings = context.sharedContext.currentPresentationData.with { $0 }.strings var allItems: [SettingsSearchableItem] = [] @@ -999,9 +4418,16 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let profileItems = profileSearchableItems(context: context, canAddAccount: canAddAccount) allItems.append(contentsOf: profileItems) - let savedMessages = SettingsSearchableItem(id: .savedMessages(0), title: strings.Settings_SavedMessages, alternate: synonyms(strings.SettingsSearch_Synonyms_SavedMessages), icon: .savedMessages, breadcrumbs: [], present: { context, _, present in - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) - }) + let savedMessages = SettingsSearchableItem( + id: "saved-messages", + title: strings.Settings_SavedMessages, + alternate: synonyms(strings.SettingsSearch_Synonyms_SavedMessages), + icon: .savedMessages, + breadcrumbs: [], + present: { context, _, present in + present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: context.account.peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) + } + ) allItems.append(savedMessages) let devicesItems = devicesSearchableItems(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: activeWebSessionsContext) @@ -1010,14 +4436,12 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let callItems = callSearchableItems(context: context) allItems.append(contentsOf: callItems) - let chatFolders = SettingsSearchableItem(id: .chatFolders(0), title: strings.Settings_ChatFolders, alternate: synonyms(strings.SettingsSearch_Synonyms_ChatFolders), icon: .chatFolders, breadcrumbs: [], present: { context, _, present in - present(.push, chatListFilterPresetListController(context: context, mode: .default)) - }) - allItems.append(chatFolders) + let chatFolders = chatFoldersSearchableItems(context: context) + allItems.append(contentsOf: chatFolders) let stickerItems = stickerSearchableItems(context: context, archivedStickerPacks: archivedStickerPacks) allItems.append(contentsOf: stickerItems) - + let notificationItems = notificationSearchableItems(context: context, settings: notificationSettings, exceptionsList: notificationExceptionsList) allItems.append(contentsOf: notificationItems) @@ -1033,49 +4457,50 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList let appearanceItems = appearanceSearchableItems(context: context) allItems.append(contentsOf: appearanceItems) + let powerSavingItems = energySavingSearchableItems(context: context) + allItems.append(contentsOf: powerSavingItems) + let languageItems = languageSearchableItems(context: context, localizations: localizations) allItems.append(contentsOf: languageItems) let premiumItems = premiumSearchableItems(context: context) allItems.append(contentsOf: premiumItems) - - let storiesItems = storiesSearchableItems(context: context) + + let storiesItems = myProfileSearchableItems(context: context) allItems.append(contentsOf: storiesItems) - if let hasTwoStepAuth = hasTwoStepAuth, hasTwoStepAuth { - let passport = SettingsSearchableItem(id: .passport(0), title: strings.Settings_Passport, alternate: synonyms(strings.SettingsSearch_Synonyms_Passport), icon: .passport, breadcrumbs: [], present: { context, _, present in - present(.modal, SecureIdAuthController(context: context, mode: .list)) - }) + if let hasTwoStepAuth = hasTwoStepAuth, + hasTwoStepAuth { + let passport = SettingsSearchableItem( + id: "passport", + title: strings.Settings_Passport, + alternate: synonyms(strings.SettingsSearch_Synonyms_Passport), + icon: .passport, + breadcrumbs: [], + present: { context, _, present in + present(.modal, SecureIdAuthController(context: context, mode: .list)) + } + ) allItems.append(passport) } + + let helpItems = helpSearchableItems(context: context) + allItems.append(contentsOf: helpItems) - let support = SettingsSearchableItem(id: .support(0), title: strings.Settings_Support, alternate: synonyms(strings.SettingsSearch_Synonyms_Support), icon: .support, breadcrumbs: [], present: { context, _, present in - let _ = (context.engine.peers.supportPeerId() - |> deliverOnMainQueue).start(next: { peerId in - if let peerId = peerId { - present(.push, context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)) - } - }) - }) - allItems.append(support) - - let faq = SettingsSearchableItem(id: .faq(0), title: strings.Settings_FAQ, alternate: synonyms(strings.SettingsSearch_Synonyms_FAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in - let _ = (cachedFaqInstantPage(context: context) - |> deliverOnMainQueue).start(next: { resolvedUrl in - context.sharedContext.openResolvedUrl(resolvedUrl, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in - }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { controller, arguments in + let deleteAccount = SettingsSearchableItem( + id: "delete-account", + title: strings.DeleteAccount_DeleteMyAccount, + alternate: synonyms(strings.SettingsSearch_DeleteAccount_DeleteMyAccount), + icon: .deleteAccount, + breadcrumbs: [], + present: { context, navigationController, present in + if let navigationController = navigationController { + let controller = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) present(.push, controller) - }, dismissInput: {}, contentContext: nil, progress: nil, completion: nil) - }) - }) - allItems.append(faq) - - allItems.append(SettingsSearchableItem(id: .deleteAccount(0), title: strings.DeleteAccount_DeleteMyAccount, alternate: synonyms(strings.SettingsSearch_DeleteAccount_DeleteMyAccount), icon: .deleteAccount, breadcrumbs: [], present: { context, navigationController, present in - if let navigationController = navigationController { - let controller = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData) - present(.push, controller) + } } - })) + ) + allItems.append(deleteAccount) return allItems } @@ -1136,10 +4561,15 @@ private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey } func searchSettingsItems(items: [SettingsSearchableItem], query: String) -> [SettingsSearchableItem] { + let showAll = query == "#all" + let queryTokens = stringTokens(query.lowercased()) var result: [SettingsSearchableItem] = [] for item in items { + guard item.isVisible || showAll else { + continue + } var string = item.title if !item.alternate.isEmpty { for alternate in item.alternate { @@ -1154,10 +4584,68 @@ func searchSettingsItems(items: [SettingsSearchableItem], query: String) -> [Set } let tokens = stringTokens(string) - if matchStringTokens(tokens, with: queryTokens) { + if showAll || matchStringTokens(tokens, with: queryTokens) { + var item = item + if item.title.isEmpty && !item.isVisible, let id = item.id.base as? String { + item = item.withUpdatedTitle(id) + } result.append(item) } } return result } + +public func handleSettingsPathUrl(context: AccountContext, path: String, navigationController: NavigationController) { + let _ = (settingsSearchableItems(context: context) + |> take(1) + |> deliverOnMainQueue).start(next: { items in + guard let item = items.first(where: { $0.id == AnyHashable(path) }) else { + return + } + item.present(context, navigationController, { mode, controller in + guard let controller, let topController = navigationController.topViewController as? ViewController else { + return + } + switch mode { + case .push: + navigationController.pushViewController(controller) + case .modal: + topController.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: {}), blockInteraction: false, completion: {}) + case .immediate: + topController.present(controller, in: .window(.root)) + default: + break + } + }) + }) +} + +private func presentSetupBirthday(context: AccountContext, present: @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) { + let settingsPromise: Promise + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface, let current = rootController.getPrivacySettings() { + settingsPromise = current + } else { + settingsPromise = Promise() + settingsPromise.set(.single(nil) |> then(context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init))) + } + + let controller = context.sharedContext.makeBirthdayPickerScreen( + context: context, + settings: settingsPromise, + openSettings: { + context.sharedContext.makeBirthdayPrivacyController(context: context, settings: settingsPromise, openedFromBirthdayScreen: true, present: { c in + present(.push, c) + }) + }, + completion: { value in + let _ = context.engine.accountData.updateBirthday(birthday: value).startStandalone() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + present(.immediate, UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: nil, text: presentationData.strings.Birthday_Added, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + })) + } + ) + present(.push, controller) +} diff --git a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift index be30b24f6b..c028dbaacf 100644 --- a/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/ArchivedStickerPacksController.swift @@ -249,13 +249,19 @@ private func archivedStickerPacksControllerEntries(context: AccountContext, mode return entries } -public func archivedStickerPacksController(context: AccountContext, mode: ArchivedStickerPacksControllerMode, archived: [ArchivedStickerPackItem]?, forceTheme: PresentationTheme? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void) -> ViewController { +public func archivedStickerPacksController(context: AccountContext, mode: ArchivedStickerPacksControllerMode, archived: [ArchivedStickerPackItem]?, forceTheme: PresentationTheme? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void, forceEdit: Bool = false) -> ViewController { let statePromise = ValuePromise(ArchivedStickerPacksControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ArchivedStickerPacksControllerState()) let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + if forceEdit { + updateState { + $0.withUpdatedEditing(true) + } + } + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var navigationControllerImpl: (() -> NavigationController?)? diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index c03f10d74a..00dbd99fc1 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -67,7 +67,10 @@ private enum InstalledStickerPacksSection: Int32 { } public enum InstalledStickerPacksEntryTag: ItemListItemTag { + case edit case suggestOptions + case largeEmoji + case dynamicOrder public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? InstalledStickerPacksEntryTag, self == other { @@ -397,7 +400,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { case let .largeEmoji(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleLargeEmoji(value) - }) + }, tag: InstalledStickerPacksEntryTag.largeEmoji) case let .trending(theme, text, count): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Trending")?.precomposed(), title: text, label: count == 0 ? "" : "\(count)", labelStyle: .badge(theme.list.itemAccentColor), sectionId: self.section, style: .blocks, action: { arguments.openFeatured() @@ -421,7 +424,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { case let .packOrder(_, text, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleDynamicPackOrder(value) - }) + }, tag: InstalledStickerPacksEntryTag.dynamicOrder) case let .packOrderInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) case let .suggestAnimatedEmoji(text, value): @@ -651,6 +654,12 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta statePromise.set(stateValue.modify { f($0) }) } + if focusOnItemTag == InstalledStickerPacksEntryTag.edit { + updateState { + $0.withUpdatedEditing(true) + } + } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } if let forceTheme { presentationData = presentationData.withUpdated(theme: forceTheme) @@ -1299,6 +1308,20 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta controller?.dismiss() } + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift index d026f7a4c8..4fbdde912a 100644 --- a/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift +++ b/submodules/SettingsUI/Sources/Text Size/TextSizeSelectionController.swift @@ -16,6 +16,18 @@ import WallpaperBackgroundNode import AnimationCache import MultiAnimationRenderer +public enum TextSizeSelectionEntryTag: ItemListItemTag, Equatable { + case useSystem + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? TextSizeSelectionEntryTag, self == other { + return true + } else { + return false + } + } +} + private func generateMaskImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 1.0, height: 80.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) @@ -33,6 +45,8 @@ private func generateMaskImage(color: UIColor) -> UIImage? { private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollViewDelegate { private let context: AccountContext + private let focusOnItemTag: TextSizeSelectionEntryTag? + private var presentationThemeSettings: PresentationThemeSettings private var presentationData: PresentationData @@ -57,8 +71,9 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView private var validLayout: (ContainerViewLayout, CGFloat)? - init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) { + init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, focusOnItemTag: TextSizeSelectionEntryTag?, dismiss: @escaping () -> Void, apply: @escaping (Bool, PresentationFontSize, PresentationFontSize) -> Void) { self.context = context + self.focusOnItemTag = focusOnItemTag self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationThemeSettings = presentationThemeSettings @@ -92,7 +107,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView self.chatBackgroundNode.update(wallpaper: self.presentationData.chatWallpaper, animated: false) self.chatBackgroundNode.updateBubbleTheme(bubbleTheme: self.presentationData.theme, bubbleCorners: self.presentationData.chatBubbleCorners) - self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData) + self.toolbarNode = TextSelectionToolbarNode(presentationThemeSettings: self.presentationThemeSettings, presentationData: self.presentationData, focusOnItemTag: focusOnItemTag) self.maskNode = ASImageNode() self.maskNode.displaysAsynchronously = false @@ -602,6 +617,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView final class TextSizeSelectionController: ViewController { private let context: AccountContext + private let focusOnItemTag: TextSizeSelectionEntryTag? private var controllerNode: TextSizeSelectionControllerNode { return self.displayNode as! TextSizeSelectionControllerNode @@ -618,8 +634,9 @@ final class TextSizeSelectionController: ViewController { private var disposable: Disposable? private var applyDisposable = MetaDisposable() - public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings) { + public init(context: AccountContext, presentationThemeSettings: PresentationThemeSettings, focusOnItemTag: TextSizeSelectionEntryTag? = nil) { self.context = context + self.focusOnItemTag = focusOnItemTag self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationThemeSettings = presentationThemeSettings @@ -670,7 +687,7 @@ final class TextSizeSelectionController: ViewController { override public func loadDisplayNode() { super.loadDisplayNode() - self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, dismiss: { [weak self] in + self.displayNode = TextSizeSelectionControllerNode(context: self.context, presentationThemeSettings: self.presentationThemeSettings, focusOnItemTag: self.focusOnItemTag, dismiss: { [weak self] in if let strongSelf = self { strongSelf.dismiss() } @@ -727,7 +744,7 @@ private final class TextSelectionToolbarNode: ASDisplayNode { var updateUseSystemFont: ((Bool) -> Void)? var updateCustomFontSize: ((PresentationFontSize) -> Void)? - init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData) { + init(presentationThemeSettings: PresentationThemeSettings, presentationData: PresentationData, focusOnItemTag: TextSizeSelectionEntryTag?) { self.presentationThemeSettings = presentationThemeSettings self.presentationData = presentationData @@ -774,6 +791,10 @@ private final class TextSelectionToolbarNode: ASDisplayNode { self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) self.doneButton.addTarget(self, action: #selector(self.donePressed), forControlEvents: .touchUpInside) + + if focusOnItemTag == .useSystem { + self.switchItemNode.displayHighlight() + } } func setDoneEnabled(_ enabled: Bool) { diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index 8a3574f3b0..37d174b993 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -84,6 +84,8 @@ public enum ThemeSettingsEntryTag: ItemListItemTag { case powerSaving case stickersAndEmoji case animations + case tapForNextMedia + case nightMode public func isEqual(to other: ItemListItemTag) -> Bool { if let other = other as? ThemeSettingsEntryTag, self == other { @@ -318,7 +320,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case let .autoNight(_, title, value, enabled): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, enabled: enabled, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleNightTheme(value) - }, tag: nil) + }, tag: ThemeSettingsEntryTag.nightMode) case let .autoNightTheme(_, text, value): return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: nil, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openAutoNightTheme() @@ -352,7 +354,7 @@ private enum ThemeSettingsControllerEntry: ItemListNodeEntry { case let .showNextMediaOnTap(_, title, value): return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.toggleShowNextMediaOnTap(value) - }, tag: ThemeSettingsEntryTag.animations) + }, tag: ThemeSettingsEntryTag.tapForNextMedia) case let .showNextMediaOnTapInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) } @@ -1309,6 +1311,21 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The presentCrossfadeControllerImpl?(true) }) } + + if let focusOnItemTag { + var didFocusOnItem = false + controller.afterTransactionCompleted = { [weak controller] in + if !didFocusOnItem, let controller { + controller.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, tag.isEqual(to: focusOnItemTag) { + didFocusOnItem = true + itemNode.displayHighlight() + } + } + } + } + } + return controller } diff --git a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift index c652c059e8..34f5a466ce 100644 --- a/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift +++ b/submodules/StatisticsUI/Sources/TransactionInfoScreen.swift @@ -91,10 +91,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -105,7 +105,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/StickerPackPreviewUI/BUILD b/submodules/StickerPackPreviewUI/BUILD index 73f3f1db32..67a5eab359 100644 --- a/submodules/StickerPackPreviewUI/BUILD +++ b/submodules/StickerPackPreviewUI/BUILD @@ -41,6 +41,9 @@ swift_library( "//submodules/Pasteboard:Pasteboard", "//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index c6f960fb33..1e45d99807 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -23,9 +23,13 @@ import MultiAnimationRenderer import Pasteboard import StickerPackEditTitleController import EntityKeyboard -//import CameraScreen import ComponentFlow import EmojiStatusComponent +import EdgeEffect +import GlassBarButtonComponent +import BundleIconComponent +import LottieComponent +import ButtonComponent private let maxStickersCount = 120 @@ -143,18 +147,20 @@ private final class StickerPackContainer: ASDisplayNode { private let previewIconFile: TelegramMediaFile? private var mainPreviewIcon: ComponentView? private let gridNode: GridNode - private let actionAreaBackgroundNode: NavigationBackgroundNode - private let actionAreaSeparatorNode: ASDisplayNode private let buttonNode: HighlightableButtonNode - private let titleBackgroundnode: NavigationBackgroundNode private let titleNode: ImmediateTextNode private var titlePlaceholderNode: ShimmerEffectNode? private let titleContainer: ASDisplayNode - private let titleSeparatorNode: ASDisplayNode private let topContainerNode: ASDisplayNode - private let cancelButtonNode: HighlightableButtonNode - private let moreButtonNode: MoreButtonNode + private let bottomContainerNode: ASDisplayNode + + private let topEdgeEffectView = EdgeEffectView() + private let bottomEdgeEffectView = EdgeEffectView() + private let button = ComponentView() + private let cancelButton = ComponentView() + private let moreButton = ComponentView() + private let moreButtonPlayOnce = ActionSlot() private(set) var validLayout: (ContainerViewLayout, CGRect, CGFloat, UIEdgeInsets)? @@ -176,6 +182,8 @@ private final class StickerPackContainer: ASDisplayNode { return self.isReadyValue.get() } + private var itemCount: Int32 = 0 + var expandProgress: CGFloat = 0.0 var expandScrollProgress: CGFloat = 0.0 var modalProgress: CGFloat = 0.0 @@ -225,7 +233,7 @@ private final class StickerPackContainer: ASDisplayNode { self.backgroundNode = ASImageNode() self.backgroundNode.displaysAsynchronously = true self.backgroundNode.displayWithoutProcessing = true - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 76.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.previewIconFile = previewIconFile if self.previewIconFile != nil { @@ -235,14 +243,9 @@ private final class StickerPackContainer: ASDisplayNode { self.gridNode = GridNode() self.gridNode.scrollView.alwaysBounceVertical = true self.gridNode.scrollView.showsVerticalScrollIndicator = false - - self.titleBackgroundnode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor) - - self.actionAreaBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor) - - self.actionAreaSeparatorNode = ASDisplayNode() - self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor - + + self.bottomContainerNode = ASDisplayNode() + self.buttonNode = HighlightableButtonNode() self.titleNode = ImmediateTextNode() self.titleNode.textAlignment = .center @@ -261,13 +264,7 @@ private final class StickerPackContainer: ASDisplayNode { } self.titleContainer = ASDisplayNode() - self.titleSeparatorNode = ASDisplayNode() - self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - self.topContainerNode = ASDisplayNode() - self.cancelButtonNode = HighlightableButtonNode() - self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme) - self.moreButtonNode.iconNode.enqueueState(.more, animated: false) var addStickerPackImpl: ((StickerPackCollectionInfo, [StickerPackItem]) -> Void)? var removeStickerPackImpl: ((StickerPackCollectionInfo) -> Void)? @@ -290,18 +287,13 @@ private final class StickerPackContainer: ASDisplayNode { self.addSubnode(self.backgroundNode) self.addSubnode(self.gridNode) - self.addSubnode(self.actionAreaBackgroundNode) - self.addSubnode(self.actionAreaSeparatorNode) - self.addSubnode(self.buttonNode) self.titleContainer.addSubnode(self.titleNode) self.addSubnode(self.titleContainer) - self.addSubnode(self.titleSeparatorNode) self.addSubnode(self.topContainerNode) - self.topContainerNode.addSubnode(self.cancelButtonNode) - self.topContainerNode.addSubnode(self.moreButtonNode) - + self.addSubnode(self.bottomContainerNode) + self.gridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) } @@ -329,14 +321,7 @@ private final class StickerPackContainer: ASDisplayNode { } } } - - self.gridNode.visibleContentOffsetChanged = { [weak self] _ in - guard let strongSelf = self else { - return - } - strongSelf.updateButtonBackgroundAlpha() - } - + self.gridNode.interactiveScrollingWillBeEnded = { [weak self] contentOffset, velocity, targetOffset -> CGPoint in guard let strongSelf = self, !strongSelf.isDismissed else { return targetOffset @@ -431,27 +416,27 @@ private final class StickerPackContainer: ASDisplayNode { strongSelf.updateStickerPackContents(contents, hasPremium: hasPremium) }) - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - self.buttonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") - strongSelf.buttonNode.alpha = 0.8 - } else { - strongSelf.buttonNode.alpha = 1.0 - strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3) - } - } - } +// self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) +// self.buttonNode.highligthedChanged = { [weak self] highlighted in +// if let strongSelf = self { +// if highlighted { +// strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity") +// strongSelf.buttonNode.alpha = 0.8 +// } else { +// strongSelf.buttonNode.alpha = 1.0 +// strongSelf.buttonNode.layer.animateAlpha(from: 0.8, to: 1.0, duration: 0.3) +// } +// } +// } - self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) + //self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) + //self.cancelButtonNode.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside) - self.moreButtonNode.action = { [weak self] _, gesture in - if let strongSelf = self { - strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) - } - } +// self.moreButtonNode.action = { [weak self] _, gesture in +// if let strongSelf = self { +// strongSelf.morePressed(node: strongSelf.moreButtonNode.contextSourceNode, gesture: gesture) +// } +// } self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.2) @@ -510,7 +495,7 @@ private final class StickerPackContainer: ASDisplayNode { private var reorderingGestureRecognizer: ReorderingGestureRecognizer? override func didLoad() { super.didLoad() - + let peekGestureRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>? in if let strongSelf = self { if let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? StickerPackPreviewGridItemNode, let item = itemNode.stickerPackItem { @@ -690,6 +675,9 @@ private final class StickerPackContainer: ASDisplayNode { reorderingGestureRecognizer.isEnabled = self.isEditing self.reorderingGestureRecognizer = reorderingGestureRecognizer self.gridNode.view.addGestureRecognizer(reorderingGestureRecognizer) + + self.topContainerNode.view.addSubview(self.topEdgeEffectView) + self.bottomContainerNode.view.addSubview(self.bottomEdgeEffectView) } private func hideMainPreviewIcon() { @@ -1005,15 +993,7 @@ private final class StickerPackContainer: ASDisplayNode { func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData - self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) - - self.titleBackgroundnode.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.actionAreaBackgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) - self.actionAreaSeparatorNode.backgroundColor = self.presentationData.theme.rootController.tabBar.separatorColor - self.titleSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - - self.cancelButtonNode.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) - self.moreButtonNode.theme = self.presentationData.theme + self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 76.0, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor) self.titleNode.linkHighlightColor = self.presentationData.theme.actionSheet.controlAccentColor.withAlphaComponent(0.5) @@ -1056,7 +1036,7 @@ private final class StickerPackContainer: ASDisplayNode { self.titleNode.attributedText = stringWithAppliedEntities(title, entities: entities, baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) if let (layout, _, _, _) = self.validLayout { - let _ = self.titleNode.updateLayout(CGSize(width: layout.size.width - max(12.0, self.cancelButtonNode.frame.width) * 2.0 - 40.0, height: .greatestFiniteMagnitude)) + let _ = self.titleNode.updateLayout(CGSize(width: layout.size.width - 44.0 * 2.0 - 40.0, height: .greatestFiniteMagnitude)) self.updateLayout(layout: layout, transition: .immediate) } } @@ -1084,7 +1064,6 @@ private final class StickerPackContainer: ASDisplayNode { private func updateIsEditing(_ isEditing: Bool) { self.isEditing = isEditing self.updateEntries(reload: true) - self.updateButton() self.peekGestureRecognizer?.longPressEnabled = !isEditing self.reorderingGestureRecognizer?.isEnabled = isEditing if let (layout, _, _, _) = self.validLayout { @@ -1096,7 +1075,7 @@ private final class StickerPackContainer: ASDisplayNode { } } - @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { + @objc private func morePressed(view: UIView, gesture: ContextGesture?) { guard let controller = self.controller else { return } @@ -1228,7 +1207,13 @@ private final class StickerPackContainer: ASDisplayNode { }))) } - let contextController = makeContextController(presentationData: self.presentationData, source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(StickerPackContextReferenceContentSource(controller: controller, sourceView: view)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture + ) self.presentInGlobalOverlay(contextController, nil) } @@ -1562,104 +1547,6 @@ private final class StickerPackContainer: ASDisplayNode { } } - private func updateButtonBackgroundAlpha() { - let offset = self.gridNode.visibleContentOffset() - - let backgroundAlpha: CGFloat - switch offset { - case .known: - let topPosition = self.view.convert(self.topContainerNode.frame, to: self.view).minY - let bottomPosition = self.actionAreaBackgroundNode.view.convert(self.actionAreaBackgroundNode.bounds, to: self.view).minY - let bottomEdgePosition = topPosition + self.topContainerNode.frame.height + self.gridNode.scrollView.contentSize.height - let bottomOffset = bottomPosition - bottomEdgePosition - - backgroundAlpha = min(10.0, max(0.0, -1.0 * bottomOffset)) / 10.0 - case .unknown, .none: - backgroundAlpha = 1.0 - } - - let transition: ContainedViewLayoutTransition - var delay: Double = 0.0 - if backgroundAlpha >= self.actionAreaBackgroundNode.alpha || abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) < 0.01 { - transition = .immediate - } else { - transition = .animated(duration: 0.2, curve: .linear) - if abs(backgroundAlpha - self.actionAreaBackgroundNode.alpha) > 0.9 { - delay = 0.2 - } - } - transition.updateAlpha(node: self.actionAreaBackgroundNode, alpha: backgroundAlpha, delay: delay) - transition.updateAlpha(node: self.actionAreaSeparatorNode, alpha: backgroundAlpha, delay: delay) - } - - private func updateButton(count: Int32 = 0) { - if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content { - if installed { - let text: String - if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) { - if self.isEditing { - var updated = false - if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done { - updated = true - } - - if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.buttonNode.view.frame - self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - self.buttonNode.setTitle(self.presentationData.strings.Common_Done, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) - self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) - } else { - let buttonTitle = self.presentationData.strings.StickerPack_EditStickers - var updated = false - if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != buttonTitle { - updated = true - } - - if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.buttonNode.view.frame - self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - } - - text = buttonTitle - self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemAccentColor, for: .normal) - self.buttonNode.setBackgroundImage(nil, for: []) - } - } else { - if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = self.presentationData.strings.StickerPack_RemoveStickerCount(count) - } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { - text = self.presentationData.strings.StickerPack_RemoveEmojiCount(count) - } else { - text = self.presentationData.strings.StickerPack_RemoveMaskCount(count) - } - self.buttonNode.setTitle(text, with: Font.regular(17.0), with: self.presentationData.theme.list.itemDestructiveColor, for: .normal) - self.buttonNode.setBackgroundImage(nil, for: []) - } - } else { - let text: String - if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { - text = self.presentationData.strings.StickerPack_AddStickerCount(count) - } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { - text = self.presentationData.strings.StickerPack_AddEmojiCount(count) - } else { - text = self.presentationData.strings.StickerPack_AddMaskCount(count) - } - self.buttonNode.setTitle(text, with: Font.semibold(17.0), with: self.presentationData.theme.list.itemCheckColors.foregroundColor, for: .normal) - self.buttonNode.setBackgroundImage(generateStretchableFilledCircleImage(radius: 11, color: self.presentationData.theme.list.itemCheckColors.fillColor), for: []) - } - } - } - private func updateStickerPackContents(_ contents: [LoadedStickerPack], hasPremium: Bool) { self.currentContents = contents self.didReceiveStickerPackResult = true @@ -1864,7 +1751,8 @@ private final class StickerPackContainer: ASDisplayNode { } else { count = Int32(entries.count) } - self.updateButton(count: count) + self.itemCount = count + updateLayout = true } if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) && entries.count < maxStickersCount { @@ -2032,43 +1920,15 @@ private final class StickerPackContainer: ASDisplayNode { insets.top += 10.0 } - var buttonHeight: CGFloat = 50.0 - var actionAreaTopInset: CGFloat = 8.0 - var actionAreaBottomInset: CGFloat = 16.0 - if let _ = self.controller?.mainActionTitle { - - } else { - if !self.currentStickerPacks.isEmpty { - var installedCount = 0 - for (_, _, isInstalled) in self.currentStickerPacks { - if isInstalled { - installedCount += 1 - } - } - if installedCount == self.currentStickerPacks.count { - buttonHeight = 42.0 - actionAreaTopInset = 1.0 - actionAreaBottomInset = 2.0 - } - } - if let (info, _, isInstalled) = self.currentStickerPack, isInstalled, (!info.flags.contains(.isCreator) || info.flags.contains(.isEmoji)) { - buttonHeight = 42.0 - actionAreaTopInset = 1.0 - actionAreaBottomInset = 2.0 - } - } + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: layout.intrinsicInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let titleAreaInset: CGFloat = 76.0 + let buttonHeight: CGFloat = 52.0 + let buttonSideInset: CGFloat = 30.0 + let actionAreaHeight: CGFloat = buttonHeight + buttonInsets.bottom - let buttonSideInset: CGFloat = 16.0 - let titleAreaInset: CGFloat = 56.0 - - var actionAreaHeight: CGFloat = buttonHeight - actionAreaHeight += insets.bottom + actionAreaBottomInset - - transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonSideInset, y: layout.size.height - actionAreaHeight + actionAreaTopInset), size: CGSize(width: layout.size.width - buttonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: buttonHeight))) + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + buttonSideInset, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width - buttonSideInset * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: buttonHeight))) - transition.updateFrame(node: self.actionAreaBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: actionAreaHeight))) - self.actionAreaBackgroundNode.update(size: CGSize(width: layout.size.width, height: actionAreaHeight), transition: .immediate) - transition.updateFrame(node: self.actionAreaSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + transition.updateFrame(node: self.bottomContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - actionAreaHeight), size: CGSize(width: layout.size.width, height: 90.0))) let gridFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top + titleAreaInset), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - titleAreaInset)) @@ -2131,17 +1991,171 @@ private final class StickerPackContainer: ASDisplayNode { titlePlaceholderNode.updateAbsoluteRect(titlePlaceholderNode.frame.offsetBy(dx: self.titleContainer.frame.minX, dy: self.titleContainer.frame.minY - gridInsets.top - gridFrame.minY), within: gridFrame.size) } - let cancelSize = self.cancelButtonNode.measure(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude)) - self.cancelButtonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: 18.0), size: cancelSize) + //let cancelSize = self.cancelButtonNode.measure(CGSize(width: layout.size.width, height: .greatestFiniteMagnitude)) + //self.cancelButtonNode.frame = CGRect(origin: CGPoint(x: layout.safeInsets.left + 16.0, y: 18.0), size: cancelSize) - let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - cancelSize.width * 2.0 - 40.0, height: .greatestFiniteMagnitude)) + let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - 44.0 * 2.0 - 40.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: floor((-titleSize.width) / 2.0), y: floor((-titleSize.height) / 2.0)), size: titleSize) - self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0)) + //self.moreButtonNode.frame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - 46.0, y: 5.0), size: CGSize(width: 44.0, height: 44.0)) - transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0) - transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0) + //transition.updateAlpha(node: self.cancelButtonNode, alpha: self.isEditing ? 0.0 : 1.0) + //transition.updateAlpha(node: self.moreButtonNode, alpha: self.isEditing ? 0.0 : 1.0) + + + let cancelButtonSize = self.cancelButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: false, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.cancelPressed() + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0), + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize) + if let cancelButtonView = self.cancelButton.view { + if cancelButtonView.superview == nil { + self.topContainerNode.view.addSubview(cancelButtonView) + } + transition.updateFrame(view: cancelButtonView, frame: cancelButtonFrame) + transition.updateAlpha(layer: cancelButtonView.layer, alpha: self.isEditing ? 0.0 : 1.0) + } + + let moreButtonSize = self.moreButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: false, + state: .glass, + component: AnyComponentWithIdentity(id: "more", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent( + name: "anim_morewide" + ), + color: self.presentationData.theme.chat.inputPanel.panelControlColor, + size: CGSize(width: 34.0, height: 34.0), + playOnce: self.moreButtonPlayOnce + ) + )), + action: { [weak self] view in + guard let self else { + return + } + self.morePressed(view: view, gesture: nil) + self.moreButtonPlayOnce.invoke(Void()) + } + )), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0), + ) + let moreButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - 16.0 - moreButtonSize.width, y: 16.0), size: moreButtonSize) + if let moreButtonView = self.moreButton.view { + if moreButtonView.superview == nil { + self.topContainerNode.view.addSubview(moreButtonView) + } + transition.updateFrame(view: moreButtonView, frame: moreButtonFrame) + transition.updateAlpha(layer: moreButtonView.layer, alpha: self.isEditing ? 0.0 : 1.0) + } + + var buttonTitle: String = "" + var buttonForegroundColor = self.presentationData.theme.list.itemCheckColors.foregroundColor + var buttonBackgroundColor = self.presentationData.theme.list.itemCheckColors.fillColor + + if let currentContents = self.currentContents, currentContents.count == 1, let content = currentContents.first, case let .result(info, _, installed) = content { + if installed { + if info.flags.contains(.isCreator) && !info.flags.contains(.isEmoji) { + if self.isEditing { + var updated = false + if let current = self.buttonNode.attributedTitle(for: .normal)?.string, !current.isEmpty && current != self.presentationData.strings.Common_Done { + updated = true + } + + if updated, let snapshotView = self.buttonNode.view.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = self.buttonNode.view.frame + self.buttonNode.view.superview?.insertSubview(snapshotView, belowSubview: self.buttonNode.view) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + self.buttonNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + + buttonTitle = self.presentationData.strings.Common_Done + } else { + buttonTitle = self.presentationData.strings.StickerPack_EditStickers + buttonBackgroundColor = self.presentationData.theme.list.plainBackgroundColor + buttonBackgroundColor = self.presentationData.theme.list.itemAccentColor + } + } else { + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + buttonTitle = self.presentationData.strings.StickerPack_RemoveStickerCount(self.itemCount) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + buttonTitle = self.presentationData.strings.StickerPack_RemoveEmojiCount(self.itemCount) + } else { + buttonTitle = self.presentationData.strings.StickerPack_RemoveMaskCount(self.itemCount) + } + buttonBackgroundColor = self.presentationData.theme.list.plainBackgroundColor + buttonForegroundColor = self.presentationData.theme.list.itemDestructiveColor + } + } else { + if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { + buttonTitle = self.presentationData.strings.StickerPack_AddStickerCount(self.itemCount) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + buttonTitle = self.presentationData.strings.StickerPack_AddEmojiCount(self.itemCount) + } else { + buttonTitle = self.presentationData.strings.StickerPack_AddMaskCount(self.itemCount) + } + } + } + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: buttonBackgroundColor, + foreground: buttonForegroundColor, + pressedColor: buttonBackgroundColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(Text(text: buttonTitle, font: Font.semibold(17.0), color: buttonForegroundColor)) + ), + action: { [weak self] in + self?.buttonPressed() + } + ) + ), + environment: {}, + containerSize: CGSize(width: layout.size.width - buttonInsets.left - buttonInsets.right, height: 52.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: 0.0), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.bottomContainerNode.view.addSubview(buttonView) + } + buttonView.frame = buttonFrame + } + + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: actionAreaHeight - 90.0), size: CGSize(width: layout.size.width, height: 90.0)) + transition.updateFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + self.bottomEdgeEffectView.update(content: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 0.65, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) if firstTime { while !self.enqueuedTransactions.isEmpty { @@ -2194,7 +2208,6 @@ private final class StickerPackContainer: ASDisplayNode { if !transition.isAnimated { self.backgroundNode.layer.removeAllAnimations() self.titleContainer.layer.removeAllAnimations() - self.titleSeparatorNode.layer.removeAllAnimations() } var backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: max(minBackgroundY, unclippedBackgroundY)), size: CGSize(width: layout.size.width, height: layout.size.height)) @@ -2203,7 +2216,7 @@ private final class StickerPackContainer: ASDisplayNode { backgroundFrame.origin.y = min(0.0, backgroundFrame.origin.y) titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: floor((56.0) / 2.0)), size: CGSize()) } else { - titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0)), size: CGSize()) + titleContainerFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width) / 2.0), y: backgroundFrame.minY + floor((56.0) / 2.0) + 10.0), size: CGSize()) } transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) @@ -2239,14 +2252,8 @@ private final class StickerPackContainer: ASDisplayNode { } transition.updateFrame(node: self.titleContainer, frame: titleContainerFrame) - transition.updateFrame(node: self.titleSeparatorNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY + 56.0 - UIScreenPixel), size: CGSize(width: backgroundFrame.width, height: UIScreenPixel))) - transition.updateFrame(node: self.titleBackgroundnode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) - self.titleBackgroundnode.update(size: CGSize(width: layout.size.width, height: 56.0), transition: .immediate) - transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 56.0))) - - let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) - transition.updateAlpha(node: self.titleSeparatorNode, alpha: unclippedBackgroundY < minBackgroundY ? 1.0 : 0.0) + transition.updateFrame(node: self.topContainerNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width, height: 76.0))) } private func enqueueTransaction(_ transaction: StickerPackPreviewGridTransaction) { @@ -2423,7 +2430,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let containerInsets: UIEdgeInsets if case .regular = layout.metrics.widthClass { self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.01) - self.containerContainingNode.cornerRadius = 10.0 + self.containerContainingNode.cornerRadius = 38.0 let size = CGSize(width: 390.0, height: min(560.0, layout.size.height - 60.0)) var contentRect: CGRect @@ -2476,8 +2483,15 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { let shadowFrame = containerContainingFrame.insetBy(dx: -60.0, dy: -60.0) transition.updateFrame(node: self.shadowNode, frame: shadowFrame) - let expandProgress: CGFloat = 1.0 - let scaledInset: CGFloat = 12.0 + let expandProgress: CGFloat + if case .regular = layout.metrics.widthClass { + expandProgress = 1.0 + } else if let selectedContainer = self.containers[self.selectedStickerPackIndex] { + expandProgress = selectedContainer.expandProgress + } else { + expandProgress = 0.0 + } + let scaledInset: CGFloat = 6.0 let scaledDistance: CGFloat = 4.0 let minScale = (layout.size.width - scaledInset * 2.0) / layout.size.width let containerScale = expandProgress * 1.0 + (1.0 - expandProgress) * minScale @@ -3101,15 +3115,15 @@ public func StickerPackScreen( private final class StickerPackContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController - private let sourceNode: ContextReferenceContentNode + private let sourceView: UIView - init(controller: ViewController, sourceNode: ContextReferenceContentNode) { + init(controller: ViewController, sourceView: UIView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index a07520bb90..15e4a1f70b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -185,6 +185,13 @@ public enum MessageActionUrlAuthResult { public let platform: String public let ip: String public let region: String + + public init(browser: String, platform: String, ip: String, region: String) { + self.browser = browser + self.platform = platform + self.ip = ip + self.region = region + } } case `default` diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 92dd57af88..c7b736ceae 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -96,7 +96,7 @@ public extension TelegramEngine { } public func requestMessageActionUrlAuth(subject: MessageActionUrlSubject) -> Signal { - _internal_requestMessageActionUrlAuth(account: self.account, subject: subject) + return _internal_requestMessageActionUrlAuth(account: self.account, subject: subject) } public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 8fc803a7db..2d67d2f562 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -3186,6 +3186,12 @@ private final class CraftGiftsContextImpl { return _internal_craftStarGift(account: self.account, references: references) } + func addGift(gift: ProfileGiftsContext.State.StarGift) { + self.gifts.insert(gift, at: 0) + self.count = self.count + 1 + self.pushState() + } + func removeGifts(references: [StarGiftReference]) { let referencesSet = Set(references) self.gifts.removeAll { gift in @@ -3318,6 +3324,12 @@ public final class CraftGiftsContext { impl.reload() } } + + public func addGift(gift: ProfileGiftsContext.State.StarGift) { + self.impl.with { impl in + impl.addGift(gift: gift) + } + } public func removeGifts(references: [StarGiftReference]) { self.impl.with { impl in @@ -3838,6 +3850,7 @@ private final class ResaleGiftsContextImpl { private let queue: Queue private let account: Account private let giftId: Int64 + private let forCrafting: Bool private let disposable = MetaDisposable() @@ -3861,11 +3874,13 @@ private final class ResaleGiftsContextImpl { init( queue: Queue, account: Account, - giftId: Int64 + giftId: Int64, + forCrafting: Bool ) { self.queue = queue self.account = account self.giftId = giftId + self.forCrafting = forCrafting self.loadMore() } @@ -3898,6 +3913,10 @@ private final class ResaleGiftsContextImpl { } var flags: Int32 = 0 + if self.forCrafting { + flags |= (1 << 4) + } + switch sorting { case .date: break @@ -4169,11 +4188,12 @@ public final class ResaleGiftsContext { public init( account: Account, - giftId: Int64 + giftId: Int64, + forCrafting: Bool ) { let queue = self.queue self.impl = QueueLocalObject(queue: queue, generate: { - return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId) + return ResaleGiftsContextImpl(queue: queue, account: account, giftId: giftId, forCrafting: forCrafting) }) } diff --git a/submodules/TelegramNotices/Sources/Notices.swift b/submodules/TelegramNotices/Sources/Notices.swift index 0734033617..1fd68beefb 100644 --- a/submodules/TelegramNotices/Sources/Notices.swift +++ b/submodules/TelegramNotices/Sources/Notices.swift @@ -205,6 +205,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 { case voiceMessagesResumeTrimWarning = 82 case globalPostsSearch = 83 case giftAuctionTips = 84 + case giftCraftingTips = 85 var key: ValueBoxKey { let v = ValueBoxKey(length: 4) @@ -574,6 +575,10 @@ private struct ApplicationSpecificNoticeKeys { static func giftAuctionTips() -> NoticeEntryKey { return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.giftAuctionTips.key) } + + static func giftCraftingTips() -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.giftCraftingTips.key) + } } public struct ApplicationSpecificNotice { @@ -2514,4 +2519,31 @@ public struct ApplicationSpecificNotice { return Int(previousValue) } } + + public static func getGiftCraftingTips(accountManager: AccountManager) -> Signal { + return accountManager.transaction { transaction -> Int32 in + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.giftCraftingTips())?.get(ApplicationSpecificCounterNotice.self) { + return value.value + } else { + return 0 + } + } + } + + public static func incrementGiftCraftingTips(accountManager: AccountManager, count: Int = 1) -> Signal { + return accountManager.transaction { transaction -> Int in + var currentValue: Int32 = 0 + if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.giftCraftingTips())?.get(ApplicationSpecificCounterNotice.self) { + currentValue = value.value + } + let previousValue = currentValue + currentValue += Int32(count) + + if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { + transaction.setNotice(ApplicationSpecificNoticeKeys.giftCraftingTips(), entry) + } + + return Int(previousValue) + } + } } diff --git a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift index 56384410f5..545608d6f2 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationTheme.swift @@ -555,6 +555,10 @@ public final class PresentationThemeList { public func withUpdated(blocksBackgroundColor: UIColor? = nil, modalBlocksBackgroundColor: UIColor? = nil, plainBackgroundColor: UIColor? = nil, modalPlainBackgroundColor: UIColor? = nil, itemPrimaryTextColor: UIColor? = nil, itemSecondaryTextColor: UIColor? = nil, itemDisabledTextColor: UIColor? = nil, itemAccentColor: UIColor? = nil, itemHighlightedColor: UIColor? = nil, itemDestructiveColor: UIColor? = nil, itemPlaceholderTextColor: UIColor? = nil, itemBlocksBackgroundColor: UIColor? = nil, itemModalBlocksBackgroundColor: UIColor? = nil, itemHighlightedBackgroundColor: UIColor? = nil, itemBlocksSeparatorColor: UIColor? = nil, itemPlainSeparatorColor: UIColor? = nil, disclosureArrowColor: UIColor? = nil, sectionHeaderTextColor: UIColor? = nil, freeTextColor: UIColor? = nil, freeTextErrorColor: UIColor? = nil, freeTextSuccessColor: UIColor? = nil, freeMonoIconColor: UIColor? = nil, itemSwitchColors: PresentationThemeSwitch? = nil, itemDisclosureActions: PresentationThemeItemDisclosureActions? = nil, itemCheckColors: PresentationThemeFillStrokeForeground? = nil, controlSecondaryColor: UIColor? = nil, freeInputField: PresentationInputFieldTheme? = nil, freePlainInputField: PresentationInputFieldTheme? = nil, mediaPlaceholderColor: UIColor? = nil, scrollIndicatorColor: UIColor? = nil, pageIndicatorInactiveColor: UIColor? = nil, inputClearButtonColor: UIColor? = nil, itemBarChart: PresentationThemeItemBarChart? = nil, itemInputField: PresentationInputFieldTheme? = nil, paymentOption: PaymentOption? = nil) -> PresentationThemeList { return PresentationThemeList(blocksBackgroundColor: blocksBackgroundColor ?? self.blocksBackgroundColor, modalBlocksBackgroundColor: modalBlocksBackgroundColor ?? self.modalBlocksBackgroundColor, plainBackgroundColor: plainBackgroundColor ?? self.plainBackgroundColor, modalPlainBackgroundColor: modalPlainBackgroundColor ?? self.modalPlainBackgroundColor, itemPrimaryTextColor: itemPrimaryTextColor ?? self.itemPrimaryTextColor, itemSecondaryTextColor: itemSecondaryTextColor ?? self.itemSecondaryTextColor, itemDisabledTextColor: itemDisabledTextColor ?? self.itemDisabledTextColor, itemAccentColor: itemAccentColor ?? self.itemAccentColor, itemHighlightedColor: itemHighlightedColor ?? self.itemHighlightedColor, itemDestructiveColor: itemDestructiveColor ?? self.itemDestructiveColor, itemPlaceholderTextColor: itemPlaceholderTextColor ?? self.itemPlaceholderTextColor, itemBlocksBackgroundColor: itemBlocksBackgroundColor ?? self.itemBlocksBackgroundColor, itemModalBlocksBackgroundColor: itemModalBlocksBackgroundColor ?? self.itemModalBlocksBackgroundColor, itemHighlightedBackgroundColor: itemHighlightedBackgroundColor ?? self.itemHighlightedBackgroundColor, itemBlocksSeparatorColor: itemBlocksSeparatorColor ?? self.itemBlocksSeparatorColor, itemPlainSeparatorColor: itemPlainSeparatorColor ?? self.itemPlainSeparatorColor, disclosureArrowColor: disclosureArrowColor ?? self.disclosureArrowColor, sectionHeaderTextColor: sectionHeaderTextColor ?? self.sectionHeaderTextColor, freeTextColor: freeTextColor ?? self.freeTextColor, freeTextErrorColor: freeTextErrorColor ?? self.freeTextErrorColor, freeTextSuccessColor: freeTextSuccessColor ?? self.freeTextSuccessColor, freeMonoIconColor: freeMonoIconColor ?? self.freeMonoIconColor, itemSwitchColors: itemSwitchColors ?? self.itemSwitchColors, itemDisclosureActions: itemDisclosureActions ?? self.itemDisclosureActions, itemCheckColors: itemCheckColors ?? self.itemCheckColors, controlSecondaryColor: controlSecondaryColor ?? self.controlSecondaryColor, freeInputField: freeInputField ?? self.freeInputField, freePlainInputField: freePlainInputField ?? self.freePlainInputField, mediaPlaceholderColor: mediaPlaceholderColor ?? self.mediaPlaceholderColor, scrollIndicatorColor: scrollIndicatorColor ?? self.scrollIndicatorColor, pageIndicatorInactiveColor: pageIndicatorInactiveColor ?? self.pageIndicatorInactiveColor, inputClearButtonColor: inputClearButtonColor ?? self.inputClearButtonColor, itemBarChart: itemBarChart ?? self.itemBarChart, itemInputField: itemInputField ?? self.itemInputField, paymentOption: paymentOption ?? self.paymentOption) } + + public var itemSearchHighlightColor: UIColor { + return self.itemAccentColor.withMultipliedAlpha(0.2) + } } public final class PresentationThemeArchiveAvatarColors { diff --git a/submodules/TelegramStringFormatting/Sources/TonFormat.swift b/submodules/TelegramStringFormatting/Sources/TonFormat.swift index c0614200ec..fe252cc156 100644 --- a/submodules/TelegramStringFormatting/Sources/TonFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/TonFormat.swift @@ -96,7 +96,7 @@ public func formatTonAmountText(_ value: Int64, dateTimeFormat: PresentationDate } public func formatStarsAmountText(_ amount: StarsAmount, dateTimeFormat: PresentationDateTimeFormat, showPlus: Bool = false) -> String { - var balanceText = presentationStringsFormattedNumber(Int32(amount.value), dateTimeFormat.groupingSeparator) + var balanceText = presentationStringsFormattedNumber(Int32(clamping: amount.value), dateTimeFormat.groupingSeparator) let fraction = abs(Double(amount.nanos)) / 10e6 if fraction > 0.0 { balanceText.append(dateTimeFormat.decimalSeparator) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 66ca4ad438..7170d356e7 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -465,6 +465,7 @@ swift_library( "//submodules/TelegramUI/Components/Gifts/GiftOptionsScreen", "//submodules/TelegramUI/Components/Gifts/GiftStoreScreen", "//submodules/TelegramUI/Components/Gifts/GiftSetupScreen", + "//submodules/TelegramUI/Components/Gifts/GiftCraftScreen", "//submodules/TelegramUI/Components/ContentReportScreen", "//submodules/TelegramUI/Components/PeerInfo/AffiliateProgramSetupScreen", "//submodules/TelegramUI/Components/Stars/StarsBalanceOverlayComponent", @@ -513,7 +514,9 @@ swift_library( "//submodules/TelegramUI/Components/AlertComponent", "//submodules/TelegramUI/Components/Chat/ChatAgeRestrictionAlertController", "//submodules/TelegramUI/Components/CocoonInfoScreen", + "//submodules/TelegramUI/Components/ProxyServerPreviewScreen", "//submodules/TelegramUI/Components/ContextControllerImpl", + "//submodules/TelegramUI/Components/AuthConfirmationScreen", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift index 8d4ee3766b..5edc3135aa 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/RecentActionsSettingsSheet.swift @@ -489,10 +489,10 @@ private final class RecentActionsSettingsSheetComponent: Component { let leftButtonSize = self.leftButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -507,7 +507,7 @@ private final class RecentActionsSettingsSheetComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: leftButtonSize) if let leftButtonView = self.leftButton.view { diff --git a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift index 4bf2386847..9d334fbe0b 100644 --- a/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/Sources/AlertComponent.swift @@ -547,11 +547,12 @@ private final class AlertScreenComponent: Component { self.containerView.update(size: availableSize, isDark: environment.theme.overallDarkAppearance, transition: transition) var availableHeight = availableSize.height + availableHeight -= environment.statusBarHeight if component.configuration.allowInputInset, environment.inputHeight > 0.0 { availableHeight -= environment.inputHeight } - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize)) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - alertSize.width) / 2.0), y: environment.statusBarHeight + floorToScreenPixels((availableHeight - alertSize.height) / 2.0)), size: alertSize)) self.backgroundView.update(size: alertSize, cornerRadius: 35.0, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, transition: transition) return availableSize diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD new file mode 100644 index 0000000000..f5e828e899 --- /dev/null +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD @@ -0,0 +1,38 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AuthConfirmationScreen", + module_name = "AuthConfirmationScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/AvatarComponent", + "//submodules/Markdown", + "//submodules/PhoneNumberFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift new file mode 100644 index 0000000000..5cf4ff2540 --- /dev/null +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -0,0 +1,588 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import GlassBarButtonComponent +import ButtonComponent +import PresentationDataUtils +import BundleIconComponent +import ListSectionComponent +import ListActionItemComponent +import AvatarComponent +import Markdown +import PhoneNumberFormat + +private final class AuthConfirmationSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: MessageActionUrlAuthResult + let completion: (Bool, Bool) -> Void + let cancel: (Bool) -> Void + + init( + context: AccountContext, + subject: MessageActionUrlAuthResult, + completion: @escaping (Bool, Bool) -> Void, + cancel: @escaping (Bool) -> Void + ) { + self.context = context + self.subject = subject + self.completion = completion + self.cancel = cancel + } + + static func ==(lhs: AuthConfirmationSheetContent, rhs: AuthConfirmationSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let subject: MessageActionUrlAuthResult + + fileprivate var inProgress = false + var allowWrite = true + weak var controller: ViewController? + + init(context: AccountContext, subject: MessageActionUrlAuthResult) { + self.context = context + self.subject = subject + + super.init() + } + + func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) { + guard case let .request(domain, _, _, _) = self.subject else { + return + } + + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, case let .user(user) = peer, let phone = user.phone else { + return + } + let phoneNumber = formatPhoneNumber(context: self.context, number: phone) + + //TODO:localize + let alertController = textAlertController( + context: self.context, + title: "Phone Number", + text: "**\(domain)** wants to access your phone number **\(phoneNumber)**. Allow access?", + actions: [ + TextAlertAction(type: .genericAction, title: "Deny", action: { + commit(false) + }), + TextAlertAction(type: .defaultAction, title: "Allow", action: { + commit(true) + }) + ] + ) + self.controller?.present(alertController, in: .window(.root)) + }) + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let avatar = Child(AvatarComponent.self) + let title = Child(MultilineTextComponent.self) + let description = Child(MultilineTextComponent.self) + + let clientSection = Child(ListSectionComponent.self) + let optionsSection = Child(ListSectionComponent.self) + + let cancelButton = Child(ButtonComponent.self) + let doneButton = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let state = context.state + if state.controller == nil { + state.controller = environment.controller() + } + + let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } + let _ = strings + + guard case let .request(domain, bot, clientData, flags) = component.subject else { + fatalError() + } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + component.cancel(true) + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + + var contentHeight: CGFloat = 32.0 + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: environment.theme, + peer: EnginePeer(bot), + clipStyle: .roundedRect + ), + availableSize: CGSize(width: 92.0, height: 92.0), + transition: .immediate + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + avatar.size.height / 2.0)) + ) + contentHeight += avatar.size.height + contentHeight += 18.0 + + let titleFont = Font.bold(24.0) + let title = title.update( + component: MultilineTextComponent( + text: .markdown(text: "Log in to **\(domain)**", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), + horizontalAlignment: .center, + maximumNumberOfLines: 2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + title.size.height / 2.0)) + ) + contentHeight += title.size.height + contentHeight += 16.0 + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let description = description.update( + component: MultilineTextComponent( + text: .markdown(text: "This site will receive your **name**,\n**username** and **profile photo**.", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + description.size.height / 2.0)) + ) + contentHeight += description.size.height + contentHeight += 16.0 + + var clientSectionItems: [AnyComponentWithIdentity] = [] + clientSectionItems.append( + AnyComponentWithIdentity(id: "device", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Device", + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: clientData?.platform ?? "", + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: clientData?.browser ?? "", + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0), + textColor: theme.list.itemSecondaryTextColor + )), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .right, spacing: 3.0) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + + clientSectionItems.append( + AnyComponentWithIdentity(id: "region", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "IP Address", + font: Font.regular(17.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + )), + contentInsets: UIEdgeInsets(top: 19.0, left: 0.0, bottom: 19.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "info", + component: AnyComponent( + VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: clientData?.ip ?? "", + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: clientData?.region ?? "", + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 15.0), + textColor: theme.list.itemSecondaryTextColor + )), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .right, spacing: 3.0) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) + ) + + let clientSection = clientSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "This login attempt came from the device above.", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: clientSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(clientSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + clientSection.size.height / 2.0)) + ) + contentHeight += clientSection.size.height + + if flags.contains(.requestWriteAccess) { + contentHeight += 22.0 + + var optionsSectionItems: [AnyComponentWithIdentity] = [] + optionsSectionItems.append(AnyComponentWithIdentity(id: "allowWrite", component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Allow Messages", + font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: state.allowWrite, action: { [weak state] _ in + guard let state else { + return + } + state.allowWrite = !state.allowWrite + state.updated() + })), + action: nil + )))) + let optionsSection = optionsSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "This will allow \(EnginePeer(bot).compactDisplayTitle) to message you.", + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + items: optionsSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(optionsSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + optionsSection.size.height / 2.0)) + ) + contentHeight += optionsSection.size.height + } + contentHeight += 32.0 + + let buttonSpacing: CGFloat = 10.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonWidth = (context.availableSize.width - buttonInsets.left - buttonInsets.right - buttonSpacing) / 2.0 + + let cancelButton = cancelButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), + foreground: theme.list.itemPrimaryTextColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Cancel", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)))) + ), + action: { + component.cancel(true) + } + ), + availableSize: CGSize(width: buttonWidth, height: 52.0), + transition: .immediate + ) + context.add(cancelButton + .position(CGPoint(x: context.availableSize.width / 2.0 - buttonSpacing / 2.0 - cancelButton.size.width / 2.0, y: contentHeight + cancelButton.size.height / 2.0)) + ) + + let doneButton = doneButton.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0, + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Log In", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + displaysProgress: state.inProgress, + action: { [weak state] in + guard let state else { + return + } + var allowWrite = false + if flags.contains(.requestWriteAccess) && state.allowWrite { + allowWrite = true + } + if flags.contains(.requestPhoneNumber) { + state.displayPhoneNumberConfirmation(commit: { sharePhoneNumber in + component.completion(allowWrite, sharePhoneNumber) + state.inProgress = true + state.updated() + }) + } else { + component.completion(allowWrite, false) + state.inProgress = true + state.updated() + } + } + ), + availableSize: CGSize(width: buttonWidth, height: 52.0), + transition: .immediate + ) + context.add(doneButton + .position(CGPoint(x: context.availableSize.width / 2.0 + buttonSpacing / 2.0 + doneButton.size.width / 2.0, y: contentHeight + doneButton.size.height / 2.0)) + ) + contentHeight += doneButton.size.height + contentHeight += buttonInsets.bottom + + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + +private final class AuthConfirmationSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: MessageActionUrlAuthResult + let completion: (Bool, Bool) -> Void + + init( + context: AccountContext, + subject: MessageActionUrlAuthResult, + completion: @escaping (Bool, Bool) -> Void + ) { + self.context = context + self.subject = subject + self.completion = completion + } + + static func ==(lhs: AuthConfirmationSheetComponent, rhs: AuthConfirmationSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(AuthConfirmationSheetContent( + context: context.component.context, + subject: context.component.subject, + completion: context.component.completion, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + } + )), + style: .glass, + backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class AuthConfirmationScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let subject: MessageActionUrlAuthResult + fileprivate let completion: (Bool, Bool) -> Void + + public init( + context: AccountContext, + subject: MessageActionUrlAuthResult, + completion: @escaping (Bool, Bool) -> Void + ) { + self.context = context + self.subject = subject + self.completion = completion + + super.init( + context: context, + component: AuthConfirmationSheetComponent( + context: context, + subject: subject, + completion: completion + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift b/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift index a9d18e4e4a..18cce36a2f 100644 --- a/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift +++ b/submodules/TelegramUI/Components/AvatarComponent/Sources/AvatarComponent.swift @@ -8,20 +8,28 @@ import AvatarNode import AccountContext public final class AvatarComponent: Component { + public enum ClipStyle { + case round + case roundedRect + } + let context: AccountContext let theme: PresentationTheme let peer: EnginePeer + let clipStyle: ClipStyle let icon: AnyComponent? public init( context: AccountContext, theme: PresentationTheme, peer: EnginePeer, + clipStyle: ClipStyle = .round, icon: AnyComponent? = nil ) { self.context = context self.theme = theme self.peer = peer + self.clipStyle = clipStyle self.icon = icon } @@ -35,6 +43,9 @@ public final class AvatarComponent: Component { if lhs.peer != rhs.peer { return false } + if lhs.clipStyle != rhs.clipStyle { + return false + } if lhs.icon != rhs.icon { return false } @@ -81,7 +92,7 @@ public final class AvatarComponent: Component { transition: .immediate, component: icon, environment: {}, - containerSize: availableSize + containerSize: CGSize(width: 24.0, height: 24.0) ) let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - iconSize.width + 2.0, y: availableSize.height - iconSize.height + 2.0), size: iconSize) if let iconView = iconView.view { @@ -93,11 +104,17 @@ public final class AvatarComponent: Component { cutoutRect = CGRect(origin: CGPoint(x: iconFrame.minX, y: availableSize.height - iconFrame.maxY), size: iconFrame.size).insetBy(dx: -2.0 + UIScreenPixel, dy: -2.0 + UIScreenPixel) } + var clipStyle: AvatarNodeClipStyle = .round + if case .roundedRect = component.clipStyle { + clipStyle = .roundedRect + } + self.avatarNode.frame = CGRect(origin: .zero, size: availableSize) self.avatarNode.setPeer( context: component.context, theme: component.theme, peer: component.peer, + clipStyle: clipStyle, synchronousLoad: true, cutoutRect: cutoutRect ) diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD index c8f7959aa1..c8f00d99c9 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/BUILD @@ -42,6 +42,8 @@ swift_library( "//submodules/TelegramUI/Components/AvatarBackground", "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/PremiumAlertController", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/BundleIconComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift index a43f361591..45bee7e333 100644 --- a/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift +++ b/submodules/TelegramUI/Components/AvatarEditorScreen/Sources/AvatarEditorScreen.swift @@ -28,6 +28,8 @@ import AvatarBackground import LottieComponent import UndoUI import PremiumAlertController +import GlassBarButtonComponent +import BundleIconComponent public struct AvatarKeyboardInputData: Equatable { var emoji: EmojiPagerContentComponent @@ -837,50 +839,63 @@ final class AvatarEditorScreenComponent: Component { } } - let backgroundIsBright = UIColor(rgb: state.selectedBackground.colors.first ?? 0).lightness > 0.8 + //let backgroundIsBright = UIColor(rgb: state.selectedBackground.colors.first ?? 0).lightness > 0.8 + //state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor let navigationCancelButtonSize = self.navigationCancelButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor)), - action: { [weak self] in - guard let self else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: environment.theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: environment.theme.chat.inputPanel.panelControlColor))), + action: { [weak self] _ in + guard let self else { + return + } + self.controller?()?.dismiss() } - self.controller?()?.dismiss() - } - ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + ) + ), environment: {}, - containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + containerSize: CGSize(width: 44.0, height: 44.0) ) if let navigationCancelButtonView = self.navigationCancelButton.view { if navigationCancelButtonView.superview == nil { self.addSubview(navigationCancelButtonView) } - transition.setFrame(view: navigationCancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: environment.statusBarHeight), size: navigationCancelButtonSize)) + transition.setFrame(view: navigationCancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0 + environment.safeInsets.left, y: 16.0), size: navigationCancelButtonSize)) transition.setAlpha(view: navigationCancelButtonView, alpha: !state.editingColor ? 1.0 : 0.0) } let navigationDoneButtonSize = self.navigationDoneButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Text(text: component.peerType == .suggest ? strings.AvatarEditor_Suggest : strings.AvatarEditor_Set, font: Font.semibold(17.0), color: state.expanded && !backgroundIsBright ? .white : environment.theme.rootController.navigationBar.accentTextColor)), - action: { [weak self] in - guard let self else { - return + component: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: environment.theme.list.itemCheckColors.fillColor, + isDark: environment.theme.overallDarkAppearance, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "done", component: AnyComponent(BundleIconComponent(name: "Navigation/Done", tintColor: environment.theme.list.itemCheckColors.foregroundColor))), + action: { [weak self] _ in + guard let self else { + return + } + self.complete() } - self.complete() - } - ).minSize(CGSize(width: 16.0, height: environment.navigationHeight - environment.statusBarHeight))), + ) + ), environment: {}, - containerSize: CGSize(width: 150.0, height: environment.navigationHeight - environment.statusBarHeight) + containerSize: CGSize(width: 44.0, height: 44.0) ) if let navigationDoneButtonView = self.navigationDoneButton.view { if navigationDoneButtonView.superview == nil { self.addSubview(navigationDoneButtonView) } - transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: environment.statusBarHeight), size: navigationDoneButtonSize)) + transition.setFrame(view: navigationDoneButtonView, frame: CGRect(origin: CGPoint(x: availableSize.width - 16.0 - environment.safeInsets.right - navigationDoneButtonSize.width, y: 16.0), size: navigationDoneButtonSize)) transition.setAlpha(view: navigationDoneButtonView, alpha: (state.expanded || environment.inputHeight > 0.0) && !state.editingColor ? 1.0 : 0.0) } @@ -1190,9 +1205,10 @@ final class AvatarEditorScreenComponent: Component { contentHeight += keyboardTitleSize.height contentHeight += 8.0 - var bottomInset: CGFloat = environment.safeInsets.bottom > 0.0 ? environment.safeInsets.bottom : 16.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + var bottomInset: CGFloat = buttonInsets.bottom if !effectiveIsExpanded { - bottomInset += 50.0 + 16.0 + bottomInset += 52.0 + buttonInsets.bottom - 14.0 } let keyboardContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - contentHeight - bottomInset)) @@ -1318,7 +1334,6 @@ final class AvatarEditorScreenComponent: Component { )))) } - let bottomInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let buttonSize = self.buttonView.update( transition: transition, component: AnyComponent( @@ -1340,13 +1355,13 @@ final class AvatarEditorScreenComponent: Component { ) ), environment: {}, - containerSize: CGSize(width: availableSize.width - bottomInsets.left - bottomInsets.right, height: 52.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) if let buttonView = self.buttonView.view { if buttonView.superview == nil { self.addSubview(buttonView) } - transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: bottomInsets.left, y: contentHeight), size: buttonSize)) + transition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: buttonSize)) } let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight - 4.0), size: CGSize(width: availableSize.width, height: availableSize.height - contentHeight + 4.0)) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 8f05aa0097..6840cfbd57 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -46,13 +46,13 @@ let collageGrids: [Camera.CollageGrid] = [ Camera.CollageGrid(rows: [Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2), Camera.CollageGrid.Row(columns: 2)]) ] -enum CameraMode: Int32, Equatable { - case photo - case video - case live -} - struct CameraState: Equatable { + enum CameraMode: Int32, Equatable { + case photo + case video + case live + } + enum Recording: Equatable { case none case holding @@ -548,7 +548,7 @@ private final class CameraScreenComponent: CombinedComponent { self.buttonPressTimestamp = nil } - func updateCameraMode(_ mode: CameraMode) { + func updateCameraMode(_ mode: CameraState.CameraMode) { guard let controller = self.getController(), let camera = controller.camera else { return } @@ -2102,7 +2102,7 @@ private final class CameraScreenComponent: CombinedComponent { availableModeControlSize = availableSize } - var availableModes: [CameraMode] = [.photo, .video] + var availableModes: [CameraState.CameraMode] = [.photo, .video] if !isTablet && state.canLivestream { availableModes.append(.live) } @@ -2218,6 +2218,12 @@ public class CameraScreenImpl: ViewController, CameraScreen { case avatar } + public enum CameraMode { + case photo + case video + case live + } + public enum PIPPosition: Int32 { case topLeft case topRight @@ -2528,8 +2534,18 @@ public class CameraScreenImpl: ViewController, CameraScreen { self.mainPreviewView.resetPlaceholder(front: cameraFrontPosition) } + let cameraMode: CameraState.CameraMode + switch controller.cameraMode { + case .photo: + cameraMode = .photo + case .video: + cameraMode = .video + case .live: + cameraMode = .live + } + self.cameraState = CameraState( - mode: .photo, + mode: cameraMode, position: cameraFrontPosition ? .front : .back, flashMode: .off, flashModeDidChange: false, @@ -4002,6 +4018,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { private let context: AccountContext fileprivate let mode: Mode + fileprivate let cameraMode: CameraMode fileprivate let customTarget: EnginePeer.Id? fileprivate let resumeLiveStream: Bool fileprivate let holder: CameraHolder? @@ -4071,6 +4088,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { public init( context: AccountContext, mode: Mode, + cameraMode: CameraMode = .photo, customTarget: EnginePeer.Id? = nil, resumeLiveStream: Bool = false, holder: CameraHolder? = nil, @@ -4080,6 +4098,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { ) { self.context = context self.mode = mode + self.cameraMode = cameraMode self.customTarget = customTarget self.resumeLiveStream = resumeLiveStream self.holder = holder diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index 5936b26a54..fc56774d12 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -8,7 +8,7 @@ import GlassBackgroundComponent import LiquidLens import TabSelectionRecognizer -extension CameraMode { +extension CameraState.CameraMode { func title(strings: PresentationStrings) -> String { switch self { case .photo: @@ -28,18 +28,18 @@ final class ModeComponent: Component { let isTablet: Bool let strings: PresentationStrings let tintColor: UIColor - let availableModes: [CameraMode] - let currentMode: CameraMode - let updatedMode: (CameraMode) -> Void + let availableModes: [CameraState.CameraMode] + let currentMode: CameraState.CameraMode + let updatedMode: (CameraState.CameraMode) -> Void let tag: AnyObject? init( isTablet: Bool, strings: PresentationStrings, tintColor: UIColor, - availableModes: [CameraMode], - currentMode: CameraMode, - updatedMode: @escaping (CameraMode) -> Void, + availableModes: [CameraState.CameraMode], + currentMode: CameraState.CameraMode, + updatedMode: @escaping (CameraState.CameraMode) -> Void, tag: AnyObject? ) { self.isTablet = isTablet diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 16219b683f..823e154722 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -1428,8 +1428,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .theme: break - case .settings: - break case .premiumOffer: break case .starsTopup: @@ -1465,6 +1463,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { break case .sendGift: break + case .chats, .contacts, .compose, .postStory, .settings, .unknownDeepLink, .oauth: + break } } })) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 2104a6d740..bfe32c4177 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -443,6 +443,10 @@ public final class ChatListHeaderComponent: Component { alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) } + func openEmojiStatusSetup() { + self.chatListTitleView?.openEmojiStatusSetup() + } + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) @@ -815,6 +819,14 @@ public final class ChatListHeaderComponent: Component { private func updateContentStoryOffsets(transition: ComponentTransition) { } + func openEmojiStatusSetup() { + if let storyPeerListView = self.storyPeerList?.view as? StoryPeerListComponent.View { + storyPeerListView.openEmojiStatusSetup() + } else { + self.primaryContentView?.openEmojiStatusSetup() + } + } + func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 9caa3e9d81..f7a2a7246f 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -615,6 +615,12 @@ public final class ChatListNavigationBar: Component { } } + public func openEmojiStatusSetup() { + if let headerContentView = self.headerContent.view as? ChatListHeaderComponent.View { + headerContentView.openEmojiStatusSetup() + } + } + public func updateStoryUploadProgress(storyUploadProgress: [EnginePeer.Id: Float]) { guard let component = self.component else { return diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift index 41cfe13d99..36bd2872e0 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift @@ -172,10 +172,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation particleColor: statusParticleColor, isVisibleForAnimations: true, action: { [weak self] in - guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + guard let self else { return } - strongSelf.openStatusSetup?(titleCredibilityIconView) + self.openEmojiStatusSetup() } )), environment: {}, @@ -327,6 +327,13 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation } } + public func openEmojiStatusSetup() { + guard let titleCredibilityIconView = self.titleCredibilityIconView else { + return + } + self.openStatusSetup?(titleCredibilityIconView) + } + public func updateLayout(availableSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let _ = self.updateLayoutInternal(size: availableSize, transition: transition) return availableSize @@ -414,10 +421,10 @@ public final class ChatListTitleView: UIView, NavigationBarTitleView, Navigation content: statusContent, isVisibleForAnimations: true, action: { [weak self] in - guard let strongSelf = self, let titleCredibilityIconView = strongSelf.titleCredibilityIconView else { + guard let self else { return } - strongSelf.openStatusSetup?(titleCredibilityIconView) + self.openEmojiStatusSetup() } )), environment: {}, diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index ed7ad021be..57358626bd 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -167,15 +167,15 @@ private final class ChatScheduleTimeSheetContentComponent: Component { var contentHeight: CGFloat = 0.0 contentHeight += 30.0 - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelSize = self.cancel.update( transition: transition, component: AnyComponent( GlassBarButtonComponent( size: barButtonSize, - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", diff --git a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift index 61d04d1edf..f2e9a23a6c 100644 --- a/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift +++ b/submodules/TelegramUI/Components/CocoonInfoScreen/Sources/CocoonInfoScreen.swift @@ -52,6 +52,8 @@ private final class CocoonInfoSheetContent: CombinedComponent { fileprivate let playButtonAnimation = ActionSlot() private var didPlayAnimation = false + + var cachedDescription: String? init( context: AccountContext, @@ -91,16 +93,17 @@ private final class CocoonInfoSheetContent: CombinedComponent { } static var body: Body { + let background = Child(GradientBackgroundComponent.self) let closeButton = Child(GlassBarButtonComponent.self) let icon = Child(BundleIconComponent.self) - let title = Child(BalancedTextComponent.self) + let logo = Child(BundleIconComponent.self) let text = Child(BalancedTextComponent.self) let list = Child(List.self) let additionalText = Child(MultilineTextComponent.self) let button = Child(ButtonComponent.self) let navigateDisposable = MetaDisposable() - + return { context in let component = context.component let environment = context.environment[ViewControllerComponentContainer.Environment.self].value @@ -112,7 +115,6 @@ private final class CocoonInfoSheetContent: CombinedComponent { let sideInset: CGFloat = 30.0 + environment.safeInsets.left let textSideInset: CGFloat = 30.0 + environment.safeInsets.left - let titleFont = Font.bold(24.0) let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) @@ -122,42 +124,48 @@ private final class CocoonInfoSheetContent: CombinedComponent { let spacing: CGFloat = 16.0 var contentSize = CGSize(width: context.availableSize.width, height: 28.0) - - let icon = icon.update( - component: BundleIconComponent( - name: "Premium/Cocoon", tintColor: nil - ), - availableSize: context.availableSize, - transition: context.transition - ) - context.add(icon - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) - ) - contentSize.height += icon.size.height - contentSize.height += 14.0 - - let title = title.update( - component: BalancedTextComponent( - text: .plain(NSAttributedString(string: strings.CocoonInfo_Title, font: titleFont, textColor: textColor)), - horizontalAlignment: .center, - maximumNumberOfLines: 0, - lineSpacing: 0.1 - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) - ) - contentSize.height += title.size.height - contentSize.height += spacing - 8.0 + + let descriptionText: String + if let cachedDescription = state.cachedDescription { + descriptionText = cachedDescription + } else { + func updateText(_ input: String) -> String { + let pattern = #"\(([^()]*)\)"# + let boldPattern = #"\*\*(.*?)\*\*"# + + let regex = try! NSRegularExpression(pattern: pattern) + let boldRegex = try! NSRegularExpression(pattern: boldPattern) + + let nsInput = input as NSString + var result = input + + let matches = regex.matches(in: input, range: NSRange(location: 0, length: nsInput.length)) + + for match in matches.reversed() { + let range = match.range(at: 1) + let inner = nsInput.substring(with: range) + + let replacedInner = boldRegex.stringByReplacingMatches( + in: inner, + range: NSRange(location: 0, length: (inner as NSString).length), + withTemplate: "[$1]()" + ) + + result = (result as NSString).replacingCharacters(in: range, with: replacedInner) + } + + return result + } + descriptionText = updateText(strings.CocoonInfo_Description) + state.cachedDescription = descriptionText + } let attributedText = parseMarkdownIntoAttributedString( - strings.CocoonInfo_Description, + descriptionText, attributes: MarkdownAttributes( - body: MarkdownAttributeSet(font: textFont, textColor: textColor), - bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), - link: MarkdownAttributeSet(font: textFont, textColor: linkColor), + body: MarkdownAttributeSet(font: textFont, textColor: UIColor(rgb: 0xb8c9ef)), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xb8c9ef)), + link: MarkdownAttributeSet(font: boldTextFont, textColor: UIColor(rgb: 0xffffff)), linkAttribute: { _ in return nil } ) ) @@ -171,11 +179,52 @@ private final class CocoonInfoSheetContent: CombinedComponent { availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) + + let background = background.update( + component: GradientBackgroundComponent( + colors: [ + UIColor(rgb: 0x061129), + UIColor(rgb: 0x08153d) + ] + ), + availableSize: CGSize(width: context.availableSize.width, height: text.size.height + 220.0), + transition: context.transition + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let icon = icon.update( + component: BundleIconComponent( + name: "Premium/Cocoon", tintColor: nil + ), + availableSize: context.availableSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + icon.size.height / 2.0)) + ) + contentSize.height += icon.size.height + contentSize.height += 14.0 + + let logo = logo.update( + component: BundleIconComponent( + name: "Premium/CocoonLogo", tintColor: nil + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(logo + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + logo.size.height / 2.0)) + ) + contentSize.height += logo.size.height + contentSize.height += spacing - 8.0 + context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + text.size.height / 2.0)) ) contentSize.height += text.size.height - contentSize.height += spacing + 9.0 + contentSize.height += spacing + 31.0 var items: [AnyComponentWithIdentity] = [] items.append( @@ -292,14 +341,14 @@ private final class CocoonInfoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, - isDark: theme.overallDarkAppearance, - state: .generic, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: UIColor(rgb: 0x071533), + isDark: true, + state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", - tintColor: theme.chat.inputPanel.panelControlColor + tintColor: .white ) )), action: { [weak state] _ in @@ -309,7 +358,7 @@ private final class CocoonInfoSheetContent: CombinedComponent { state.dismiss(animated: true) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton @@ -667,3 +716,66 @@ private final class ParagraphComponent: CombinedComponent { } } } + +private final class GradientBackgroundComponent: Component { + let colors: [UIColor] + + init( + colors: [UIColor] + ) { + self.colors = colors + } + + static func ==(lhs: GradientBackgroundComponent, rhs: GradientBackgroundComponent) -> Bool { + if lhs.colors != rhs.colors { + return false + } + return true + } + + public final class View: UIView { + private let gradientLayer: CAGradientLayer + + private var component: GradientBackgroundComponent? + + override init(frame: CGRect) { + self.gradientLayer = CAGradientLayer() + + super.init(frame: frame) + + self.layer.addSublayer(self.gradientLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: GradientBackgroundComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.gradientLayer.frame = CGRect(origin: .zero, size: availableSize) + + var locations: [NSNumber] = [] + let delta = 1.0 / CGFloat(component.colors.count - 1) + for i in 0 ..< component.colors.count { + locations.append((delta * CGFloat(i)) as NSNumber) + } + + self.gradientLayer.locations = locations + self.gradientLayer.colors = component.colors.reversed().map { $0.cgColor } + self.gradientLayer.type = .radial + self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift index 47447151df..f4dffd096e 100644 --- a/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift +++ b/submodules/TelegramUI/Components/Contacts/NewContactScreen/Sources/NewContactScreen.swift @@ -886,12 +886,12 @@ final class NewContactScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButtonSize = self.cancelButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( size: barButtonSize, - backgroundColor: environment.theme.rootController.navigationBar.opaqueBackgroundColor, + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( diff --git a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift index e7da15e90f..36f8a71930 100644 --- a/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift +++ b/submodules/TelegramUI/Components/ContentReportScreen/Sources/ContentReportScreen.swift @@ -519,13 +519,13 @@ private final class SheetContent: CombinedComponent { contentSize.height += navigation.size.height let isBack = items.count > 1 - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let backButton = backButton.update( component: GlassBarButtonComponent( size: barButtonSize, - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( BundleIconComponent( name: isBack ? "Navigation/Back" : "Navigation/Close", diff --git a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal index f2b8683051..17bcd9820e 100644 --- a/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal +++ b/submodules/TelegramUI/Components/DustEffect/Metal/DustEffectShaders.metal @@ -40,6 +40,7 @@ struct Particle { kernel void dustEffectInitializeParticle( device Particle *particles [[ buffer(0) ]], + const device float &verticalDirection [[ buffer(1) ]], uint gid [[ thread_position_in_grid ]] ) { Loki rng = Loki(gid); @@ -49,7 +50,9 @@ kernel void dustEffectInitializeParticle( float direction = rng.rand() * (3.14159265 * 2.0); float velocity = (0.1 + rng.rand() * (0.2 - 0.1)) * 420.0; - particle.velocity = packed_float2(cos(direction) * velocity, sin(direction) * velocity); + float2 initialVelocity = float2(cos(direction) * velocity, sin(direction) * velocity); + initialVelocity.y *= verticalDirection; + particle.velocity = packed_float2(initialVelocity); particle.lifetime = 0.7 + rng.rand() * (1.5 - 0.7); @@ -107,6 +110,7 @@ kernel void dustEffectUpdateParticle( const device uint2 &size [[ buffer(1) ]], const device float &phase [[ buffer(2) ]], const device float &timeStep [[ buffer(3) ]], + const device float &verticalDirection [[ buffer(4) ]], uint gid [[ thread_position_in_grid ]] ) { uint count = size.x * size.y; @@ -124,7 +128,7 @@ kernel void dustEffectUpdateParticle( Particle particle = particles[gid]; particle.offsetFromBasePosition += (particle.velocity * timeStep) * particleFraction; - particle.velocity += float2(0.0, timeStep * 120.0) * particleFraction; + particle.velocity += float2(0.0, timeStep * 120.0 * verticalDirection) * particleFraction; particle.lifetime = max(0.0, particle.lifetime - timeStep * particleFraction); particles[gid] = particle; } diff --git a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift index bec3370cf2..b4686038f7 100644 --- a/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift +++ b/submodules/TelegramUI/Components/DustEffect/Sources/DustEffectLayer.swift @@ -146,6 +146,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject public var animationSpeed: Float = 1.0 public var playsBackwards: Bool = false + public var animateDown: Bool = false public var becameEmpty: (() -> Void)? @@ -280,6 +281,8 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject return } + var verticalDirection: Float = self.animateDown ? -1.0 : 1.0 + for item in self.items { guard let particleBuffer = item.particleBuffer else { continue @@ -297,6 +300,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject if !item.particleBufferIsInitialized { item.particleBufferIsInitialized = true computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle) + computeEncoder.setBytes(&verticalDirection, length: 4, index: 1) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) } @@ -309,6 +313,7 @@ public final class DustEffectLayer: MetalEngineSubjectLayer, MetalEngineSubject var timeStep: Float = Float(lastTimeStep) / Float(UIView.animationDurationFactor()) timeStep *= 2.0 computeEncoder.setBytes(&timeStep, length: 4, index: 3) + computeEncoder.setBytes(&verticalDirection, length: 4, index: 4) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) } } diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD index 0200885e73..945235a576 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/BUILD @@ -48,6 +48,7 @@ swift_library( "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Components/ResizableSheetComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift index 59a534c9f0..ec2173bae3 100644 --- a/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift +++ b/submodules/TelegramUI/Components/EmojiGameStakeScreen/Sources/EmojiGameStakeScreen.swift @@ -31,6 +31,7 @@ import LottieComponent import LottieComponentResourceContent import EdgeEffect import PlainButtonComponent +import ResizableSheetComponent private let amountTag = GenericComponentViewTag() @@ -465,10 +466,10 @@ private final class EmojiGameStakeSheetComponent: CombinedComponent { ), leftItem: AnyComponent( GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -480,25 +481,25 @@ private final class EmojiGameStakeSheetComponent: CombinedComponent { } ) ), - bottomItem: AnyComponent( - ButtonComponent( - background: ButtonComponent.Background( - style: .glass, - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(HStack(buttonItems, spacing: 7.0)) - ), - isEnabled: true, - displaysProgress: false, - action: { - dismiss(true) - } - ) - ), +// bottomItem: AnyComponent( +// ButtonComponent( +// background: ButtonComponent.Background( +// style: .glass, +// color: theme.list.itemCheckColors.fillColor, +// foreground: theme.list.itemCheckColors.foregroundColor, +// pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) +// ), +// content: AnyComponentWithIdentity( +// id: AnyHashable(0), +// component: AnyComponent(HStack(buttonItems, spacing: 7.0)) +// ), +// isEnabled: true, +// displaysProgress: false, +// action: { +// dismiss(true) +// } +// ) +// ), backgroundColor: .color(theme.list.blocksBackgroundColor), animateOut: animateOut ), @@ -512,6 +513,7 @@ private final class EmojiGameStakeSheetComponent: CombinedComponent { deviceMetrics: environment.deviceMetrics, isDisplaying: environment.value.isVisible, isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in dismiss(animated) @@ -1650,571 +1652,3 @@ private struct EmojiGameStakeConfiguration { } } } - - - - - - - - - - -public final class ResizableSheetComponentEnvironment: Equatable { - public let theme: PresentationTheme - public let statusBarHeight: CGFloat - public let safeInsets: UIEdgeInsets - public let metrics: LayoutMetrics - public let deviceMetrics: DeviceMetrics - public let isDisplaying: Bool - public let isCentered: Bool - public let regularMetricsSize: CGSize? - public let dismiss: (Bool) -> Void - - public init( - theme: PresentationTheme, - statusBarHeight: CGFloat, - safeInsets: UIEdgeInsets, - metrics: LayoutMetrics, - deviceMetrics: DeviceMetrics, - isDisplaying: Bool, - isCentered: Bool, - regularMetricsSize: CGSize?, - dismiss: @escaping (Bool) -> Void - ) { - self.theme = theme - self.statusBarHeight = statusBarHeight - self.safeInsets = safeInsets - self.metrics = metrics - self.deviceMetrics = deviceMetrics - self.isDisplaying = isDisplaying - self.isCentered = isCentered - self.regularMetricsSize = regularMetricsSize - self.dismiss = dismiss - } - - public static func ==(lhs: ResizableSheetComponentEnvironment, rhs: ResizableSheetComponentEnvironment) -> Bool { - if lhs.theme != rhs.theme { - return false - } - if lhs.statusBarHeight != rhs.statusBarHeight { - return false - } - if lhs.safeInsets != rhs.safeInsets { - return false - } - if lhs.metrics != rhs.metrics { - return false - } - if lhs.deviceMetrics != rhs.deviceMetrics { - return false - } - if lhs.isDisplaying != rhs.isDisplaying { - return false - } - if lhs.isCentered != rhs.isCentered { - return false - } - if lhs.regularMetricsSize != rhs.regularMetricsSize { - return false - } - return true - } -} - -public final class ResizableSheetComponent: Component { - public typealias EnvironmentType = (ChildEnvironmentType, ResizableSheetComponentEnvironment) - - public class ExternalState { - public fileprivate(set) var contentHeight: CGFloat - - public init() { - self.contentHeight = 0.0 - } - } - - public enum BackgroundColor: Equatable { - case color(UIColor) - } - - public let content: AnyComponent - public let titleItem: AnyComponent? - public let leftItem: AnyComponent? - public let rightItem: AnyComponent? - public let bottomItem: AnyComponent? - public let backgroundColor: BackgroundColor - public let externalState: ExternalState? - public let animateOut: ActionSlot> - - public init( - content: AnyComponent, - titleItem: AnyComponent? = nil, - leftItem: AnyComponent? = nil, - rightItem: AnyComponent? = nil, - bottomItem: AnyComponent? = nil, - backgroundColor: BackgroundColor, - externalState: ExternalState? = nil, - animateOut: ActionSlot>, - ) { - self.content = content - self.titleItem = titleItem - self.leftItem = leftItem - self.rightItem = rightItem - self.bottomItem = bottomItem - self.backgroundColor = backgroundColor - self.externalState = externalState - self.animateOut = animateOut - } - - public static func ==(lhs: ResizableSheetComponent, rhs: ResizableSheetComponent) -> Bool { - if lhs.content != rhs.content { - return false - } - if lhs.titleItem != rhs.titleItem { - return false - } - if lhs.leftItem != rhs.leftItem { - return false - } - if lhs.rightItem != rhs.rightItem { - return false - } - if lhs.bottomItem != rhs.bottomItem { - return false - } - if lhs.backgroundColor != rhs.backgroundColor { - return false - } - if lhs.animateOut != rhs.animateOut { - return false - } - return true - } - - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var containerCornerRadius: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.containerCornerRadius = containerCornerRadius - self.bottomInset = bottomInset - self.topInset = topInset - } - } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { - public final class Tag { - public init() { - } - } - - public func matches(tag: Any) -> Bool { - if let _ = tag as? Tag { - return true - } - return false - } - - private let dimView: UIView - private let containerView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - - private let topEdgeEffectView: EdgeEffectView - private let contentView: ComponentView - - private var titleItemView: ComponentView? - private var leftItemView: ComponentView? - private var rightItemView: ComponentView? - private var bottomItemView: ComponentView? - - private let backgroundHandleView: UIImageView - - private var ignoreScrolling: Bool = false - - private var component: ResizableSheetComponent? - private weak var state: EmptyComponentState? - private var isUpdating: Bool = false - private var environment: ResizableSheetComponentEnvironment? - private var itemLayout: ItemLayout? - - override init(frame: CGRect) { - self.dimView = UIView() - self.containerView = UIView() - - self.containerView.clipsToBounds = true - self.containerView.layer.cornerRadius = 40.0 - self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 40.0 - - self.backgroundHandleView = UIImageView() - - self.navigationBarContainer = SparseContainerView() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - - self.topEdgeEffectView = EdgeEffectView() - self.topEdgeEffectView.clipsToBounds = true - self.topEdgeEffectView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.topEdgeEffectView.layer.cornerRadius = 40.0 - - self.contentView = ComponentView() - - super.init(frame: frame) - - self.addSubview(self.dimView) - self.addSubview(self.containerView) - self.containerView.layer.addSublayer(self.backgroundLayer) - - self.scrollView.delaysContentTouches = true - self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - self.scrollView.contentInsetAdjustmentBehavior = .never - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.containerView.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.containerView.addSubview(self.navigationBarContainer) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.dismissAnimated() - } - } - - func dismissAnimated() { - guard let environment = self.environment else { - return - } - self.endEditing(true) - environment.dismiss(true) - } - - private func updateScrolling(transition: ComponentTransition) { - guard let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - var topOffsetFraction = self.scrollView.bounds.minY / 100.0 - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width - let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 - let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius - - let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction - let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) - let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction - - var containerTransform = CATransform3DIdentity - containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) - containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) - transition.setTransform(view: self.containerView, transform: containerTransform) - transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) - } - - private var didPlayAppearanceAnimation = false - func animateIn() { - self.didPlayAppearanceAnimation = true - - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - } - - func update(component: ResizableSheetComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.isUpdating = true - defer { - self.isUpdating = false - } - - let sheetEnvironment = environment[ResizableSheetComponentEnvironment.self].value - component.animateOut.connect { [weak self] completion in - guard let self else { - return - } - self.animateOut { - completion(Void()) - } - } - - let themeUpdated = self.environment?.theme !== sheetEnvironment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - - let fillingSize: CGFloat - if case .regular = sheetEnvironment.metrics.widthClass { - fillingSize = min(availableSize.width, 414.0) - sheetEnvironment.safeInsets.left * 2.0 - } else { - fillingSize = min(availableSize.width, sheetEnvironment.deviceMetrics.screenSize.width) - sheetEnvironment.safeInsets.left * 2.0 - } - let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) - - self.component = component - self.state = state - self.environment = sheetEnvironment - - let theme = sheetEnvironment.theme.withModalBlocksBackground() - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor - } - - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - - var containerSize: CGSize - if !"".isEmpty, sheetEnvironment.isCentered { - let verticalInset: CGFloat = 44.0 - let maxSide = max(availableSize.width, availableSize.height) - let minSide = min(availableSize.width, availableSize.height) - containerSize = CGSize(width: min(availableSize.width - 20.0, floor(maxSide / 2.0)), height: min(availableSize.height, minSide) - verticalInset * 2.0) - if let regularMetricsSize = sheetEnvironment.regularMetricsSize { - containerSize = regularMetricsSize - } - } else { - containerSize = CGSize(width: fillingSize, height: .greatestFiniteMagnitude) - } - - let containerInset: CGFloat = sheetEnvironment.statusBarHeight + 10.0 - let clippingY: CGFloat - - self.contentView.parentState = state - let contentViewSize = self.contentView.update( - transition: transition, - component: component.content, - environment: { - environment[ChildEnvironmentType.self] - }, - containerSize: containerSize - ) - component.externalState?.contentHeight = contentViewSize.height - - if let contentView = self.contentView.view { - if contentView.superview == nil { - self.scrollContentView.addSubview(contentView) - } - transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: contentViewSize)) - } - - let contentHeight = contentViewSize.height - let initialContentHeight = contentHeight - - let edgeEffectHeight: CGFloat = 80.0 - let edgeEffectFrame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: edgeEffectHeight)) - transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame) - self.topEdgeEffectView.update(content: theme.actionSheet.opaqueItemBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition) - if self.topEdgeEffectView.superview == nil { - self.navigationBarContainer.insertSubview(self.topEdgeEffectView, at: 0) - } - - if let titleItem = component.titleItem { - let titleItemView: ComponentView - if let current = self.titleItemView { - titleItemView = current - } else { - titleItemView = ComponentView() - self.titleItemView = titleItemView - } - - let titleItemSize = titleItemView.update( - transition: transition, - component: titleItem, - environment: {}, - containerSize: CGSize(width: containerSize.width - 66.0 * 2.0, height: 66.0) - ) - let titleItemFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerSize.width - titleItemSize.width)) / 2.0, y: floorToScreenPixels(36.0 - titleItemSize.height * 0.5)), size: titleItemSize) - if let view = titleItemView.view { - if view.superview == nil { - self.navigationBarContainer.addSubview(view) - } - transition.setFrame(view: view, frame: titleItemFrame) - } - } else if let titleItemView = self.titleItemView { - self.titleItemView = nil - titleItemView.view?.removeFromSuperview() - } - - - if let leftItem = component.leftItem { - let leftItemView: ComponentView - if let current = self.leftItemView { - leftItemView = current - } else { - leftItemView = ComponentView() - self.leftItemView = leftItemView - } - - let leftItemSize = leftItemView.update( - transition: transition, - component: leftItem, - environment: {}, - containerSize: CGSize(width: 66.0, height: 66.0) - ) - let leftItemFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: leftItemSize) - if let view = leftItemView.view { - if view.superview == nil { - self.navigationBarContainer.addSubview(view) - } - transition.setFrame(view: view, frame: leftItemFrame) - } - } else if let leftItemView = self.leftItemView { - self.leftItemView = nil - leftItemView.view?.removeFromSuperview() - } - - if let rightItem = component.rightItem { - let rightItemView: ComponentView - if let current = self.rightItemView { - rightItemView = current - } else { - rightItemView = ComponentView() - self.rightItemView = rightItemView - } - - let rightItemSize = rightItemView.update( - transition: transition, - component: rightItem, - environment: {}, - containerSize: CGSize(width: 66.0, height: 66.0) - ) - let rightItemFrame = CGRect(origin: CGPoint(x: containerSize.width - 16.0 - rightItemSize.width, y: 16.0), size: rightItemSize) - if let view = rightItemView.view { - if view.superview == nil { - self.navigationBarContainer.addSubview(view) - } - transition.setFrame(view: view, frame: rightItemFrame) - } - } else if let rightItemView = self.rightItemView { - self.rightItemView = nil - rightItemView.view?.removeFromSuperview() - } - - - clippingY = availableSize.height - - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.scrollContentClippingView.layer.cornerRadius = 38.0 - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: sheetEnvironment.deviceMetrics.screenCornerRadius, bottomInset: sheetEnvironment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) - - transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) - transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - if sheetEnvironment.isDisplaying && !self.didPlayAppearanceAnimation { - self.animateIn() - } - - return availableSize - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift index 177ed05c0f..93b7c74637 100644 --- a/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift +++ b/submodules/TelegramUI/Components/FaceScanScreen/Sources/AgeVerificationScreen.swift @@ -109,10 +109,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -123,7 +123,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD new file mode 100644 index 0000000000..e5a3568ad1 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/BUILD @@ -0,0 +1,56 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftCraftScreen", + module_name = "GiftCraftScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", + "//submodules/TelegramStringFormatting", + "//submodules/Markdown", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/LocalMediaResources", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/TelegramUI/Components/Gifts/InfoParagraphComponent", + "//submodules/TelegramUI/Components/Gifts/GiftAnimationComponent", + "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/AnimatedTextComponent", + "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent", + "//submodules/Components/MultilineTextWithEntitiesComponent", + "//submodules/TooltipUI", + "//submodules/TextFormat", + "//submodules/TelegramUI/Components/Gifts/GiftStoreScreen", + "//submodules/TelegramUI/Components/SpaceWarpView", + "//submodules/ConfettiEffect", + "//submodules/TelegramNotices", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift new file mode 100644 index 0000000000..251e5097e7 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift @@ -0,0 +1,791 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import GiftItemComponent +import GlassBackgroundComponent +import GlassBarButtonComponent +import BundleIconComponent +import LottieComponent + +private let cubeSide: CGFloat = 110.0 + +struct GiftItem: Equatable { + let gift: StarGift.UniqueGift + let reference: StarGiftReference +} + +final class CraftTableComponent: Component { + enum Result { + case gift(ProfileGiftsContext.State.StarGift) + case fail + } + + let context: AccountContext + let gifts: [Int32: GiftItem] + let buttonColor: UIColor + let isCrafting: Bool + let result: Result? + let select: (Int32) -> Void + let remove: (Int32) -> Void + let willFinish: (Bool) -> Void + let finished: (UIView?) -> Void + + public init( + context: AccountContext, + gifts: [Int32: GiftItem], + buttonColor: UIColor, + isCrafting: Bool, + result: Result?, + select: @escaping (Int32) -> Void, + remove: @escaping (Int32) -> Void, + willFinish: @escaping (Bool) -> Void, + finished: @escaping (UIView?) -> Void + ) { + self.context = context + self.gifts = gifts + self.buttonColor = buttonColor + self.isCrafting = isCrafting + self.result = result + self.select = select + self.remove = remove + self.willFinish = willFinish + self.finished = finished + } + + public static func ==(lhs: CraftTableComponent, rhs: CraftTableComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gifts != rhs.gifts { + return false + } + if lhs.buttonColor != rhs.buttonColor { + return false + } + if lhs.isCrafting != rhs.isCrafting { + return false + } + return true + } + + public final class View: UIView { + private var selectedGifts: [AnyHashable: ComponentView] = [:] + private var faces: [AnyHashable: ComponentView] = [:] + private let successFace = ComponentView() + + private let anvilPlayOnce = ActionSlot() + private let animationView = CubeAnimationView() + + private let failOverlay = ComponentView() + private let craftFailOverlayPlayOnce = ActionSlot() + + private let craftFailPlayOnce = ActionSlot() + + private var didSetupFinishAnimation = false + private var flipFaces = false + + private var isSuccess = false + private var isFailed = false + private var failDidStartCrossAnimation = false + private var failDidBringToFront = false + private var failWillFinish = false + private var failDidFinish = false + + private var component: CraftTableComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.animationView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupFailAnimation() { + guard !self.didSetupFinishAnimation else { + return + } + self.didSetupFinishAnimation = true + + self.animationView.onFinishApproach = { [weak self] isUpsideDown in + guard let self else { + return + } + self.isFailed = true + self.animationView.setSticker(nil, face: 0, mirror: false) + + var availableStickers: [ComponentView] = [] + for gift in self.selectedGifts.values { + availableStickers.append(gift) + } + for i in 0 ..< min(2, availableStickers.count) { + if let sticker = availableStickers[i].view { + self.animationView.setSticker(sticker, face: 3 - i, mirror: isUpsideDown) + } + } + + self.state?.updated() + + if let failOverlayView = self.failOverlay.view as? LottieComponent.View { + failOverlayView.isHidden = false + failOverlayView.onFrameUpdate = { [weak self] frameIndex in + guard let self else { + return + } + if frameIndex >= 5 && !self.failDidStartCrossAnimation { + self.failDidStartCrossAnimation = true + self.craftFailPlayOnce.invoke(Void()) + } + if frameIndex >= 65 && !self.failDidBringToFront { + self.failDidBringToFront = true + failOverlayView.superview?.bringSubviewToFront(failOverlayView) + + self.animationView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) + } + if frameIndex >= 75 && !self.failWillFinish { + self.failWillFinish = true + self.component?.willFinish(false) + } + if frameIndex >= 82 && !self.failDidFinish { + self.failDidFinish = true + + failOverlayView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.component?.finished(nil) + } + } + } + self.craftFailOverlayPlayOnce.invoke(Void()) + } + } + + func setupSuccessAnimation(_ gift: StarGift.UniqueGift) { + guard !self.didSetupFinishAnimation, let component = self.component else { + return + } + self.didSetupFinishAnimation = true + + self.animationView.onFinishApproach = { [weak self] isUpsideDown in + guard let self else { + return + } + self.isSuccess = true + + var availableStickers: [ComponentView] = [] + for gift in self.selectedGifts.values { + availableStickers.append(gift) + } + for i in 0 ..< min(2, availableStickers.count) { + if let sticker = availableStickers[i].view { + let face: Int + if isUpsideDown { + face = i + 1 + } else { + face = 3 - i + } + self.animationView.setSticker(sticker, face: face, mirror: isUpsideDown) + } + } + + self.flipFaces = isUpsideDown + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let _ = self.successFace.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: component.context, + style: .glass, + theme: presentationData.theme, + strings: presentationData.strings, + peer: nil, + subject: .uniqueGift(gift: gift, price: nil), + ribbon: nil, + resellPrice: nil, + isHidden: false, + isSelected: false, + isPinned: false, + isEditing: false, + mode: .grid, + cornerRadius: 28.0, + action: nil, + contextAction: nil + ) + ), + environment: {}, + containerSize: CGSize(width: cubeSide, height: cubeSide) + ) + if let successView = self.successFace.view as? GiftItemComponent.View { + let backgroundLayer = successView.backgroundLayer + if let patternView = successView.pattern { + backgroundLayer.opacity = 0.0 + patternView.alpha = 0.0 + Queue.mainQueue().after(1.0, { + let transition = ComponentTransition.easeInOut(duration: 0.3) + + transition.animateBlur(layer: backgroundLayer, fromRadius: 10.0, toRadius: 0.0) + transition.setAlpha(layer: backgroundLayer, alpha: 1.0) + + transition.setAlpha(view: patternView, alpha: 1.0) + transition.animateBlur(layer: patternView.layer, fromRadius: 10.0, toRadius: 0.0) + + Queue.mainQueue().after(1.0, { + self.component?.finished(successView) + }) + }) + } + + self.animationView.setSticker(successView, face: 0, mirror: isUpsideDown) + } + self.state?.updated() + } + } + + func update(component: CraftTableComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + self.animationView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + + let permilleValue = component.gifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) }) + + for index in 0 ..< 6 { + let face: ComponentView + if let current = self.faces[index] { + face = current + } else { + face = ComponentView() + self.faces[index] = face + } + + let faceComponent: AnyComponent + var faceItems: [AnyComponentWithIdentity] = [] + if index == 0 { + faceItems.append( + AnyComponentWithIdentity(id: "background", component: AnyComponent( + RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + )) + ) + if !component.isCrafting { + faceItems.append( + AnyComponentWithIdentity(id: "glass", component: AnyComponent( + GlassBackgroundComponent(size: CGSize(width: cubeSide, height: cubeSide), cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor))) + )) + ) + } + if self.isFailed { + faceItems.append( + AnyComponentWithIdentity(id: "fail", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: "CraftFail"), + size: CGSize(width: 96.0, height: 96.0), + playOnce: self.craftFailPlayOnce + ) + )) + ) + } else if !self.isSuccess { + faceItems.append( + AnyComponentWithIdentity(id: "dial", component: AnyComponent( + DialIndicatorComponent( + content: AnyComponentWithIdentity(id: "empty", component: AnyComponent(Rectangle(color: .clear))), + backgroundColor: .white.withAlphaComponent(0.1), + foregroundColor: .white, + diameter: 84.0, + lineWidth: 5.0, + fontSize: 18.0, + percentage: permilleValue / 10, + isVisible: !component.isCrafting + ) + )) + ) + faceItems.append( + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: "Anvil"), + size: CGSize(width: 52.0, height: 52.0), + playOnce: self.anvilPlayOnce + ) + )) + ) + } + } else { + faceItems.append( + AnyComponentWithIdentity(id: "background", component: AnyComponent( + RoundedRectangle(color: component.buttonColor, cornerRadius: 28.0) + )) + ) + faceItems.append( + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: "Components/CubeSide", tintColor: nil, flipVertically: self.flipFaces) + )) + ) + } + faceComponent = AnyComponent( + ZStack(faceItems) + ) + + let _ = face.update( + transition: transition, + component: faceComponent, + environment: {}, + containerSize: CGSize(width: cubeSide, height: cubeSide) + ) + } + + if previousComponent == nil { + var faceViews: [UIView] = [] + for index in 0 ..< 6 { + if let faceView = self.faces[index]?.view { + faceView.bounds = CGRect(origin: .zero, size: CGSize(width: cubeSide, height: cubeSide)) + faceView.clipsToBounds = true + faceView.layer.rasterizationScale = UIScreenScale + faceView.layer.cornerRadius = 28.0 + faceViews.append(faceView) + } + } + self.animationView.setFaces(faceViews) + } + + var stickerViews: [UIView] = [] + for index in 0 ..< 4 { + let itemId = AnyHashable(index) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.selectedGifts[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.selectedGifts[itemId] = visibleItem + itemTransition = .immediate + } + + let gift = component.gifts[Int32(index)] + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + GiftSlotComponent( + context: component.context, + gift: gift, + buttonColor: component.buttonColor, + isCrafting: component.isCrafting, + action: { + component.select(Int32(index)) + }, + removeAction: index > 0 ? { + component.remove(Int32(index)) + } : nil + ) + ), + environment: {}, + containerSize: CGSize(width: cubeSide, height: cubeSide) + ) + if let itemView = visibleItem.view { + stickerViews.append(itemView) + } + } + + if previousComponent == nil { + self.animationView.setStickers(stickerViews) + } + + if let previousComponent, previousComponent.isCrafting != component.isCrafting { + var indices: [Int] = [] + for index in component.gifts.keys.sorted() { + indices.append(Int(index)) + } + self.anvilPlayOnce.invoke(Void()) + Queue.mainQueue().after(0.6, { + self.animationView.startStickerSequence(indices: indices) + + switch component.result { + case let .gift(gift): + if case let .unique(uniqueGift) = gift.gift { + self.setupSuccessAnimation(uniqueGift) + } + case .fail: + self.setupFailAnimation() + default: + break + } + }) + } + + if self.isFailed { + let failOverlaySize = self.failOverlay.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.AppBundleContent(name: "CraftFailOverlay"), + size: CGSize(width: availableSize.width, height: availableSize.width), + playOnce: self.craftFailOverlayPlayOnce + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.width) + ) + let failOverlayFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - failOverlaySize.width) / 2.0), y: floor((availableSize.height - failOverlaySize.height) / 2.0)), size: failOverlaySize) + if let failOverlayView = self.failOverlay.view { + if failOverlayView.superview == nil { + failOverlayView.isHidden = true + self.insertSubview(failOverlayView, belowSubview: self.animationView) + } + failOverlayView.frame = failOverlayFrame + } + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +final class GiftSlotComponent: Component { + let context: AccountContext + let gift: GiftItem? + let buttonColor: UIColor + let isCrafting: Bool + let action: () -> Void + let removeAction: (() -> Void)? + + public init( + context: AccountContext, + gift: GiftItem?, + buttonColor: UIColor, + isCrafting: Bool, + action: @escaping () -> Void, + removeAction: (() -> Void)? + ) { + self.context = context + self.gift = gift + self.buttonColor = buttonColor + self.isCrafting = isCrafting + self.action = action + self.removeAction = removeAction + } + + public static func ==(lhs: GiftSlotComponent, rhs: GiftSlotComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + if lhs.buttonColor != rhs.buttonColor { + return false + } + if lhs.isCrafting != rhs.isCrafting { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView = GlassBackgroundView() + private let addIcon = UIImageView() + private var icon: ComponentView? + private let button = HighlightTrackingButton() + + private var badge: ComponentView? + private var removeIcon: ComponentView? + private let removeButton = HighlightTrackingButton() + + private var component: GiftSlotComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addIcon.image = generateAddIcon(backgroundColor: .white) + + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.addIcon) + self.backgroundView.contentView.addSubview(self.button) + self.addSubview(self.removeButton) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + self.removeButton.addTarget(self, action: #selector(self.removeButtonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + guard let _ = self.component?.removeAction else { + return + } + self.component?.action() + } + + @objc private func removeButtonPressed() { + self.component?.removeAction?() + } + + func update(component: GiftSlotComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let backgroundFrame = CGRect(origin: .zero, size: availableSize).insetBy(dx: 1.0, dy: 1.0) + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 28.0, isDark: true, tintColor: .init(kind: .custom(style: .default, color: component.buttonColor)), transition: .immediate) + transition.setFrame(view: self.backgroundView, frame: backgroundFrame) + if component.gift == nil && component.isCrafting { + transition.setBlur(layer: self.backgroundView.layer, radius: 10.0) + self.backgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) + transition.setBlur(layer: self.addIcon.layer, radius: 10.0) + } + transition.setAlpha(view: self.addIcon, alpha: component.isCrafting ? 0.0 : 1.0) + + if let icon = self.addIcon.image { + transition.setFrame(view: self.addIcon, frame: CGRect(origin: CGPoint(x: floor((backgroundFrame.width - icon.size.width) / 2.0), y: floor((backgroundFrame.height - icon.size.height) / 2.0)), size: icon.size)) + } + + if previousComponent?.gift?.gift.id != component.gift?.gift.id { + if let iconView = self.icon?.view { + if transition.animation.isImmediate { + iconView.removeFromSuperview() + } else { + transition.setScale(view: iconView, scale: 0.01) + transition.setAlpha(view: iconView, alpha: 0.0, completion: { _ in + iconView.removeFromSuperview() + }) + } + } + self.icon = nil + } + + if (previousComponent?.gift?.gift.id == nil) != (component.gift?.gift.id == nil) || ((previousComponent?.isCrafting ?? false) != component.isCrafting && component.isCrafting) { + if let badgeView = self.badge?.view { + if transition.animation.isImmediate { + badgeView.removeFromSuperview() + } else { + transition.setBlur(layer: badgeView.layer, radius: 10.0) + transition.setAlpha(view: badgeView, alpha: 0.0, completion: { _ in + badgeView.removeFromSuperview() + }) + } + } + self.badge = nil + + if let removeButtonView = self.removeIcon?.view { + if transition.animation.isImmediate { + removeButtonView.removeFromSuperview() + } else { + transition.setBlur(layer: removeButtonView.layer, radius: 10.0) + transition.setAlpha(view: removeButtonView, alpha: 0.0, completion: { _ in + removeButtonView.removeFromSuperview() + }) + } + } + self.removeIcon = nil + } + + if let gift = component.gift { + let icon: ComponentView + var iconTransition = transition + if let current = self.icon { + icon = current + } else { + iconTransition = .immediate + icon = ComponentView() + self.icon = icon + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + let iconSize = icon.update( + transition: iconTransition, + component: AnyComponent( + GiftItemComponent( + context: component.context, + style: .glass, + theme: presentationData.theme, + strings: presentationData.strings, + peer: nil, + subject: .uniqueGift(gift: gift.gift, price: nil), + ribbon: nil, + resellPrice: nil, + isHidden: false, + isSelected: false, + isPinned: false, + isEditing: false, + mode: .grid, + cornerRadius: 28.0, + action: nil, + contextAction: nil + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: iconSize) + if let iconView = icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + if let badgeView = self.badge?.view { + self.backgroundView.contentView.insertSubview(iconView, belowSubview: badgeView) + } else { + self.backgroundView.contentView.addSubview(iconView) + } + + if !transition.animation.isImmediate { + transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) + transition.animateScale(view: iconView, from: 0.01, to: 1.0) + } + } + iconTransition.setFrame(view: iconView, frame: iconFrame) + } + + if !component.isCrafting { + var buttonColor: UIColor = component.buttonColor + if let backdropAttribute = gift.gift.attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(0.65) + } + + let badge: ComponentView + var badgeTransition = transition + if let current = self.badge { + badge = current + } else { + badgeTransition = .immediate + badge = ComponentView() + self.badge = badge + } + + let badgeSize = badge.update( + transition: badgeTransition, + component: AnyComponent( + ZStack([ + AnyComponentWithIdentity(id: "background", component: AnyComponent( + RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 54.0, height: 27.0)) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + Text(text: "\((gift.gift.craftChancePermille ?? 0) / 10)%", font: Font.semibold(17.0), color: .white) + )) + ]) + ), + environment: {}, + containerSize: CGSize(width: 54.0, height: 27.0) + ) + let badgeFrame = CGRect(origin: CGPoint(x: -6.0, y: -6.0 - UIScreenPixel), size: badgeSize) + if let badgeView = badge.view { + if badgeView.superview == nil { + badgeView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(badgeView) + + if !transition.animation.isImmediate { + transition.animateAlpha(view: badgeView, from: 0.0, to: 1.0) + transition.animateScale(view: badgeView, from: 0.01, to: 1.0) + } + } + badgeTransition.setFrame(view: badgeView, frame: badgeFrame) + } + + + if let _ = component.removeAction { + let removeButton: ComponentView + var removeButtonTransition = transition + if let current = self.removeIcon { + removeButton = current + } else { + removeButtonTransition = .immediate + removeButton = ComponentView() + self.removeIcon = removeButton + } + + let removeButtonSize = removeButton.update( + transition: removeButtonTransition, + component: AnyComponent( + ZStack([ + AnyComponentWithIdentity(id: "background", component: AnyComponent( + RoundedRectangle(color: buttonColor, cornerRadius: 13.5, size: CGSize(width: 27.0, height: 27.0)) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + BundleIconComponent(name: "Media Gallery/PictureInPictureClose", tintColor: .white) + )) + ]) + ), + environment: {}, + containerSize: CGSize(width: 27.0, height: 27.0) + ) + let removeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - 21.0, y: -6.0 - UIScreenPixel), size: removeButtonSize) + if let removeButtonView = removeButton.view { + if removeButtonView.superview == nil { + removeButtonView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(removeButtonView) + + if !transition.animation.isImmediate { + transition.animateAlpha(view: removeButtonView, from: 0.0, to: 1.0) + transition.animateScale(view: removeButtonView, from: 0.01, to: 1.0) + } + } + removeButtonTransition.setFrame(view: removeButtonView, frame: removeButtonFrame) + } + } + } + } + + self.isUserInteractionEnabled = !component.isCrafting + self.button.frame = CGRect(origin: .zero, size: availableSize) + + self.removeButton.isUserInteractionEnabled = component.removeAction != nil + if let removeIcon = self.removeIcon?.view { + self.removeButton.frame = removeIcon.frame.insetBy(dx: -8.0, dy: -8.0) + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private func generateAddIcon(backgroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 46.0, height: 46.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size)) + + context.setBlendMode(.clear) + context.setStrokeColor(UIColor.clear.cgColor) + context.setLineWidth(4.0) + context.setLineCap(.round) + + context.move(to: CGPoint(x: 23.0, y: 13.0)) + context.addLine(to: CGPoint(x: 23.0, y: 33.0)) + context.strokePath() + + context.move(to: CGPoint(x: 13.0, y: 23.0)) + context.addLine(to: CGPoint(x: 33.0, y: 23.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift new file mode 100644 index 0000000000..d65ce5986c --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeAnimationView.swift @@ -0,0 +1,773 @@ +import UIKit +import simd +import Display + +final class Transform3DView: UIView { + override class var layerClass: AnyClass { CATransformLayer.self } +} + +final class PassthroughView: UIView { + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for subview in self.subviews where !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled { + let converted = self.convert(point, to: subview) + if subview.point(inside: converted, with: event) { + return true + } + } + return false + } +} + +final class CubeAnimationView: UIView { + private let cubeSize: CGFloat + private var perspective: CGFloat = 400.0 + private let stickerSize: CGFloat + private let stickerGap: CGFloat + + private let camera = UIView() + private let cubeContainer = Transform3DView() + private var faces: [UIView] = [] + private var faceOccupants: [Int: UIView] = [:] + + let stickerContainer = PassthroughView() + + private var stickers: [UIView] = [] + private var isRunning = false + + private var displayLink: SharedDisplayLinkDriver.Link? + private var lastTimestamp: CFTimeInterval = 0 + private var warpDisplayLink: SharedDisplayLinkDriver.Link? + private weak var warpView: UIView? + private var warpStartQuad: Quad? + private var warpEndQuad: Quad? + private var warpDuration: TimeInterval = 0 + private var warpDynamicTarget: (() -> Quad)? + private var warpCompletion: (() -> Void)? + private var warpStartTimestamp: CFTimeInterval = 0 + private var warpLastProgress: CGFloat = 0 + private var warpCurrentQuad: Quad? + private var warpHasCompleted = false + private var warpSnapshot: UIView? + + private var rotation = SIMD3(repeating: 0) + private var angularVelocity = SIMD3(repeating: 0) + + private let dampingPerSecond: Float = 0.66 + private let finishSpringX: Float = 28.0 + private let finishSpringY: Float = 18.0 + private let finishDampingX: Float = 2.0 * sqrt(28.0) + private let finishDampingY: Float = 2.0 * sqrt(18.0) + private let finishWobbleAmplitudeZ: Float = 10.0 * .pi / 180.0 + private let finishWobbleCycles: Float = 1.0 + private let finishWobbleDampingExponent: Float = 0.6 + private let finishSuccessScale: Float = 1.3 + private let finishSuccessScaleTriggerAngle: Float = 0.4 * .pi + private let finishApproachTriggerAngle: Float = 1.5 * .pi + private let baseImpulseStrength: Float = 4.0 + private let impactNudgeDistance: CGFloat = 20.0 + private let impactNudgeEmphasis: CGFloat = 28.0 + + private var isFinishingX = false + private var isFinishingY = false + private var finishTargetX: Float = 0.0 + private var finishTargetY: Float = 0.0 + private var finishDirectionY: Float = 1.0 + private var finishRotationY: Float = 0.0 + private var finishTargetYUnwrapped: Float = 0.0 + private var finishRemainingYStart: Float = 0.0 + private var finishDelayTimerX: Timer? + private var finishDelayTimerY: Timer? + private var cubeScale: Float = 1.0 + private var hasFiredFinishApproach = false + + var onFinishApproach: ((Bool) -> Void)? + + private let defaultStickOrder: [Int] = [0, 5, 4, 3] + private let sequenceStickOrders: [String: [Int]] = [ + "0": [0], + "0,1": [0, 5], + "0,2": [0, 5], + "0,3": [0, 5], + "0,1,2": [0, 5, 4], + "0,1,3": [0, 5, 2], + "0,2,3": [0, 5, 1], + "0,1,2,3": [0, 5, 4, 3] + ] + private var activeStickOrder: [Int] = [] + + init(cubeSize: CGFloat = 110.0, stickerSize: CGFloat = 76.0, stickerGap: CGFloat = 30.0) { + self.cubeSize = cubeSize + self.stickerSize = stickerSize + self.stickerGap = stickerGap + + super.init(frame: .zero) + + self.activeStickOrder = self.defaultStickOrder + + self.camera.backgroundColor = .clear + self.camera.clipsToBounds = false + self.addSubview(self.camera) + + self.cubeContainer.backgroundColor = .clear + self.cubeContainer.clipsToBounds = false + self.camera.addSubview(self.cubeContainer) + + var p = CATransform3DIdentity + p.m34 = -1.0 / self.perspective + self.camera.layer.sublayerTransform = p + self.stickerContainer.layer.sublayerTransform = p + + self.stickerContainer.backgroundColor = .clear + self.stickerContainer.clipsToBounds = false + self.addSubview(self.stickerContainer) + +#if DEBUG + let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) + self.camera.addGestureRecognizer(pan) +#endif + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.camera.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize) + self.camera.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) + + self.cubeContainer.frame = self.camera.bounds + self.stickerContainer.frame = self.bounds + + self.layoutStickers() + self.layoutFaces() + self.applyCubeRotation() + } + + func setStickers(_ views: [UIView]) { + self.stickers = views + for view in views { + view.layer.anchorPoint = .zero + view.isUserInteractionEnabled = true + + if view.superview !== self.stickerContainer { + self.stickerContainer.addSubview(view) + } + } + self.layoutStickers() + } + + func setSticker(_ sticker: UIView?, face index: Int, mirror: Bool) { + guard self.faces.indices.contains(index) else { + return + } + + if let existing = self.faceOccupants[index] { + existing.removeFromSuperview() + self.faceOccupants[index] = nil + } + + guard let sticker else { + return + } + + if let priorIndex = self.faceOccupants.first(where: { $0.value === sticker })?.key { + self.faceOccupants[priorIndex] = nil + } + sticker.removeFromSuperview() + + let targetFace = self.faces[index] + targetFace.addSubview(sticker) + self.faceOccupants[index] = sticker + + sticker.layer.removeAllAnimations() + sticker.transform = .identity + sticker.layer.transform = CATransform3DIdentity + sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + sticker.layer.isDoubleSided = false + sticker.clipsToBounds = false + sticker.isUserInteractionEnabled = false + + let faceStickerSize = self.cubeSize + sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize) + sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2) + + var snappedAngle: CGFloat = 0.0 + if mirror { + snappedAngle += .pi + } + sticker.transform = CGAffineTransform(rotationAngle: snappedAngle) + } + + func startStickerSequence(indices: [Int]? = nil) { + guard !self.isRunning else { + return + } + guard self.stickers.contains(where: { $0.superview === self.stickerContainer }) else { + return + } + self.isRunning = true + + let sequence: [Int] + if let indices, !indices.isEmpty { + var seen = Set() + var result: [Int] = [] + for index in indices where self.stickers.indices.contains(index) { + if seen.insert(index).inserted { + result.append(index) + } + } + sequence = result + } else { + sequence = Array(self.stickers.indices) + } + + var stickOrder: [Int] + let key = sequence.map(String.init).joined(separator: ",") + if let order = self.sequenceStickOrders[key] { + stickOrder = order + } else { + stickOrder = Array(self.defaultStickOrder.prefix(sequence.count)) + } + self.activeStickOrder = stickOrder + + self.scheduleStickerSequence(from: 0, indices: sequence) + } + + func resetAll() { + self.isRunning = false + self.resetStickers() + self.resetCube() + self.activeStickOrder = self.defaultStickOrder + } + + func setFaces(_ views: [UIView]) { + guard views.count == 6 else { + return + } + self.faces.forEach { $0.removeFromSuperview() } + self.faces = views + for face in views { + face.layer.isDoubleSided = false + self.cubeContainer.addSubview(face) + } + self.layoutFaces() + } + + private func layoutFaces() { + guard self.faces.count == 6 else { + return + } + let half = self.cubeSize / 2 + + for face in self.faces { + face.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize) + face.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2) + } + + func faceTransform(rx: CGFloat, ry: CGFloat) -> CATransform3D { + var m = CATransform3DIdentity + m = CATransform3DRotate(m, rx, 1, 0, 0) + m = CATransform3DRotate(m, ry, 0, 1, 0) + m = CATransform3DTranslate(m, 0, 0, half) + return m + } + + self.faces[0].layer.transform = faceTransform(rx: 0, ry: 0) + self.faces[1].layer.transform = faceTransform(rx: 0, ry: .pi / 2) + self.faces[2].layer.transform = faceTransform(rx: 0, ry: .pi) + self.faces[3].layer.transform = faceTransform(rx: 0, ry: -.pi / 2) + self.faces[4].layer.transform = faceTransform(rx: -.pi / 2, ry: 0) + self.faces[5].layer.transform = faceTransform(rx: .pi / 2, ry: 0) + } + + private func animateWarp(for view: UIView, from startQuad: Quad, to targetQuad: Quad, duration: TimeInterval, dynamicTarget: (() -> Quad)? = nil, completion: @escaping () -> Void) { + self.cancelWarp() + self.warpView = view + self.warpStartQuad = startQuad + self.warpEndQuad = targetQuad + self.warpDuration = duration + self.warpDynamicTarget = dynamicTarget + self.warpCompletion = completion + self.warpStartTimestamp = 0 + self.warpLastProgress = 0 + self.warpHasCompleted = false + self.warpCurrentQuad = startQuad + startQuad.apply(to: view) + + let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in + self?.stepWarp() + } + link.isPaused = false + self.warpDisplayLink = link + } + + private func stepWarp() { + guard let view = self.warpView, let currentQuad = self.warpCurrentQuad, let endQuad = self.warpEndQuad else { + self.finishWarp() + return + } + + if self.warpStartTimestamp == 0 { + self.warpStartTimestamp = CACurrentMediaTime() + } + + let elapsed = CACurrentMediaTime() - self.warpStartTimestamp + let progress = self.warpDuration > 0 ? min(1.0, elapsed / self.warpDuration) : 1.0 + let t = CGFloat(progress) + let eased = t * t * (3 - 2 * t) + let target = self.warpDynamicTarget?() ?? endQuad + let delta = eased - self.warpLastProgress + let remaining = max(1 - self.warpLastProgress, 0.0001) + let weight = max(0, min(1, delta / remaining)) + let nextQuad = currentQuad.interpolated(to: target, t: weight) + nextQuad.apply(to: view) + self.warpCurrentQuad = nextQuad + self.warpLastProgress = eased + + if progress >= 1.0 { + self.finishWarp() + } + } + + private func cancelWarp() { + self.warpHasCompleted = true + self.warpDisplayLink?.invalidate() + self.warpDisplayLink = nil + self.warpCompletion = nil + self.clearWarpState() + } + + private func finishWarp() { + guard !self.warpHasCompleted else { return } + self.warpHasCompleted = true + self.warpDisplayLink?.invalidate() + self.warpDisplayLink = nil + self.warpCompletion?() + self.warpCompletion = nil + self.clearWarpState() + } + + private func clearWarpState() { + self.warpView = nil + self.warpStartQuad = nil + self.warpEndQuad = nil + self.warpDynamicTarget = nil + self.warpStartTimestamp = 0 + self.warpLastProgress = 0 + self.warpCurrentQuad = nil + } + + private func projectedQuad(for face: UIView) -> ProjectedFace { + let bounds = face.bounds + + func project(_ p: CGPoint) -> CGPoint { + let inRoot = face.layer.convert(p, to: self.layer) + return self.stickerContainer.layer.convert(inRoot, from: self.layer) + } + + var topLeft = project(CGPoint(x: bounds.minX, y: bounds.minY)) + var topRight = project(CGPoint(x: bounds.maxX, y: bounds.minY)) + var bottomLeft = project(CGPoint(x: bounds.minX, y: bounds.maxY)) + var bottomRight = project(CGPoint(x: bounds.maxX, y: bounds.maxY)) + + func center(_ a: CGPoint, _ b: CGPoint) -> CGPoint { + CGPoint(x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5) + } + + func normalized(_ v: CGPoint) -> CGPoint? { + let len = hypot(v.x, v.y) + guard len > 1e-5 else { return nil } + return CGPoint(x: v.x / len, y: v.y / len) + } + + func dot(_ a: CGPoint, _ b: CGPoint) -> CGFloat { + a.x * b.x + a.y * b.y + } + + let screenUp = CGPoint(x: 0, y: -1) + let screenRight = CGPoint(x: 1, y: 0) + + if let up = normalized(CGPoint( + x: center(topLeft, topRight).x - center(bottomLeft, bottomRight).x, + y: center(topLeft, topRight).y - center(bottomLeft, bottomRight).y + )), dot(up, screenUp) < 0 { + swap(&topLeft, &bottomLeft) + swap(&topRight, &bottomRight) + } + + let faceOrigin = project(.zero) + let faceX = project(CGPoint(x: 1, y: 0)) + + if let right = normalized(CGPoint( + x: center(topRight, bottomRight).x - center(topLeft, bottomLeft).x, + y: center(topRight, bottomRight).y - center(topLeft, bottomLeft).y + )), dot(right, screenRight) < 0 { + swap(&topLeft, &topRight) + swap(&bottomLeft, &bottomRight) + } + + let quad = Quad(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) + + let desiredTopVector = CGPoint(x: quad.topRight.x - quad.topLeft.x, y: quad.topRight.y - quad.topLeft.y) + let baseTopVector = CGPoint(x: faceX.x - faceOrigin.x, y: faceX.y - faceOrigin.y) + + let desiredAngle = atan2(desiredTopVector.y, desiredTopVector.x) + let baseAngle = atan2(baseTopVector.y, baseTopVector.x) + let rotation = normalizeAngle(desiredAngle - baseAngle) + + return ProjectedFace(quad: quad, rotation: rotation) + } + + private func layoutStickers() { + guard !self.stickers.isEmpty else { + return + } + + let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self) + let r = self.cubeSize / 2 + self.stickerGap + self.stickerSize / 2 + let scale = self.stickerSize / self.cubeSize + + let positions = [ + CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y - r * 0.4), + CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y - r * 0.4), + CGPoint(x: cubeCenterInSticker.x - r, y: cubeCenterInSticker.y + r * 0.4), + CGPoint(x: cubeCenterInSticker.x + r, y: cubeCenterInSticker.y + r * 0.4) + ] + + for (i, view) in self.stickers.enumerated() { + if view.superview !== self.stickerContainer { + continue + } + view.bounds = CGRect(x: 0, y: 0, width: self.cubeSize, height: self.cubeSize) + view.transform = CGAffineTransform(scaleX: scale, y: scale) + view.center = CGPoint(x: positions[i].x - self.stickerSize * 0.5, y: positions[i].y - self.stickerSize * 0.5) + } + } + + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + let translation = gesture.translation(in: self.camera) + switch gesture.state { + case .changed: + let delta = CGPoint(x: translation.x, y: translation.y) + + self.rotation.y += Float(delta.x) * 0.018 + self.rotation.x += Float(-delta.y) * 0.018 + self.rotation = normalizedRotation(self.rotation) + self.applyCubeRotation() + + gesture.setTranslation(.zero, in: self.camera) + default: + break + } + } + + func launchStickerView(_ sticker: UIView, emphasized: Bool, willFinish: Bool = false) { + guard sticker.superview === self.stickerContainer else { + return + } + var number = 0 + if self.faceOccupants.count < self.activeStickOrder.count { + number = self.activeStickOrder[self.faceOccupants.count] + } + let faceIndex = number + guard self.faces.count > faceIndex else { return } + let targetFace = self.faces[faceIndex] + + let startCenterInSticker = sticker.center + let cubeCenterInSticker = self.stickerContainer.convert(self.camera.center, from: self) + + sticker.isUserInteractionEnabled = false + sticker.layer.isDoubleSided = false + + let faceStickerSize = self.cubeSize + let duration: TimeInterval = 0.2 + let startQuad = Quad(rect: sticker.frame) + let animationView: UIView + if let snapshot = sticker.snapshotView(afterScreenUpdates: true) { + self.warpSnapshot?.removeFromSuperview() + self.warpSnapshot = snapshot + + snapshot.bounds = sticker.bounds + snapshot.center = sticker.center + snapshot.layer.anchorPoint = sticker.layer.anchorPoint + snapshot.layer.transform = sticker.layer.transform + snapshot.layer.isDoubleSided = sticker.layer.isDoubleSided + snapshot.isUserInteractionEnabled = false + self.stickerContainer.addSubview(snapshot) + + sticker.isHidden = true + animationView = snapshot + } else { + animationView = sticker + } + sticker.transform = .identity + + let projectedFace = self.projectedQuad(for: targetFace) + let targetQuad = projectedFace.quad + let dynamicTarget: () -> Quad = { [weak self, weak targetFace] in + guard let self, let face = targetFace else { + return targetQuad + } + return self.projectedQuad(for: face).quad + } + + self.animateWarp(for: animationView, from: startQuad, to: targetQuad, duration: duration, dynamicTarget: dynamicTarget) { [weak self, weak sticker, weak targetFace, weak animationView] in + guard let self, let sticker, let targetFace else { + return + } + + if let animationView, animationView !== sticker { + animationView.removeFromSuperview() + self.warpSnapshot = nil + sticker.isHidden = false + } + + sticker.removeFromSuperview() + targetFace.addSubview(sticker) + self.faceOccupants[faceIndex] = sticker + + sticker.bounds = CGRect(x: 0, y: 0, width: faceStickerSize, height: faceStickerSize) + sticker.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + sticker.center = CGPoint(x: self.cubeSize / 2, y: self.cubeSize / 2) + sticker.layer.transform = CATransform3DIdentity + let finalProjection = self.projectedQuad(for: targetFace) + let snappedAngle = snappedRightAngle(finalProjection.rotation) + sticker.transform = CGAffineTransform(rotationAngle: snappedAngle) + + let delta = SIMD2(Float(cubeCenterInSticker.x - startCenterInSticker.x), Float(cubeCenterInSticker.y - startCenterInSticker.y)) + let direction = normalize2(delta) + self.applyImpulse(direction: direction, emphasized: emphasized, replace: true) + self.applyImpactSpring(direction: direction, emphasized: emphasized) + if willFinish { + self.startFinishingAnimation() + } + self.startSpinLoopIfNeeded() + } + } + + private func resetStickers() { + self.cancelWarp() + self.warpSnapshot?.removeFromSuperview() + self.warpSnapshot = nil + + for sticker in self.stickers { + sticker.layer.removeAllAnimations() + sticker.transform = .identity + sticker.layer.transform = CATransform3DIdentity + sticker.layer.anchorPoint = .zero + sticker.layer.isDoubleSided = true + sticker.clipsToBounds = false + sticker.isUserInteractionEnabled = true + sticker.removeFromSuperview() + self.stickerContainer.addSubview(sticker) + } + + self.faceOccupants.removeAll() + self.layoutStickers() + } + + private func resetCube() { + self.displayLink?.invalidate() + self.displayLink = nil + self.angularVelocity = .zero + self.lastTimestamp = 0 + self.isFinishingX = false + self.isFinishingY = false + self.finishDelayTimerX?.invalidate() + self.finishDelayTimerX = nil + self.finishDelayTimerY?.invalidate() + self.finishDelayTimerY = nil + self.cubeScale = 1.0 + self.hasFiredFinishApproach = false + + self.rotation = SIMD3(repeating: 0) + self.cubeScale = 1.0 + self.applyCubeRotation() + } + + private func scheduleStickerSequence(from index: Int, indices: [Int]) { + guard self.isRunning else { + return + } + guard index < indices.count else { + self.isRunning = false + return + } + + let delay: TimeInterval = index == 0 ? 0.0 : 1.0 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self else { + return + } + guard self.isRunning else { + return + } + let stickerIndex = indices[index] + if self.stickers.indices.contains(stickerIndex) { + let isLast = stickerIndex == indices.count - 1 + self.launchStickerView(self.stickers[stickerIndex], emphasized: isLast, willFinish: isLast) + } + self.scheduleStickerSequence(from: index + 1, indices: indices) + } + } + + private func applyImpulse(direction: SIMD2, emphasized: Bool, replace: Bool) { + var xStrength = self.baseImpulseStrength + var yStrength = self.baseImpulseStrength + if emphasized { + xStrength *= 10.0 + yStrength *= 4.0 + } + let impulseX: Float = -direction.y * xStrength + let impulseY: Float = direction.x * yStrength + let impulseZ: Float = 0.0 + + if replace { + self.angularVelocity = SIMD3(impulseX, impulseY, impulseZ) + } else { + self.angularVelocity += SIMD3(impulseX, impulseY, impulseZ) + } + } + + private func applyImpactSpring(direction: SIMD2, emphasized: Bool) { + guard simd_length(direction) > 0.0001 else { + return + } + let distance = emphasized ? self.impactNudgeEmphasis : self.impactNudgeDistance + let offsetX = CGFloat(direction.x) * distance + let offsetY = CGFloat(direction.y) * distance + + let currentTransform = self.camera.layer.presentation()?.affineTransform() ?? self.camera.transform + self.camera.layer.removeAllAnimations() + let impactTransform = currentTransform.translatedBy(x: offsetX, y: offsetY) + + UIView.animate(withDuration: 0.08, delay: 0.0, options: [.curveEaseOut, .beginFromCurrentState]) { + self.camera.transform = impactTransform + } completion: { _ in + UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 0.72, initialSpringVelocity: 0.2, options: .beginFromCurrentState) { + self.camera.transform = .identity + } + } + } + + private func startSpinLoopIfNeeded() { + if self.displayLink == nil { + let link = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max) { [weak self] _ in + self?.tick() + } + link.isPaused = false + self.displayLink = link + self.lastTimestamp = 0.0 + } + } + + private func tick() { + let ts = CACurrentMediaTime() + if self.lastTimestamp == 0 { self.lastTimestamp = ts; return } + let dt = Float(ts - self.lastTimestamp) + self.lastTimestamp = ts + + self.rotation += self.angularVelocity * dt + if self.isFinishingX { + let delta = shortestAngleDelta(from: self.rotation.x, to: self.finishTargetX) + let accel = self.finishSpringX * delta - self.finishDampingX * self.angularVelocity.x + self.angularVelocity.x += accel * dt + if abs(delta) < 0.0006 && abs(self.angularVelocity.x) < 0.001 { + self.rotation.x = self.finishTargetX + self.angularVelocity.x = 0.0 + self.isFinishingX = false + } + } + if self.isFinishingY { + self.finishRotationY += self.angularVelocity.y * dt + let remaining = self.finishTargetYUnwrapped - self.finishRotationY + let accel = self.finishSpringY * remaining - self.finishDampingY * self.angularVelocity.y + self.angularVelocity.y += accel * dt + self.rotation.y = normalizeAngle(self.finishRotationY) + let total = max(abs(self.finishRemainingYStart), 0.0001) + let progress = min(max(1.0 - abs(remaining) / total, 0.0), 1.0) + let damping = pow(1.0 - progress, self.finishWobbleDampingExponent) + let phase = 2.0 * Float.pi * self.finishWobbleCycles * progress + self.rotation.z = self.finishWobbleAmplitudeZ * sin(phase) * damping + let absRemaining = abs(remaining) + if !self.hasFiredFinishApproach && absRemaining <= self.finishApproachTriggerAngle { + self.hasFiredFinishApproach = true + let upsideDown = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) < (Float.pi / 2) + self.onFinishApproach?(upsideDown) + } + if absRemaining <= self.finishSuccessScaleTriggerAngle { + let raw = (self.finishSuccessScaleTriggerAngle - absRemaining) / self.finishSuccessScaleTriggerAngle + let eased = raw * raw * (3 - 2 * raw) + self.cubeScale = 1.0 + (self.finishSuccessScale - 1.0) * eased + } + if abs(remaining) < 0.0008 && abs(self.angularVelocity.y) < 0.0015 { + self.finishRotationY = self.finishTargetYUnwrapped + self.rotation.y = self.finishTargetY + self.angularVelocity.y = 0.0 + self.isFinishingY = false + self.rotation.z = 0.0 + self.angularVelocity.z = 0.0 + } + } else if self.rotation.z != 0 { + self.rotation.z = 0.0 + } + self.rotation = normalizedRotation(self.rotation) + + let damp = pow(self.dampingPerSecond, dt) + self.angularVelocity *= damp + + self.applyCubeRotation() + } + + private func startFinishingAnimation() { + self.finishDelayTimerX?.invalidate() + self.finishDelayTimerX = Timer.scheduledTimer(withTimeInterval: 0.75, repeats: false) { [weak self] _ in + self?.beginFinishingX() + } + } + + private func beginFinishingX() { + let deltaToZero = abs(shortestAngleDelta(from: self.rotation.x, to: 0)) + let deltaToPi = abs(shortestAngleDelta(from: self.rotation.x, to: Float.pi)) + self.finishTargetX = deltaToZero <= deltaToPi ? 0 : Float.pi + self.finishTargetY = self.finishTargetX == 0 ? 0 : Float.pi + self.isFinishingX = true + self.finishDelayTimerY?.invalidate() + self.finishDelayTimerY = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in + self?.beginFinishingY() + } + } + + private func beginFinishingY() { + self.finishRotationY = self.rotation.y + let directionY = nonZeroSign(self.angularVelocity.y, fallback: 1) + self.finishDirectionY = directionY + let startMod = normalizeAnglePositive(self.finishRotationY) + let targetMod = normalizeAnglePositive(self.finishTargetY) + let baseDelta: Float + if directionY >= 0 { + baseDelta = targetMod >= startMod ? targetMod - startMod : (Float.pi * 2) - (startMod - targetMod) + } else { + baseDelta = startMod >= targetMod ? startMod - targetMod : (Float.pi * 2) - (targetMod - startMod) + } + var delta = baseDelta + if delta < Float.pi { + delta += Float.pi * 2 + } + self.finishTargetYUnwrapped = self.finishRotationY + directionY * delta + self.finishRemainingYStart = self.finishTargetYUnwrapped - self.finishRotationY + self.isFinishingY = true + self.hasFiredFinishApproach = false + } + + private func applyCubeRotation() { + var m = CATransform3DIdentity + m = CATransform3DRotate(m, CGFloat(self.rotation.x), 1, 0, 0) + m = CATransform3DRotate(m, CGFloat(self.rotation.y), 0, 1, 0) + m = CATransform3DRotate(m, CGFloat(self.rotation.z), 0, 0, 1) + m = CATransform3DScale(m, CGFloat(self.cubeScale), CGFloat(self.cubeScale), 1) + self.cubeContainer.layer.transform = m + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeUtils.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeUtils.swift new file mode 100644 index 0000000000..af8ff126f0 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CubeUtils.swift @@ -0,0 +1,193 @@ +import UIKit +import simd + +func normalize2(_ v: SIMD2) -> SIMD2 { + let l = simd_length(v) + return l > 1e-5 ? v / l : SIMD2(0, 0) +} + +func normalizedRotation(_ r: SIMD3) -> SIMD3 { + SIMD3(normalizeAngle(r.x), normalizeAngle(r.y), normalizeAngle(r.z)) +} + +struct ProjectedFace { + let quad: Quad + let rotation: CGFloat +} + +struct Quad { + var topLeft: CGPoint + var topRight: CGPoint + var bottomLeft: CGPoint + var bottomRight: CGPoint + + init(topLeft: CGPoint, topRight: CGPoint, bottomLeft: CGPoint, bottomRight: CGPoint) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + + init(rect: CGRect) { + self.init( + topLeft: rect.origin, + topRight: CGPoint(x: rect.maxX, y: rect.minY), + bottomLeft: CGPoint(x: rect.minX, y: rect.maxY), + bottomRight: CGPoint(x: rect.maxX, y: rect.maxY) + ) + } + + func boundingBox() -> CGRect { + let xs = [topLeft.x, topRight.x, bottomLeft.x, bottomRight.x] + let ys = [topLeft.y, topRight.y, bottomLeft.y, bottomRight.y] + guard let minX = xs.min(), let maxX = xs.max(), let minY = ys.min(), let maxY = ys.max() else { + return .zero + } + return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) + } + + func offsetting(dx: CGFloat, dy: CGFloat) -> Quad { + return Quad( + topLeft: CGPoint(x: topLeft.x + dx, y: topLeft.y + dy), + topRight: CGPoint(x: topRight.x + dx, y: topRight.y + dy), + bottomLeft: CGPoint(x: bottomLeft.x + dx, y: bottomLeft.y + dy), + bottomRight: CGPoint(x: bottomRight.x + dx, y: bottomRight.y + dy) + ) + } + + func interpolated(to other: Quad, t: CGFloat) -> Quad { + return Quad( + topLeft: lerp(topLeft, other.topLeft, t), + topRight: lerp(topRight, other.topRight, t), + bottomLeft: lerp(bottomLeft, other.bottomLeft, t), + bottomRight: lerp(bottomRight, other.bottomRight, t) + ) + } + + func apply(to view: UIView) { + let bounds = boundingBox() + let localQuad = offsetting(dx: -bounds.origin.x, dy: -bounds.origin.y) + + CATransaction.begin() + CATransaction.setDisableActions(true) + view.frame = bounds + let transform = rectToQuad(rect: view.bounds, quad: localQuad) + view.layer.transform = transform + CATransaction.commit() + } +} + +func lerp(_ a: CGFloat, _ b: CGFloat, _ t: CGFloat) -> CGFloat { + return a + (b - a) * t +} + +func lerp(_ a: CGPoint, _ b: CGPoint, _ t: CGFloat) -> CGPoint { + return CGPoint(x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t)) +} + +func normalizeAngle(_ angle: CGFloat) -> CGFloat { + var result = angle + let twoPi = CGFloat.pi * 2 + while result > CGFloat.pi { + result -= twoPi + } + while result <= -CGFloat.pi { + result += twoPi + } + return result +} + +func normalizeAngle(_ angle: Float) -> Float { + var result = angle + let twoPi = Float.pi * 2 + while result > Float.pi { + result -= twoPi + } + while result <= -Float.pi { + result += twoPi + } + return result +} + +func normalizeAnglePositive(_ angle: Float) -> Float { + var result = angle + let twoPi = Float.pi * 2 + while result < 0 { result += twoPi } + while result >= twoPi { result -= twoPi } + return result +} + +func shortestAngleDelta(from: Float, to: Float) -> Float { + return normalizeAngle(to - from) +} + +func nonZeroSign(_ value: Float, fallback: Float) -> Float { + if value > 0 { return 1 } + if value < 0 { return -1 } + return fallback +} + +func snappedRightAngle(_ angle: CGFloat) -> CGFloat { + let quarter = CGFloat.pi / 2 + let normalized = normalizeAngle(angle) + let step = round(normalized / quarter) + return step * quarter +} + +func rectToQuad(rect: CGRect, quad: Quad) -> CATransform3D { + let x1a = quad.topLeft.x + let y1a = quad.topLeft.y + let x2a = quad.topRight.x + let y2a = quad.topRight.y + let x3a = quad.bottomLeft.x + let y3a = quad.bottomLeft.y + let x4a = quad.bottomRight.x + let y4a = quad.bottomRight.y + + let X = rect.origin.x + let Y = rect.origin.y + let W = rect.size.width + let H = rect.size.height + + let y21 = y2a - y1a + let y32 = y3a - y2a + let y43 = y4a - y3a + let y14 = y1a - y4a + let y31 = y3a - y1a + let y42 = y4a - y2a + + let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) + let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) + - H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43) + - W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43) + + let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a) + let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42) + let f = -( + W * (x4a * (Y * y2a * y31 + H * y1a * y32) + - x3a * (H + Y) * y1a * y42 + + H * x2a * y1a * y43 + + x2a * Y * (y1a - y3a) * y4a + + x1a * Y * y3a * (-y2a + y4a)) + - H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a)) + ) + + let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43) + let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42) + var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42) + + H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43) + + W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a)) + + let epsilon: CGFloat = 0.0001 + if abs(i) < epsilon { + i = i >= 0 ? epsilon : -epsilon + } + + return CATransform3D( + m11: a / i, m12: d / i, m13: 0, m14: g / i, + m21: b / i, m22: e / i, m23: 0, m24: h / i, + m31: 0, m32: 0, m33: 1, m34: 0, + m41: c / i, m42: f / i, m43: 0, m44: 1 + ) +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift new file mode 100644 index 0000000000..accfe3fb76 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/DialIndicatorComponent.swift @@ -0,0 +1,197 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import MultilineTextComponent +import AnimatedTextComponent + +final class DialIndicatorComponent: Component { + let content: AnyComponentWithIdentity + let backgroundColor: UIColor + let foregroundColor: UIColor + let diameter: CGFloat + let lineWidth: CGFloat + let fontSize: CGFloat + let percentage: Int + let isVisible: Bool + + public init( + content: AnyComponentWithIdentity, + backgroundColor: UIColor, + foregroundColor: UIColor, + diameter: CGFloat, + lineWidth: CGFloat, + fontSize: CGFloat, + percentage: Int, + isVisible: Bool = true + ) { + self.content = content + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.diameter = diameter + self.lineWidth = lineWidth + self.fontSize = fontSize + self.percentage = percentage + self.isVisible = isVisible + } + + public static func ==(lhs: DialIndicatorComponent, rhs: DialIndicatorComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.foregroundColor != rhs.foregroundColor { + return false + } + if lhs.diameter != rhs.diameter { + return false + } + if lhs.lineWidth != rhs.lineWidth { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.percentage != rhs.percentage { + return false + } + if lhs.isVisible != rhs.isVisible { + return false + } + return true + } + + public final class View: UIView { + private let containerView = UIView() + private let backgroundLayer = SimpleShapeLayer() + private let foregroundLayer = SimpleShapeLayer() + + private var content = ComponentView() + private let label = ComponentView() + + private var component: DialIndicatorComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.backgroundLayer.lineCap = .round + self.foregroundLayer.lineCap = .round + + self.addSubview(self.containerView) + + self.containerView.layer.addSublayer(self.backgroundLayer) + self.containerView.layer.addSublayer(self.foregroundLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DialIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let previousComponent = self.component + self.component = component + self.state = state + + let pathSize = CGSize(width: component.diameter, height: component.diameter) + let pathFrame = CGRect(origin: .zero, size: pathSize).insetBy(dx: component.lineWidth * 0.5, dy: component.lineWidth * 0.5) + + let strokeStart: CGFloat = 0.125 + let strokeEnd: CGFloat = 1.0 - strokeStart + + self.backgroundLayer.lineWidth = component.lineWidth + self.backgroundLayer.strokeColor = component.backgroundColor.cgColor + self.backgroundLayer.fillColor = UIColor.clear.cgColor + self.backgroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil) + self.backgroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) + self.backgroundLayer.strokeStart = strokeStart + self.backgroundLayer.strokeEnd = strokeEnd + self.backgroundLayer.frame = CGRect(origin: .zero, size: pathSize) + + self.foregroundLayer.lineWidth = component.lineWidth + self.foregroundLayer.strokeColor = component.foregroundColor.cgColor + self.foregroundLayer.fillColor = UIColor.clear.cgColor + self.foregroundLayer.path = CGPath(ellipseIn: pathFrame, transform: nil) + self.foregroundLayer.transform = CATransform3DMakeRotation(.pi / 2.0, 0.0, 0.0, 1.0) + self.foregroundLayer.strokeStart = strokeStart + transition.setShapeLayerStrokeEnd(layer: self.foregroundLayer, strokeEnd: strokeStart + (strokeEnd - strokeStart) * (CGFloat(component.percentage) / 100.0)) + self.foregroundLayer.frame = CGRect(origin: .zero, size: pathSize) + + if previousComponent?.content.id != component.content.id { + if let contentView = self.content.view { + if transition.animation.isImmediate { + contentView.removeFromSuperview() + } else { + transition.setScale(view: contentView, scale: 0.01) + transition.setAlpha(view: contentView, alpha: 0.0, completion: { _ in + contentView.removeFromSuperview() + }) + } + } + self.content = ComponentView() + } + + let contentFrame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: CGSize(width: component.diameter - 16.0, height: component.diameter - 16.0)) + let _ = self.content.update( + transition: .immediate, + component: component.content.component, + environment: {}, + containerSize: contentFrame.size + ) + if let contentView = self.content.view { + if contentView.superview == nil { + self.containerView.addSubview(contentView) + if !transition.animation.isImmediate { + transition.animateScale(view: contentView, from: 0.01, to: 1.0) + transition.animateAlpha(view: contentView, from: 0.0, to: 1.0) + } + } + contentView.frame = contentFrame + } + + let labelItems: [AnimatedTextComponent.Item] = [ + AnimatedTextComponent.Item(id: "percent", content: .number(component.percentage, minDigits: 1)), + AnimatedTextComponent.Item(id: "suffix", content: .text("%")) + ] + + let labelSize = self.label.update( + transition: transition, + component: AnyComponent( + AnimatedTextComponent( + font: Font.semibold(component.fontSize), + color: component.foregroundColor, + items: labelItems + ) + ), + environment: {}, + containerSize: availableSize + ) + if let labelView = self.label.view { + if labelView.superview == nil { + self.containerView.addSubview(labelView) + } + transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((pathSize.width - labelSize.width) / 2.0) + 1.0 - UIScreenPixel, y: pathSize.height - labelSize.height + 2.0 - UIScreenPixel), size: labelSize)) + } + + transition.setAlpha(view: self.containerView, alpha: component.isVisible ? 1.0 : 0.0) + transition.setBlur(layer: self.containerView.layer, radius: component.isVisible ? 0.0 : 10.0) + + self.containerView.frame = CGRect(origin: .zero, size: pathSize) + + return pathSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift new file mode 100644 index 0000000000..1a34ed0c24 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -0,0 +1,1661 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import AppBundle +import LocalMediaResources +import TelegramPresentationData +import TelegramStringFormatting +import ViewControllerComponent +import BundleIconComponent +import BalancedTextComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import ButtonComponent +import PlainButtonComponent +import GiftItemComponent +import GiftAnimationComponent +import AccountContext +import GlassBarButtonComponent +import ResizableSheetComponent +import AnimatedTextComponent +import Markdown +import InfoParagraphComponent +import PresentationDataUtils +import GiftViewScreen +import PeerInfoCoverComponent +import LottieComponent +import TooltipUI +import TextFormat +import GlassBackgroundComponent +import SpaceWarpView +import ConfettiEffect +import TelegramNotices + +private let backdropButtonTag = GenericComponentViewTag() +private let symbolButtonTag = GenericComponentViewTag() + +private final class CraftGiftPageContent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + class ExternalState { + fileprivate(set) var giftMap: [Int64: GiftItem] + + fileprivate(set) var testFailOrSuccess: Bool? + + public init() { + self.giftMap = [:] + } + } + + let context: AccountContext + let craftContext: CraftGiftsContext + let resaleContext: () -> ResaleGiftsContext? + let colors: (UIColor, UIColor, UIColor, UIColor, UIColor) + let gift: StarGift.UniqueGift + let selectedGiftIds: [Int32: Int64] + let displayCraftInfo: Bool + let isCrafting: Bool + let inProgress: Bool + let result: CraftTableComponent.Result? + let screenSize: CGSize + let externalState: ExternalState + let selectGift: (Int32, GiftItem) -> Void + let removeGift: (Int32) -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + craftContext: CraftGiftsContext, + resaleContext: @escaping () -> ResaleGiftsContext?, + colors: (UIColor, UIColor, UIColor, UIColor, UIColor), + gift: StarGift.UniqueGift, + selectedGiftIds: [Int32: Int64], + displayCraftInfo: Bool, + isCrafting: Bool, + inProgress: Bool, + result: CraftTableComponent.Result?, + screenSize: CGSize, + externalState: ExternalState, + selectGift: @escaping (Int32, GiftItem) -> Void, + removeGift: @escaping (Int32) -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.craftContext = craftContext + self.resaleContext = resaleContext + self.colors = colors + self.gift = gift + self.selectedGiftIds = selectedGiftIds + self.displayCraftInfo = displayCraftInfo + self.isCrafting = isCrafting + self.inProgress = inProgress + self.result = result + self.screenSize = screenSize + self.externalState = externalState + self.selectGift = selectGift + self.removeGift = removeGift + self.dismiss = dismiss + } + + static func ==(lhs: CraftGiftPageContent, rhs: CraftGiftPageContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + if lhs.colors.0 != rhs.colors.0 || lhs.colors.1 != rhs.colors.1 || lhs.colors.2 != rhs.colors.2 || lhs.colors.3 != rhs.colors.3 { + return false + } + if lhs.selectedGiftIds != rhs.selectedGiftIds { + return false + } + if lhs.displayCraftInfo != rhs.displayCraftInfo { + return false + } + if lhs.isCrafting != rhs.isCrafting { + return false + } + if lhs.inProgress != rhs.inProgress { + return false + } + if lhs.screenSize != rhs.screenSize { + return false + } + return true + } + + final class View: UIView, UIScrollViewDelegate { + private let tableContainer = UIView() + private let background = SimpleGradientLayer() + private let overlay = SimpleGradientLayer() + private let pattern = ComponentView() + + private let title = ComponentView() + private let descriptionText = ComponentView() + + private let craftingTitle = ComponentView() + private let craftingSubtitle = ComponentView() + private let craftingDescription = ComponentView() + private let craftingProbability = ComponentView() + private var craftingProbabilityMeasure = ComponentView() + + private var backdropDial = ComponentView() + private var symbolDial = ComponentView() + private var variantsButton = ComponentView() + private var variantsButtonMeasure = ComponentView() + + private var craftTable = ComponentView() + private var selectedGifts: [AnyHashable: ComponentView] = [:] + + private let infoContainer = UIView() + private var infoBackground = SimpleLayer() + private var infoHeader = ComponentView() + private let infoTitle = ComponentView() + private let infoDescription = ComponentView() + private var infoList = ComponentView() + + private var actionButton = ComponentView() + + private var craftState: CraftGiftsContext.State? + private var craftStateDisposable: Disposable? + + private let upgradePreviewDisposable = DisposableSet() + private var upgradePreview: [StarGift.UniqueGift.Attribute]? + private var starGiftsMap: [Int64: StarGift.Gift] = [:] + + private let starsTopUpOptionsPromise = Promise<[StarsTopUpOption]?>(nil) + + private var availableGifts: [GiftItem] = [] + private var giftMap: [Int64: GiftItem] = [:] + private var isCrafting = false + private var isFailing = false + + private var component: CraftGiftPageContent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.background.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.background.cornerRadius = 40.0 + self.background.type = .axial + self.background.startPoint = CGPoint(x: 0.5, y: 0.0) + self.background.endPoint = CGPoint(x: 0.5, y: 1.0) + self.layer.addSublayer(self.background) + + self.overlay.type = .radial + self.overlay.startPoint = CGPoint(x: 0.5, y: 0.5) + self.overlay.endPoint = CGPoint(x: 0.0, y: 1.0) + self.layer.addSublayer(self.overlay) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.craftStateDisposable?.dispose() + self.upgradePreviewDisposable.dispose() + } + + func showAttributeInfo(tag: Any, text: String) { + guard let component = self.component, let controller = self.environment?.controller() as? GiftCraftScreen else { + return + } + controller.dismissAllTooltips() + + guard let sourceView = controller.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x + 1.0, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: component.context.account, sharedContext: component.context.sharedContext, text: .markdown(text: text), balancedTextLayout: true, style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + controller.present(tooltipController, in: .current) + } + + func openUpgradeVariants() { + guard let component = self.component, let controller = self.environment?.controller(), let gift = self.starGiftsMap[component.gift.giftId] else { + return + } + + let _ = (component.context.engine.payments.getStarGiftUpgradeAttributes(giftId: component.gift.giftId) + |> take(1) + |> deliverOnMainQueue).start(next: { [weak controller] attributes in + guard let attributes else { + return + } + let variantsController = component.context.sharedContext.makeGiftUpgradeVariantsScreen( + context: component.context, + gift: .generic(gift), + onlyCrafted: true, + attributes: attributes, + selectedAttributes: nil, + focusedAttribute: nil + ) + controller?.push(variantsController) + }) + } + + func update(component: CraftGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + let initialGiftItem = GiftItem( + gift: component.gift, + reference: .slug(slug: component.gift.slug) + ) + self.availableGifts = [ + initialGiftItem + ] + self.giftMap = [initialGiftItem.gift.id: initialGiftItem] + component.externalState.giftMap = self.giftMap + + self.craftStateDisposable = (component.craftContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + //let isFirstTime = self.craftState == nil + self.craftState = state + + var items: [GiftItem] = [] + var map: [Int64: GiftItem] = self.giftMap + var foundInitial = false + for gift in state.gifts { + guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift else { + continue + } + let giftItem = GiftItem( + gift: uniqueGift, + reference: reference + ) + if uniqueGift.id == component.gift.id { + items.insert(giftItem, at: 0) + foundInitial = true + } else { + items.append(giftItem) + } + map[uniqueGift.id] = giftItem + } + + if !foundInitial { + items.insert(initialGiftItem, at: 0) + map[initialGiftItem.gift.id] = initialGiftItem + } + self.availableGifts = items + self.giftMap = map + self.component?.externalState.giftMap = self.giftMap + + self.state?.updated(transition: .spring(duration: 0.4)) + }) + + self.upgradePreviewDisposable.add((component.context.engine.payments.getStarGiftUpgradeAttributes(giftId: initialGiftItem.gift.giftId) + |> deliverOnMainQueue).start(next: { [weak self] attributes in + guard let self, let attributes else { + return + } + var filteredAttributes: [StarGift.UniqueGift.Attribute] = [] + for attribute in attributes { + if case let .model(_, file, _, crafted) = attribute { + if crafted { + filteredAttributes.append(attribute) + self.upgradePreviewDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + } + } + } + self.upgradePreview = filteredAttributes + + self.state?.updated() + })) + + self.upgradePreviewDisposable.add((.single(nil) |> then(component.context.engine.payments.cachedStarGifts()) + |> deliverOnMainQueue).start(next: { [weak self] starGifts in + guard let self, let starGifts else { + return + } + var starGiftsMap: [Int64: StarGift.Gift] = [:] + for gift in starGifts { + if case let .generic(gift) = gift { + starGiftsMap[gift.id] = gift + } + } + self.starGiftsMap = starGiftsMap + })) + + self.starsTopUpOptionsPromise.set(component.context.engine.payments.starsTopUpOptions() |> map(Optional.init)) + } + + transition.setGradientColors(layer: self.background, colors: [component.colors.0, component.colors.1]) + transition.setGradientColors(layer: self.overlay, colors: [component.colors.2, component.colors.2.withAlphaComponent(0.0)]) + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + self.component = component + self.state = state + self.environment = environment + + var selectedGifts: [Int32: GiftItem] = [:] + for (index, giftId) in component.selectedGiftIds { + if let gift = self.giftMap[giftId] { + selectedGifts[index] = gift + } + } + + var craftContentHeight: CGFloat = 0.0 + var infoContentHeight: CGFloat = 0.0 + + let anvilPath = getAppBundle().url(forResource: "Anvil", withExtension: "tgs")?.path ?? "" + let anvilFile = TelegramMediaFile( + fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: -123456789), + partialReference: nil, + resource: BundleResource(name: "Anvil", path: anvilPath), + previewRepresentations: [], + videoThumbnails: [], + immediateThumbnailData: nil, + mimeType: "application/x-tgsticker", + size: nil, + attributes: [ + .FileName(fileName: "sticker.tgs"), + .CustomEmoji(isPremium: false, isSingleColor: true, alt: "", packReference: .animatedEmojiAnimations) + ], + alternativeRepresentations: [] + ) + + var backgroundTransition = transition + let backgroundSize = self.pattern.update( + transition: backgroundTransition, + component: AnyComponent(PeerInfoCoverComponent( + context: component.context, + subject: .custom(.clear, .clear, UIColor(rgb: 0x000000), anvilFile.fileId.id), + files: [anvilFile.fileId.id: anvilFile], + isDark: false, + avatarCenter: CGPoint(x: availableSize.width / 2.0, y: 169.0), + avatarSize: CGSize(width: 130.0, height: 130.0), + avatarScale: 1.0, + defaultHeight: 300.0, + gradientOnTop: true, + avatarTransitionFraction: self.isFailing ? 1.0 : 0.0, + patternTransitionFraction: 0.0, + patternIconScale: 1.5 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 169.0 * 2.0) + ) + let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - backgroundSize.height) / 2.0) : 0.0), size: backgroundSize) + if let backgroundView = self.pattern.view { + if backgroundView.layer.superlayer == nil { + backgroundTransition = .immediate + backgroundView.clipsToBounds = true + backgroundView.isUserInteractionEnabled = false + self.layer.insertSublayer(backgroundView.layer, above: self.overlay) + } + backgroundTransition.setFrame(view: backgroundView, frame: backgroundFrame) + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.semibold(17.0), textColor: .white))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: 16.0 + 22.0 - titleSize.height * 0.5), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + transition.setAlpha(view: titleView, alpha: component.isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: titleView.layer, radius: component.isCrafting ? 10.0 : 0.0) + } + + let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" + + //TODO:localize + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionColor = UIColor.white + let rawDescriptionString = "Add up to **4 gifts** to craft new\n**$ \(giftTitle)**.\n\nIf crafting fails, all selected gifts\nwill be consumed." + let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString + + if let gift = self.starGiftsMap[component.gift.giftId] { + let range = (descriptionString.string as NSString).range(of: "$") + if range.location != NSNotFound { + descriptionString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: gift.file.fileId.id, file: gift.file, custom: nil, enableAnimation: false), range: range) + } + } + + let descriptionTextSize = self.descriptionText.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white.withAlphaComponent(0.3), + text: .plain(descriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: { + }, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + craftContentHeight += 276.0 + craftContentHeight += descriptionTextSize.height + craftContentHeight += 25.0 + + var attributes: [ResaleGiftsContext.Attribute: StarGift.UniqueGift.Attribute] = [:] + var backdropAttributeCount: [ResaleGiftsContext.Attribute: Int32] = [:] + var patternAttributeCount: [ResaleGiftsContext.Attribute: Int32] = [:] + for gift in selectedGifts.values { + for attribute in gift.gift.attributes { + switch attribute { + case let .backdrop(_, id, _, _, _, _, _): + let attributeId: ResaleGiftsContext.Attribute = .backdrop(id) + attributes[attributeId] = attribute + if let count = backdropAttributeCount[attributeId] { + backdropAttributeCount[attributeId] = count + 1 + } else { + backdropAttributeCount[attributeId] = 1 + } + case let .pattern(_, file, _): + let attributeId: ResaleGiftsContext.Attribute = .pattern(file.fileId.id) + attributes[attributeId] = attribute + if let count = patternAttributeCount[attributeId] { + patternAttributeCount[attributeId] = count + 1 + } else { + patternAttributeCount[attributeId] = 1 + } + default: + break + } + } + } + + func mostFrequentAttribute(from counts: [ResaleGiftsContext.Attribute: Int32]) -> (attribute: StarGift.UniqueGift.Attribute, count: Int32)? { + guard let (id, count) = counts.max(by: { $0.value < $1.value }), + count > 1, + let attribute = attributes[id] else { + return nil + } + return (attribute, count) + } + + var possibleBackdrop: (StarGift.UniqueGift.Attribute, Int32)? + var possiblePattern: (StarGift.UniqueGift.Attribute, Int32)? + + var backdropColor: UIColor = .white + var backdropPermille: Int = 0 + var backdropName = "" + var symbolFile: TelegramMediaFile? + var symbolPermille: Int = 0 + var symbolName = "" + + for attribute in component.gift.attributes { + switch attribute { + case .backdrop: + possibleBackdrop = (attribute, 1) + case .pattern: + possiblePattern = (attribute, 1) + default: + break + } + } + + if let betterBackdrop = mostFrequentAttribute(from: backdropAttributeCount) { + possibleBackdrop = betterBackdrop + } + + if let betterPattern = mostFrequentAttribute(from: patternAttributeCount) { + possiblePattern = betterPattern + } + + let appConfiguration = component.context.currentAppConfiguration.with { $0 } + let giftCraftConfiguration = GiftCraftConfiguration.with(appConfiguration: appConfiguration) + + if case let .backdrop(name, _, innerColor, _, _, _, _) = possibleBackdrop?.0 { + backdropColor = UIColor(rgb: UInt32(bitPattern: innerColor)) + backdropName = name + } + if let possibleBackdrop { + backdropPermille = Int(giftCraftConfiguration.craftAttributePermilles[Int(possibleBackdrop.1 - 1)]) + } + + if case let .pattern(name, file, _) = possiblePattern?.0 { + symbolFile = file + symbolName = name + } + if let possiblePattern { + symbolPermille = Int(giftCraftConfiguration.craftAttributePermilles[Int(possiblePattern.1 - 1)]) + } + + let backdropDialSize = self.backdropDial.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + DialIndicatorComponent( + content: AnyComponentWithIdentity( + id: "color", + component: AnyComponent( + BundleIconComponent(name: "Components/ColorMask", tintColor: backdropColor) + ) + ), + backgroundColor: .white.withAlphaComponent(0.1), + foregroundColor: .white, + diameter: 48.0, + lineWidth: 4.0, + fontSize: 10.0, + percentage: backdropPermille / 10 + ) + ), + action: { [weak self] in + guard let self else { + return + } + #if DEBUG + self.component?.externalState.testFailOrSuccess = true + #endif + self.showAttributeInfo(tag: backdropButtonTag, text: "There's **\(backdropPermille / 10)%** chance the crafted gift will have **\(backdropName)** backdrop.") + }, + tag: backdropButtonTag + ) + ), + environment: {}, + containerSize: availableSize + ) + + let symbolDialSize = self.symbolDial.update( + transition: transition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + DialIndicatorComponent( + content: symbolFile.flatMap { AnyComponentWithIdentity( + id: "symbol", + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent( + context: component.context, + file: $0, + attemptSynchronously: true, + providesPlaceholder: true + ), + color: .white, + size: CGSize(width: 32.0, height: 32.0) + ) + ) + ) } ?? AnyComponentWithIdentity( + id: "empty", component: AnyComponent(Rectangle(color: .clear))), + backgroundColor: .white.withAlphaComponent(0.1), + foregroundColor: .white, + diameter: 48.0, + lineWidth: 4.0, + fontSize: 10.0, + percentage: symbolPermille / 10 + ) + ), + action: { [weak self] in + guard let self else { + return + } + #if DEBUG + self.component?.externalState.testFailOrSuccess = false + #endif + self.showAttributeInfo(tag: symbolButtonTag, text: "There's **\(symbolPermille / 10)%** chance the crafted gift will have **\(symbolName)** backdrop.") + }, + tag: symbolButtonTag + ) + ), + environment: {}, + containerSize: availableSize + ) + craftContentHeight += backdropDialSize.height + craftContentHeight += 15.0 + + let variantsString = "View all new variants" + let variantsButtonMeasure = self.variantsButtonMeasure.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .clear)))), + environment: {}, + containerSize: availableSize + ) + + let variantsButtonSize = CGSize(width: variantsButtonMeasure.width + 87.0, height: 24.0) + if let gift = self.starGiftsMap[component.gift.giftId] { + var variant1: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant2: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + var variant3: GiftItemComponent.Subject = .starGift(gift: gift, price: "") + + if let upgradePreview = self.upgradePreview { + var i = 0 + for attribute in upgradePreview { + if case .model = attribute { + switch i { + case 0: + variant1 = .preview(attributes: [attribute], rarity: nil) + case 1: + variant2 = .preview(attributes: [attribute], rarity: nil) + case 2: + variant3 = .preview(attributes: [attribute], rarity: nil) + default: + break + } + i += 1 + } + } + } + + let _ = self.variantsButton.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: variantsButtonSize, + backgroundColor: component.colors.3, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "content", component: AnyComponent(HStack([ + AnyComponentWithIdentity(id: "icon1", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: variant1, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon2", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: variant2, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon3", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: variant3, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: variantsString, font: Font.semibold(13.0), textColor: .white))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) + )) + ], spacing: 3.0))), + action: { [weak self] _ in + self?.openUpgradeVariants() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + } + craftContentHeight += 160.0 + + let originalCraftContentHeight = craftContentHeight + if component.isCrafting { + craftContentHeight = component.screenSize.height + } + + let descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionTextSize.width) * 0.5), y: craftContentHeight - 145.0 - 78.0 - 115.0), size: descriptionTextSize) + if let descriptionTextView = self.descriptionText.view { + if descriptionTextView.superview == nil { + self.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + transition.setAlpha(view: descriptionTextView, alpha: component.isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: descriptionTextView.layer, radius: component.isCrafting ? 10.0 : 0.0) + } + + let backdropDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 - 9.0 - backdropDialSize.width, y: craftContentHeight - 145.0 - 78.0), size: backdropDialSize) + if let backdropDialView = self.backdropDial.view { + if backdropDialView.superview == nil { + self.addSubview(backdropDialView) + } + transition.setFrame(view: backdropDialView, frame: backdropDialFrame) + transition.setAlpha(view: backdropDialView, alpha: component.isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: backdropDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + } + + let symbolDialFrame = CGRect(origin: CGPoint(x: availableSize.width * 0.5 + 9.0, y: craftContentHeight - 145.0 - 78.0), size: symbolDialSize) + if let symbolDialView = self.symbolDial.view { + if symbolDialView.superview == nil { + self.addSubview(symbolDialView) + } + transition.setFrame(view: symbolDialView, frame: symbolDialFrame) + transition.setAlpha(view: symbolDialView, alpha: component.isCrafting ? 0.0 : 1.0) + transition.setBlur(layer: symbolDialView.layer, radius: component.isCrafting ? 10.0 : 0.0) + } + + let variantsButtonFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - variantsButtonSize.width) / 2.0), y: craftContentHeight - 145.0), size: variantsButtonSize) + var varitantsButtonTransition = transition + if let variantsButtonView = self.variantsButton.view { + if variantsButtonView.superview == nil && !component.isCrafting { + varitantsButtonTransition = .immediate + if let symbolDialView = self.symbolDial.view { + self.insertSubview(variantsButtonView, aboveSubview: symbolDialView) + } else { + self.addSubview(variantsButtonView) + } + } + varitantsButtonTransition.setFrame(view: variantsButtonView, frame: variantsButtonFrame) + varitantsButtonTransition.setBlur(layer: variantsButtonView.layer, radius: component.isCrafting ? 10.0 : 0.0) + if component.isCrafting { + variantsButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.41, removeOnCompletion: false, completion: { _ in + variantsButtonView.removeFromSuperview() + }) + } + } + + + let permilleValue = selectedGifts.reduce(0, { $0 + Int($1.value.gift.craftChancePermille ?? 0) }) + if component.isCrafting { + var craftingOriginY = craftContentHeight * 0.5 + 160.0 + let offset = -(craftContentHeight - originalCraftContentHeight) + + let titleSize = self.craftingTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Crafting", font: Font.bold(20.0), textColor: .white))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: craftingOriginY), size: titleSize) + if let titleView = self.craftingTitle.view { + if titleView.superview == nil { + transition.animateAlpha(view: titleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: titleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: titleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + craftingOriginY += titleSize.height + craftingOriginY += 7.0 + + let subtitleSize = self.craftingSubtitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.5)))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) * 0.5), y: craftingOriginY), size: subtitleSize) + if let subtitleView = self.craftingSubtitle.view { + if subtitleView.superview == nil { + transition.animateAlpha(view: subtitleView, from: 0.0, to: 1.0) + transition.animateBlur(layer: subtitleView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: subtitleView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(subtitleView) + } + subtitleView.frame = subtitleFrame + } + craftingOriginY += subtitleSize.height + craftingOriginY += 21.0 + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionColor = UIColor.white.withAlphaComponent(0.5) + let rawDescriptionString = "If crafting fails, all selected gifts\nwill be consumed." + let descriptionString = parseMarkdownIntoAttributedString(rawDescriptionString, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionColor), linkAttribute: { _ in return nil })).mutableCopy() as! NSMutableAttributedString + + let craftingDescriptionSize = self.craftingDescription.update( + transition: transition, + component: AnyComponent( + MultilineTextWithEntitiesComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + placeholderColor: .white.withAlphaComponent(0.3), + text: .plain(descriptionString), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let craftingDescriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingDescriptionSize.width) * 0.5), y: craftingOriginY), size: craftingDescriptionSize) + if let craftingDescriptionView = self.craftingDescription.view { + if craftingDescriptionView.superview == nil { + transition.animateAlpha(view: craftingDescriptionView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingDescriptionView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingDescriptionView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(craftingDescriptionView) + } + craftingDescriptionView.frame = craftingDescriptionFrame + } + craftingOriginY += craftingDescriptionSize.height + craftingOriginY += 24.0 + + let craftingProbabilityString = "\(permilleValue / 10)% Success Chance" + let craftingProbabilityMeasure = self.craftingProbabilityMeasure.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .clear)))), + environment: {}, + containerSize: availableSize + ) + + let craftingProbabilitySize = CGSize(width: craftingProbabilityMeasure.width + 18.0, height: 24.0) + let _ = self.craftingProbability.update( + transition: transition, + component: AnyComponent( + GlassBarButtonComponent( + size: craftingProbabilitySize, + backgroundColor: component.colors.3.mixedWith(component.colors.1, alpha: 0.3), + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: craftingProbabilityString, font: Font.semibold(13.0), textColor: .white))) + )), + action: nil + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let craftingProbabilityFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - craftingProbabilitySize.width) * 0.5), y: craftingOriginY), size: craftingProbabilitySize) + if let craftingProbabilityView = self.craftingProbability.view { + if craftingProbabilityView.superview == nil { + transition.animateAlpha(view: craftingProbabilityView, from: 0.0, to: 1.0) + transition.animateBlur(layer: craftingProbabilityView.layer, fromRadius: 10.0, toRadius: 0.0) + transition.animatePosition(view: craftingProbabilityView, from: CGPoint(x: 0.0, y: offset), to: .zero, additive: true) + + self.addSubview(craftingProbabilityView) + } + craftingProbabilityView.frame = craftingProbabilityFrame + } + } + + let tableSize = CGSize(width: availableSize.width, height: 320.0) + let craftTableSize = self.craftTable.update( + transition: transition, + component: AnyComponent( + CraftTableComponent( + context: component.context, + gifts: selectedGifts, + buttonColor: component.colors.3, + isCrafting: component.isCrafting, + result: component.result, + select: { [weak self] index in + guard let self, let component = self.component, let environment = self.environment, let genericGift = self.starGiftsMap[component.gift.giftId], let resaleContext = component.resaleContext() else { + return + } + let selectController = SelectCraftGiftScreen( + context: component.context, + craftContext: component.craftContext, + resaleContext: resaleContext, + gift: component.gift, + genericGift: genericGift, + selectedGiftIds: Set(component.selectedGiftIds.values), + starsTopUpOptions: self.starsTopUpOptionsPromise.get(), + selectGift: { [weak self] item in + guard let self, let component = self.component else { + return + } + if self.giftMap[item.gift.id] == nil { + self.giftMap[item.gift.id] = item + } + component.selectGift(index, item) + } + ) + environment.controller()?.push(selectController) + }, + remove: { [weak self] index in + guard let self else { + return + } + self.component?.removeGift(index) + }, + willFinish: { [weak self] success in + guard let self else { + return + } + if !success { + self.isFailing = true + } + self.state?.updated(transition: .easeInOut(duration: 0.5)) + }, + finished: { [weak self] view in + guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() else { + return + } + if let _ = view { + if case let .gift(gift) = component.result { + let giftController = GiftViewScreen(context: component.context, subject: .profileGift(component.context.account.peerId, gift)) + if let navigationController = controller.navigationController { + navigationController.pushViewController(giftController, animated: true) + + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + } + } + controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in + controller.dismiss() + }) + } else { + if let navigationController = controller.navigationController { + controller.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { _ in + controller.dismiss() + }) + let previousController = navigationController.viewControllers[max(0, navigationController.viewControllers.count - 2)] + animateRipple(parentView: previousController.view, screenCornerRadius: environment.deviceMetrics.screenCornerRadius, location: CGPoint(x: previousController.view.bounds.midX, y: previousController.view.bounds.midY)) + } + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: tableSize.height) + ) + let craftTableFrame = CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - craftTableSize.height) / 2.0) : 10.0), size: craftTableSize) + if let craftTableView = self.craftTable.view { + if craftTableView.superview == nil { + craftTableView.layer.cornerRadius = 40.0 + craftTableView.clipsToBounds = true + craftTableView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.addSubview(craftTableView) + } + transition.setFrame(view: craftTableView, frame: craftTableFrame) + } + + transition.setAlpha(view: self.infoContainer, alpha: component.displayCraftInfo ? 1.0 : 0.0) + + let infoHeaderSize = self.infoHeader.update( + transition: transition, + component: AnyComponent( + GiftCompositionComponent( + context: component.context, + theme: environment.theme, + subject: .unique(nil, component.gift), + animationOffset: nil, + animationScale: nil, + displayAnimationStars: false, + animateScaleOnTransition: false, + externalState: nil, + requestUpdate: { _ in + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 245.0) + ) + let infoHeaderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoHeaderSize.width) * 0.5), y: 0.0), size: infoHeaderSize) + if let infoHeaderView = self.infoHeader.view { + if infoHeaderView.superview == nil { + self.infoContainer.layer.allowsGroupOpacity = true + self.addSubview(self.infoContainer) + + self.infoContainer.layer.addSublayer(self.infoBackground) + + infoHeaderView.layer.cornerRadius = 40.0 + infoHeaderView.clipsToBounds = true + infoHeaderView.layer.allowsGroupOpacity = true + infoHeaderView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.infoContainer.addSubview(infoHeaderView) + } + transition.setFrame(view: infoHeaderView, frame: infoHeaderFrame) + } + infoContentHeight += infoHeaderSize.height + infoContentHeight += 16.0 + + let infoTitleSize = self.infoTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft Gift", font: Font.bold(20.0), textColor: .white))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let infoTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoTitleSize.width) * 0.5), y: infoHeaderSize.height - 73.0), size: infoTitleSize) + if let infoTitleView = self.infoTitle.view { + if infoTitleView.superview == nil { + self.infoContainer.addSubview(infoTitleView) + } + transition.setFrame(view: infoTitleView, frame: infoTitleFrame) + } + + let infoDescriptionTextSize = self.infoDescription.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown( + text: "Use your existing gifts to craft new ones.", + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), + bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), + link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: UIColor(rgb: 0xffffff, alpha: 0.6)), + linkAttribute: { _ in return nil } + ) + ), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let infoDescriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - infoDescriptionTextSize.width) * 0.5), y: infoHeaderSize.height - 40.0), size: infoDescriptionTextSize) + if let infoDescriptionTextView = self.infoDescription.view { + if infoDescriptionTextView.superview == nil { + self.infoContainer.addSubview(infoDescriptionTextView) + } + transition.setFrame(view: infoDescriptionTextView, frame: infoDescriptionTextFrame) + } + + + self.infoBackground.backgroundColor = environment.theme.list.plainBackgroundColor.cgColor + + let infoBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 80.0), size: CGSize(width: availableSize.width, height: 1000.0)) + transition.setFrame(layer: self.infoBackground, frame: infoBackgroundFrame) + + let titleColor = environment.theme.list.itemPrimaryTextColor + let textColor = environment.theme.list.itemSecondaryTextColor + let accentColor = environment.theme.list.itemAccentColor + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "combine", + component: AnyComponent(InfoParagraphComponent( + title: "Combine Gifts", + titleColor: titleColor, + text: "Add up to 3 Gifts to attempt crafting a new upgraded model.", + textColor: textColor, + accentColor: accentColor, + iconName: "Premium/Collectible/Badge", + iconColor: environment.theme.list.itemAccentColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "input", + component: AnyComponent(InfoParagraphComponent( + title: "Input Matters", + titleColor: titleColor, + text: "Each craft has a success chance. Better combinations improve the outcome.", + textColor: textColor, + accentColor: accentColor, + iconName: "Premium/Collectible/Transferable", + iconColor: accentColor + )) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "exclusive", + component: AnyComponent(InfoParagraphComponent( + title: "Exclusive Look", + titleColor: titleColor, + text: "Reforge gifts into a rarer collectibles with a new look", + textColor: textColor, + accentColor: accentColor, + iconName: "Premium/Collectible/Unique", + iconColor: accentColor + )) + ) + ) + + let infoListSize = self.infoList.update( + transition: transition, + component: AnyComponent( + List(items) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 64.0, height: 10000) + ) + let infoListFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - infoListSize.width) / 2.0), y: infoContentHeight), size: infoListSize) + if let infoListView = self.infoList.view { + if infoListView.superview == nil { + self.infoContainer.addSubview(infoListView) + } + transition.setFrame(view: infoListView, frame: infoListFrame) + } + + if component.displayCraftInfo { + infoContentHeight += infoListSize.height + infoContentHeight += 95.0 + } + transition.setFrame(view: self.infoContainer, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: infoContentHeight))) + + transition.setFrame(layer: self.background, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: craftContentHeight))) + transition.setFrame(layer: self.overlay, frame: CGRect(origin: CGPoint(x: 0.0, y: component.isCrafting ? floor((component.screenSize.height - availableSize.width) / 2.0) : 169.0 - availableSize.width * 0.5), size: CGSize(width: availableSize.width, height: availableSize.width))) + + let effectiveContentHeight: CGFloat + if component.displayCraftInfo { + effectiveContentHeight = infoContentHeight + } else { + effectiveContentHeight = craftContentHeight + } + + return CGSize(width: availableSize.width, height: effectiveContentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let craftContext: CraftGiftsContext + let gift: StarGift.UniqueGift + + init( + context: AccountContext, + craftContext: CraftGiftsContext, + gift: StarGift.UniqueGift + ) { + self.context = context + self.craftContext = craftContext + self.gift = gift + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let giftId: Int64 + + var displayCraftInfo = false + var isCrafting = false + var inProgress = false + var result: CraftTableComponent.Result? + var selectedGiftIds: [Int32: Int64] = [:] + + private var _resaleContext: ResaleGiftsContext? + var resaleContext: ResaleGiftsContext { + if let current = self._resaleContext { + return current + } else { + let resaleContext = ResaleGiftsContext(account: self.context.account, giftId: self.giftId, forCrafting: true) + self._resaleContext = resaleContext + return resaleContext + } + } + + let preloadDisposable = DisposableSet() + + init(context: AccountContext, gift: StarGift.UniqueGift) { + self.context = context + self.giftId = gift.giftId + self.selectedGiftIds[0] = gift.id + + super.init() + + let _ = (ApplicationSpecificNotice.getGiftCraftingTips(accountManager: context.sharedContext.accountManager) + |> deliverOnMainQueue).start(next: { [weak self] count in + guard let self else { + return + } + if count < 1 { + self.displayCraftInfo = true + self.updated() + + let _ = ApplicationSpecificNotice.incrementGiftCraftingTips(accountManager: context.sharedContext.accountManager).start() + } + }) + } + + deinit { + self.preloadDisposable.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, gift: self.gift) + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let externalState = CraftGiftPageContent.ExternalState() + + return { context in + let component = context.component + let environment = context.environment[EnvironmentType.self] + let state = context.state + + let controller = environment.controller + + let craftContext = context.component.craftContext + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme + + var colors: (UIColor, UIColor, UIColor, UIColor, UIColor) = ( + UIColor(rgb: 0x263245), UIColor(rgb: 0x232e3f), UIColor(rgb: 0x304059), UIColor(rgb: 0x425168), theme.list.itemCheckColors.fillColor + ) + var permilleValue: Int32 = 0 + for id in state.selectedGiftIds.values { + if let gift = externalState.giftMap[id] { + permilleValue += gift.gift.craftChancePermille ?? 0 + } + } + if permilleValue > 900 { + colors.0 = UIColor(rgb: 0x1b3b3d) + colors.1 = UIColor(rgb: 0x1a2f38) + colors.2 = UIColor(rgb: 0x22464a) + colors.3 = UIColor(rgb: 0x2d4e50) + if !state.displayCraftInfo { + colors.4 = UIColor(rgb: 0x33bf54) + } + } + + var buttonColor = colors.3 + if state.displayCraftInfo, let backdropAttribute = component.gift.attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { + buttonColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05) + } + + var backgroundColor = colors.1 + if state.displayCraftInfo { + backgroundColor = environment.theme.list.plainBackgroundColor + } + + let giftTitle = "\(component.gift.title) #\(formatCollectibleNumber(component.gift.number, dateTimeFormat: environment.dateTimeFormat))" + + let buttonContent: AnyComponentWithIdentity + if state.displayCraftInfo { + buttonContent = AnyComponentWithIdentity(id: "info", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor))) + )) + } else { + var buttonAnimatedItems: [AnimatedTextComponent.Item] = [] + buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "percent", content: .number(Int(permilleValue / 10), minDigits: 1))) + buttonAnimatedItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text("% Success Chance"))) + + buttonContent = AnyComponentWithIdentity(id: "craft", component: AnyComponent( + VStack([ + AnyComponentWithIdentity( + id: AnyHashable("label"), + component: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: AnyHashable("label"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Craft \(giftTitle)", font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor)))) + ) + ], spacing: 2.0) + ) + ), + AnyComponentWithIdentity( + id: AnyHashable("level"), + component: AnyComponent( + AnimatedTextComponent( + font: Font.with(size: 13.0, weight: .medium, traits: .monospacedNumbers), + color: environment.theme.list.itemCheckColors.foregroundColor, + items: buttonAnimatedItems, + noDelay: true + ) + ) + ) + ], spacing: 0.0) + )) + } + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent( + CraftGiftPageContent( + context: component.context, + craftContext: component.craftContext, + resaleContext: { [weak state] in + return state?.resaleContext + }, + colors: colors, + gift: component.gift, + selectedGiftIds: state.selectedGiftIds, + displayCraftInfo: state.displayCraftInfo, + isCrafting: state.isCrafting, + inProgress: state.inProgress, + result: state.result, + screenSize: context.availableSize, + externalState: externalState, + selectGift: { [weak state] index, gift in + guard let state else { + return + } + state.selectedGiftIds[index] = gift.gift.id + state.updated(transition: .spring(duration: 0.4)) + }, + removeGift: { [weak state] index in + guard let state else { + return + } + state.selectedGiftIds[index] = nil + state.updated(transition: .spring(duration: 0.4)) + }, + dismiss: { + dismiss(true) + } + ) + ), + leftItem: state.isCrafting ? nil : AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: buttonColor, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: .white + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: state.isCrafting || state.displayCraftInfo ? nil : AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: buttonColor, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "info", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Question", + tintColor: .white + ) + )), + action: { [weak state] _ in + guard let state, !state.isCrafting else { + return + } + state.displayCraftInfo = !state.displayCraftInfo + state.updated(transition: .spring(duration: 0.3)) + } + ) + ), + hasTopEdgeEffect: false, + bottomItem: state.isCrafting ? nil : AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: colors.4, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: buttonContent, + isEnabled: state.displayCraftInfo ? true : state.selectedGiftIds.count > 0, + displaysProgress: state.inProgress, + action: { [weak state] in + guard let state else { + return + } + if state.displayCraftInfo { + state.displayCraftInfo = false + state.updated(transition: .spring(duration: 0.3)) + } else { + state.inProgress = true + state.updated(transition: .spring(duration: 0.3)) + + if let testFailOrSuccess = externalState.testFailOrSuccess { + Queue.mainQueue().after(0.5, { + state.isCrafting = true + if testFailOrSuccess { + state.result = .gift(ProfileGiftsContext.State.StarGift(gift: .unique(component.gift), reference: nil, fromPeer: nil, date: 0, text: "", entities: nil, nameHidden: false, savedToProfile: false, pinnedToTop: false, convertStars: nil, canUpgrade: false, canExportDate: nil, upgradeStars: nil, transferStars: nil, canTransferDate: nil, canResaleDate: nil, collectionIds: nil, prepaidUpgradeHash: nil, upgradeSeparate: false, dropOriginalDetailsStars: nil, number: nil, isRefunded: false, canCraftAt: nil)) + } else { + state.result = .fail + } + state.updated(transition: .spring(duration: 0.8)) + }) + return + } + + var indices: [Int] = [] + for index in state.selectedGiftIds.keys.sorted() { + indices.append(Int(index)) + } + var references: [StarGiftReference] = [] + for index in indices { + if let giftId = state.selectedGiftIds[Int32(index)], let gift = externalState.giftMap[giftId] { + references.append(gift.reference) + } + } + let _ = (craftContext.craft(references: references) + |> deliverOnMainQueue).start(next: { [weak state] result in + guard let state else { + return + } + state.isCrafting = true + state.result = .gift(result) + state.updated(transition: .spring(duration: 0.8)) + + if case let .unique(uniqueGift) = result.gift { + for attribute in uniqueGift.attributes { + switch attribute { + case let .model(_, file, _, _): + state.preloadDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + case let .pattern(_, file, _): + state.preloadDisposable.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + default: + break + } + } + } + }, error: { error in + switch error { + case .craftFailed: + state.isCrafting = true + state.result = .fail + state.updated(transition: .spring(duration: 0.8)) + default: + if let navigationController = controller()?.navigationController { + dismiss(true) + let alertController = textAlertController(context: component.context, title: nil, text: "Unknown Error", actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]) + (navigationController.topViewController as? ViewController)?.present(alertController, in: .window(.root)) + } + } + }) + } + } + ) + ), + backgroundColor: .color(backgroundColor), + isFullscreen: state.isCrafting, + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + + +public class GiftCraftScreen: ViewControllerComponentContainer { + public init( + context: AccountContext, + gift: StarGift.UniqueGift + ) { + let craftContext = CraftGiftsContext(account: context.account, giftId: gift.giftId) + + super.init( + context: context, + component: SheetContainerComponent( + context: context, + craftContext: craftContext, + gift: gift + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + + public func dismissAnimated() { + self.dismissAllTooltips() + + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } +} + + + +private struct GiftCraftConfiguration { + static var defaultValue: GiftCraftConfiguration { + return GiftCraftConfiguration( + craftAttributePermilles: [60, 180, 450, 1000] + ) + } + + let craftAttributePermilles: [Int32] + + fileprivate init( + craftAttributePermilles: [Int32] + ) { + self.craftAttributePermilles = craftAttributePermilles + } + + static func with(appConfiguration: AppConfiguration) -> GiftCraftConfiguration { + if let data = appConfiguration.data { + var craftAttributePermilles: [Int32] = [] + if let value = data["stargifts_craft_attribute_permilles"] as? [Double] { + craftAttributePermilles = value.map { Int32($0) } + } else { + craftAttributePermilles = GiftCraftConfiguration.defaultValue.craftAttributePermilles + } + + return GiftCraftConfiguration( + craftAttributePermilles: craftAttributePermilles + ) + } else { + return .defaultValue + } + } +} + +private func animateRipple(parentView: UIView, screenCornerRadius: CGFloat, location: CGPoint) { + if let snapshotView = parentView.snapshotView(afterScreenUpdates: false) { + let wrappingNode = SpaceWarpNodeImpl() + wrappingNode.isUserInteractionEnabled = false + wrappingNode.frame = CGRect(origin: .zero, size: parentView.bounds.size) + wrappingNode.update(size: parentView.bounds.size, cornerRadius: screenCornerRadius, transition: .immediate) + parentView.addSubview(wrappingNode.view) + wrappingNode.contentNode.view.addSubview(snapshotView) + + wrappingNode.triggerRipple(at: location) + + Queue.mainQueue().after(0.7, { + wrappingNode.view.removeFromSuperview() + }) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift new file mode 100644 index 0000000000..435b1905f3 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/SelectCraftGiftScreen.swift @@ -0,0 +1,636 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramStringFormatting +import ViewControllerComponent +import BundleIconComponent +import MultilineTextComponent +import GiftItemComponent +import AccountContext +import AnimatedTextComponent +import Markdown +import PresentationDataUtils +import GiftViewScreen +import NavigationStackComponent +import GiftStoreScreen +import ResizableSheetComponent +import TooltipUI +import GlassBarButtonComponent +import ConfettiEffect + +final class SelectGiftPageContent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let craftContext: CraftGiftsContext + let resaleContext: ResaleGiftsContext + let gift: StarGift.UniqueGift + let genericGift: StarGift.Gift + let selectedGiftIds: Set + let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError> + let selectGift: (GiftItem) -> Void + let dismiss: () -> Void + + init( + context: AccountContext, + craftContext: CraftGiftsContext, + resaleContext: ResaleGiftsContext, + gift: StarGift.UniqueGift, + genericGift: StarGift.Gift, + selectedGiftIds: Set, + starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>, + selectGift: @escaping (GiftItem) -> Void, + dismiss: @escaping () -> Void + ) { + self.context = context + self.craftContext = craftContext + self.resaleContext = resaleContext + self.gift = gift + self.genericGift = genericGift + self.selectedGiftIds = selectedGiftIds + self.starsTopUpOptions = starsTopUpOptions + self.selectGift = selectGift + self.dismiss = dismiss + } + + static func ==(lhs: SelectGiftPageContent, rhs: SelectGiftPageContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + if lhs.selectedGiftIds != rhs.selectedGiftIds { + return false + } + return true + } + + final class View: UIView, UIScrollViewDelegate { + private let myGiftsTitle = ComponentView() + private var gifts: [AnyHashable: ComponentView] = [:] + private let myGiftsPlaceholder = ComponentView() + + private let storeGiftsTitle = ComponentView() + private let storeGifts = ComponentView() + + private var craftState: CraftGiftsContext.State? + private var craftStateDisposable: Disposable? + + private var availableGifts: [GiftItem] = [] + private var giftMap: [Int64: GiftItem] = [:] + + private var component: SelectGiftPageContent? + private weak var state: EmptyComponentState? + private var environment: ViewControllerComponentContainer.Environment? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.cornerRadius = 40.0 + self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.craftStateDisposable?.dispose() + } + + func update(component: SelectGiftPageContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + let initialGiftItem = GiftItem( + gift: component.gift, + reference: .slug(slug: component.gift.slug) + ) + self.availableGifts = [ + initialGiftItem + ] + self.giftMap = [initialGiftItem.gift.id: initialGiftItem] + + self.craftStateDisposable = (component.craftContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + //let isFirstTime = self.craftState == nil + self.craftState = state + + var items: [GiftItem] = [] + var map: [Int64: GiftItem] = [:] + var foundInitial = false + for gift in state.gifts { + guard let reference = gift.reference, case let .unique(uniqueGift) = gift.gift else { + continue + } + let giftItem = GiftItem( + gift: uniqueGift, + reference: reference + ) + map[uniqueGift.id] = giftItem + if uniqueGift.id == component.gift.id { + foundInitial = true + } else { + if component.selectedGiftIds.contains(uniqueGift.id) { + continue + } + items.append(giftItem) + } + } + + if !foundInitial { + map[initialGiftItem.gift.id] = initialGiftItem + } + self.availableGifts = items + self.giftMap = map + + self.state?.updated(transition: .spring(duration: 0.4)) + }) + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + self.component = component + self.state = state + self.environment = environment + + self.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor + + var contentHeight: CGFloat = 88.0 + + let myGiftsTitleSize = self.myGiftsTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Your Gifts".uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let myGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: myGiftsTitleSize) + if let myGiftsTitleView = self.myGiftsTitle.view { + if myGiftsTitleView.superview == nil { + self.addSubview(myGiftsTitleView) + } + transition.setFrame(view: myGiftsTitleView, frame: myGiftsTitleFrame) + } + + contentHeight += 32.0 + + let itemSpacing: CGFloat = 10.0 + let itemSideInset = 16.0 + let itemsInRow: Int + if availableSize.width > availableSize.height || availableSize.width > 480.0 { + if case .tablet = environment.deviceMetrics.type { + itemsInRow = 4 + } else { + itemsInRow = 5 + } + } else { + itemsInRow = 3 + } + let itemWidth = (availableSize.width - itemSideInset * 2.0 - itemSpacing * CGFloat(itemsInRow - 1)) / CGFloat(itemsInRow) + let itemSize = CGSize(width: itemWidth, height: itemWidth) + var itemFrame = CGRect(origin: CGPoint(x: itemSideInset, y: contentHeight), size: itemSize) + + var itemsHeight: CGFloat = 0.0 + + var validIds: [AnyHashable] = [] + for gift in self.availableGifts { + let isVisible = "".isEmpty +// if visibleBounds.intersects(itemFrame) { +// isVisible = true +// } + if isVisible { + let itemId = AnyHashable(gift.gift.id) + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.gifts[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + self.gifts[itemId] = visibleItem + itemTransition = .immediate + } + + var ribbonColor: GiftItemComponent.Ribbon.Color = .blue + let ribbonText = "#\(gift.gift.number)" + for attribute in gift.gift.attributes { + if case let .backdrop(_, _, innerColor, outerColor, _, _, _) = attribute { + ribbonColor = .custom(outerColor, innerColor) + break + } + } + + let badge: String? = gift.gift.craftChancePermille.flatMap { "+\($0 / 10)%" } + + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + GiftItemComponent( + context: component.context, + style: .glass, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: .uniqueGift(gift: gift.gift, price: nil), + ribbon: GiftItemComponent.Ribbon(text: ribbonText, font: .monospaced, color: ribbonColor, outline: nil), + badge: badge, + resellPrice: nil, + isHidden: false, + isSelected: false, + isPinned: false, + isEditing: false, + mode: .grid, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.selectGift(gift) + component.dismiss() + }, + contextAction: { _, _ in } + ) + ), + environment: {}, + containerSize: itemSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.addSubview(itemView) + + if !transition.animation.isImmediate { + let delay = ((itemFrame.minY - contentHeight) / itemSize.height) * 0.07 + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25, delay: delay) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay) + } + } + itemView.isUserInteractionEnabled = !component.selectedGiftIds.contains(gift.gift.id) + itemView.alpha = component.selectedGiftIds.contains(gift.gift.id) ? 0.4 : 1.0 + itemView.layer.allowsGroupOpacity = itemView.alpha < 1.0 + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + + itemsHeight = itemFrame.maxY - contentHeight + + itemFrame.origin.x += itemFrame.width + itemSpacing + if itemFrame.maxX > availableSize.width { + itemFrame.origin.x = itemSideInset + itemFrame.origin.y += itemSize.height + itemSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.gifts { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.gifts.removeValue(forKey: id) + } + + if let state = self.craftState, case .ready = state.dataState, self.availableGifts.isEmpty { + contentHeight += 10.0 + let myGiftsPlaceholderSize = self.myGiftsPlaceholder.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "You don't have other gifts\nfrom this collection", font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 3, + lineSpacing: 0.1 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - 32.0, height: .greatestFiniteMagnitude) + ) + let myGiftsPlaceholderFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - myGiftsPlaceholderSize.width) / 2.0), y: contentHeight), size: myGiftsPlaceholderSize) + if let myGiftsPlaceholderView = self.myGiftsPlaceholder.view { + if myGiftsPlaceholderView.superview == nil { + self.addSubview(myGiftsPlaceholderView) + } + myGiftsPlaceholderView.frame = myGiftsPlaceholderFrame + } + contentHeight += myGiftsPlaceholderSize.height + contentHeight += 32.0 + } else { + contentHeight += itemsHeight + contentHeight += 24.0 + } + + let storeGiftsTitleSize = self.storeGiftsTitle.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "SUITABLE GIFTS ON SALE".uppercased(), font: Font.semibold(14.0), textColor: environment.theme.actionSheet.secondaryTextColor))) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let storeGiftsTitleFrame = CGRect(origin: CGPoint(x: 26.0, y: contentHeight), size: storeGiftsTitleSize) + if let storeGiftsTitleView = self.storeGiftsTitle.view { + if storeGiftsTitleView.superview == nil { + self.addSubview(storeGiftsTitleView) + } + transition.setFrame(view: storeGiftsTitleView, frame: storeGiftsTitleFrame) + } + contentHeight += 28.0 + + self.storeGifts.parentState = state + let storeGiftsSize = self.storeGifts.update( + transition: transition, + component: AnyComponent( + GiftStoreContentComponent( + context: component.context, + resaleGiftsContext: component.resaleContext, + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + safeInsets: UIEdgeInsets(), + statusBarHeight: contentHeight - 62.0, + navigationHeight: 0.0, + overNavigationContainer: self, + starsContext: component.context.starsContext!, + peerId: component.context.account.peerId, + gift: component.genericGift, + confirmPurchaseImmediately: true, + starsTopUpOptions: component.starsTopUpOptions, + scrollToTop: {}, + controller: environment.controller, + completion: { [weak self] uniqueGift in + guard let self, let component = self.component, let controller = self.environment?.controller() as? SelectCraftGiftScreen, let navigationController = controller.navigationController else { + return + } + let giftItem = GiftItem(gift: uniqueGift, reference: .slug(slug: uniqueGift.slug)) + component.selectGift(giftItem) + component.dismiss() + + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + + Queue.mainQueue().after(1.0) { + component.craftContext.reload() + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) + ) + let storeGiftsFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: storeGiftsSize) + if let storeGiftsView = self.storeGifts.view as? GiftStoreContentComponent.View { + if storeGiftsView.superview == nil { + self.insertSubview(storeGiftsView, at: 0) + } + transition.setFrame(view: storeGiftsView, frame: storeGiftsFrame) + + storeGiftsView.updateScrolling(bounds: CGRect(origin: .zero, size: availableSize), transition: .immediate) + } + contentHeight += storeGiftsSize.height + contentHeight += 90.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let craftContext: CraftGiftsContext + let resaleContext: ResaleGiftsContext + let gift: StarGift.UniqueGift + let genericGift: StarGift.Gift + let selectedGiftIds: Set + let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError> + let selectGift: (GiftItem) -> Void + + init( + context: AccountContext, + craftContext: CraftGiftsContext, + resaleContext: ResaleGiftsContext, + gift: StarGift.UniqueGift, + genericGift: StarGift.Gift, + selectedGiftIds: Set, + starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>, + selectGift: @escaping (GiftItem) -> Void + ) { + self.context = context + self.craftContext = craftContext + self.resaleContext = resaleContext + self.gift = gift + self.genericGift = genericGift + self.selectedGiftIds = selectedGiftIds + self.starsTopUpOptions = starsTopUpOptions + self.selectGift = selectGift + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + final class State: ComponentState { + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let component = context.component + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme + + let backgroundColor = environment.theme.list.modalPlainBackgroundColor + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent( + SelectGiftPageContent( + context: component.context, + craftContext: component.craftContext, + resaleContext: component.resaleContext, + gift: component.gift, + genericGift: component.genericGift, + selectedGiftIds: component.selectedGiftIds, + starsTopUpOptions: component.starsTopUpOptions, + selectGift: component.selectGift, + dismiss: { + dismiss(true) + } + ) + ), + titleItem: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Select Gifts", font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor))) + ), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "back", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Back", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: nil, + bottomItem: nil, + backgroundColor: .color(backgroundColor), + isFullscreen: false, + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +final class SelectCraftGiftScreen: ViewControllerComponentContainer { + public init( + context: AccountContext, + craftContext: CraftGiftsContext, + resaleContext: ResaleGiftsContext, + gift: StarGift.UniqueGift, + genericGift: StarGift.Gift, + selectedGiftIds: Set, + starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>, + selectGift: @escaping (GiftItem) -> Void + ) { + super.init( + context: context, + component: SheetContainerComponent( + context: context, + craftContext: craftContext, + resaleContext: resaleContext, + gift: gift, + genericGift: genericGift, + selectedGiftIds: selectedGiftIds, + starsTopUpOptions: starsTopUpOptions, + selectGift: selectGift + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + self.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + + public func dismissAnimated() { + self.dismissAllTooltips() + + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift index bdb59ec94f..812a424d9a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftDemoScreen/Sources/GiftDemoScreen.swift @@ -267,7 +267,7 @@ private final class DemoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: UIColor(rgb: 0x7f76f4), isDark: false, state: .tintedGlass, @@ -281,7 +281,7 @@ private final class DemoSheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 55268a1d69..2a7d190d3f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -159,6 +159,7 @@ public final class GiftItemComponent: Component { let label: String? let ribbon: Ribbon? let outline: Outline? + let badge: String? let resellPrice: Int64? let isLoading: Bool let isHidden: Bool @@ -186,6 +187,7 @@ public final class GiftItemComponent: Component { label: String? = nil, ribbon: Ribbon? = nil, outline: Outline? = nil, + badge: String? = nil, resellPrice: Int64? = nil, isLoading: Bool = false, isHidden: Bool = false, @@ -212,6 +214,7 @@ public final class GiftItemComponent: Component { self.label = label self.ribbon = ribbon self.outline = outline + self.badge = badge self.resellPrice = resellPrice self.isLoading = isLoading self.isHidden = isHidden @@ -262,6 +265,9 @@ public final class GiftItemComponent: Component { if lhs.outline != rhs.outline { return false } + if lhs.badge != rhs.badge { + return false + } if lhs.resellPrice != rhs.resellPrice { return false } @@ -310,7 +316,7 @@ public final class GiftItemComponent: Component { private let containerButton = HighlightTrackingButton() - private let backgroundLayer = SimpleLayer() + public let backgroundLayer = SimpleLayer() private var loadingBackground: ComponentView? private let patternView = ComponentView() @@ -349,6 +355,13 @@ public final class GiftItemComponent: Component { private var giftAuctionTimer: SwiftSignalKit.Timer? + public var pattern: UIView? { + if let view = self.patternView.view { + return view + } + return nil + } + override init(frame: CGRect) { super.init(frame: frame) @@ -416,7 +429,12 @@ public final class GiftItemComponent: Component { size = availableSize let side = floor(88.0 * availableSize.height / 116.0) iconSize = CGSize(width: side, height: side) - cornerRadius = 10.0 + switch component.style { + case .glass: + cornerRadius = 16.0 + case .legacy: + cornerRadius = 10.0 + } case .thumbnail: size = CGSize(width: availableSize.width, height: availableSize.width) iconSize = CGSize(width: floor(size.width * 0.7), height: floor(size.width * 0.7)) @@ -456,7 +474,7 @@ public final class GiftItemComponent: Component { cornerRadius = 16.0 } var backgroundSize = size - if case .grid = component.mode { + if case .grid = component.mode, component.cornerRadius == nil { backgroundSize = CGSize(width: backgroundSize.width - 4.0, height: backgroundSize.height - 4.0) } @@ -747,7 +765,7 @@ public final class GiftItemComponent: Component { backgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - backgroundSize.width) / 2.0), y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize) } } - + if case .upgradePreview = component.mode, case let .preview(attributes, rarity) = component.subject, let rarity { let isColored = attributes.count > 1 if let title = component.title { @@ -776,22 +794,37 @@ public final class GiftItemComponent: Component { } //TODO:localize + let badgeString: String + var badgeColor: UIColor? switch rarity { case let .permille(value): - badgeString = formatPercentage(Float(value) * 0.1) + if value == 0 { + badgeString = "<\(formatPercentage(0.1))" + } else { + badgeString = formatPercentage(Float(value) * 0.1) + } case .epic: badgeString = "epic" + badgeColor = UIColor(rgb: 0xaf52de) case .legendary: badgeString = "legendary" + badgeColor = UIColor(rgb: 0xd57e32) case .rare: badgeString = "rare" + badgeColor = UIColor(rgb: 0x79993d) } + var badgeTextColor = isColored ? .white : component.theme.list.itemSecondaryTextColor + var badgeBackgroundColor = isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06) + if let badgeColor { + badgeTextColor = badgeColor + badgeBackgroundColor = badgeColor.withMultipliedAlpha(0.1) + } let badgeTextSize = self.badgeText.update( transition: .spring(duration: 0.2), component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: badgeString, font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: isColored ? .white : component.theme.list.itemSecondaryTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: badgeString, font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: badgeTextColor))) ), environment: {}, containerSize: availableSize @@ -801,7 +834,7 @@ public final class GiftItemComponent: Component { let _ = self.badgeBackground.update( transition: .spring(duration: 0.2), component: AnyComponent( - RoundedRectangle(color: isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06), cornerRadius: 9.0) + RoundedRectangle(color: badgeBackgroundColor, cornerRadius: 9.0) ), environment: {}, containerSize: badgeBackgroundSize @@ -1062,6 +1095,43 @@ public final class GiftItemComponent: Component { } } + if let badgeString = component.badge { + var badgeBackgroundColor = UIColor(white: 0.0, alpha: 0.2) + if let ribbon = component.ribbon, case let .custom(bottomValue, topValue) = ribbon.color { + let topColor = UIColor(rgb: UInt32(bitPattern: topValue)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: bottomValue)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + badgeBackgroundColor = topColor.mixedWith(bottomColor, alpha: 0.8) + } + + let badgeTextSize = self.badgeText.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: badgeString, font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: .white))) + ), + environment: {}, + containerSize: availableSize + ) + + let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 11.0, height: 18.0) + let _ = self.badgeBackground.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + RoundedRectangle(color: badgeBackgroundColor, cornerRadius: 9.0) + ), + environment: {}, + containerSize: badgeBackgroundSize + ) + + if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + badgeTextView.frame = CGRect(origin: CGPoint(x: 15.0, y: 12.0), size: badgeTextSize) + badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize) + } + } + if let ribbon = component.ribbon { let ribbonFontSize: CGFloat if case .profile = component.mode { diff --git a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift index c6a1e80f69..a51f903f6e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftLoadingShimmerView/Sources/GiftLoadingShimmerView.swift @@ -181,7 +181,7 @@ public final class GiftLoadingShimmerView: UIView { y: 39.0 + 13.0 + CGFloat(rowIndex) * (itemSize.height + optionSpacing) ) context.addPath(CGPath(roundedRect: CGRect(origin: itemOrigin, size: itemSize), - cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)) + cornerWidth: 16.0, cornerHeight: 16.0, transform: nil)) } currentY += itemSize.height rowIndex += 1 diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 0ea18ea29e..3639d65c48 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -32,30 +32,64 @@ import GlassBackgroundComponent private let minimumCountToDisplayFilters = 18 -final class GiftStoreScreenComponent: Component { - typealias EnvironmentType = ViewControllerComponentContainer.Environment - +public final class GiftStoreContentComponent: Component { let context: AccountContext + let resaleGiftsContext: ResaleGiftsContext + let theme: PresentationTheme + let strings: PresentationStrings + let dateTimeFormat: PresentationDateTimeFormat + let safeInsets: UIEdgeInsets + let statusBarHeight: CGFloat + let navigationHeight: CGFloat let overNavigationContainer: UIView let starsContext: StarsContext let peerId: EnginePeer.Id let gift: StarGift.Gift + let confirmPurchaseImmediately: Bool + let starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>? + let scrollToTop: () -> Void + let controller: () -> ViewController? + let completion: ((StarGift.UniqueGift) -> Void)? - init( + public init( context: AccountContext, + resaleGiftsContext: ResaleGiftsContext, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + safeInsets: UIEdgeInsets, + statusBarHeight: CGFloat, + navigationHeight: CGFloat, overNavigationContainer: UIView, starsContext: StarsContext, peerId: EnginePeer.Id, - gift: StarGift.Gift + gift: StarGift.Gift, + confirmPurchaseImmediately: Bool, + starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>?, + scrollToTop: @escaping () -> Void, + controller: @escaping () -> ViewController?, + completion: ((StarGift.UniqueGift) -> Void)? ) { self.context = context + self.resaleGiftsContext = resaleGiftsContext + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.safeInsets = safeInsets + self.statusBarHeight = statusBarHeight + self.navigationHeight = navigationHeight self.overNavigationContainer = overNavigationContainer self.starsContext = starsContext self.peerId = peerId self.gift = gift + self.confirmPurchaseImmediately = confirmPurchaseImmediately + self.starsTopUpOptions = starsTopUpOptions + self.scrollToTop = scrollToTop + self.controller = controller + self.completion = completion } - static func ==(lhs: GiftStoreScreenComponent, rhs: GiftStoreScreenComponent) -> Bool { + public static func ==(lhs: GiftStoreContentComponent, rhs: GiftStoreContentComponent) -> Bool { if lhs.context !== rhs.context { return false } @@ -68,76 +102,33 @@ final class GiftStoreScreenComponent: Component { return true } - private final class ScrollView: UIScrollView { - override func touchesShouldCancel(in view: UIView) -> Bool { - return true - } - } - - final class View: UIView, UIScrollViewDelegate { - private let topOverscrollLayer = SimpleLayer() - private let scrollView: ScrollView - private let loadingView: GiftLoadingShimmerView + public final class View: UIView { + private let loadingView = GiftLoadingShimmerView() private let emptyResultsAnimation = ComponentView() private let emptyResultsTitle = ComponentView() private let clearFilters = ComponentView() - private let edgeEffectView: EdgeEffectView - private let cancelButton = ComponentView() - private let sortButton = ComponentView() - - private let balanceBackgroundView: GlassBackgroundView - private let balanceTitle = ComponentView() - private let balanceValue = ComponentView() - private let balanceIcon = ComponentView() - - private let title = ComponentView() - private let subtitle = ComponentView() - private var giftItems: [AnyHashable: ComponentView] = [:] private let filterSelector = ComponentView() - - private var isUpdating: Bool = false - private var starsStateDisposable: Disposable? - private var starsState: StarsContext.State? private var initialCount: Int32? private var showLoading = true private var selectedFilterId: AnyHashable? - private var component: GiftStoreScreenComponent? - private(set) weak var state: State? + private var starGiftsDisposable: Disposable? + fileprivate var starGiftsContext: ResaleGiftsContext? + fileprivate var starGiftsState: ResaleGiftsContext.State? + + private var component: GiftStoreContentComponent? + private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? + private var isUpdating: Bool = false override init(frame: CGRect) { - self.balanceBackgroundView = GlassBackgroundView() - - self.scrollView = ScrollView() - self.scrollView.showsVerticalScrollIndicator = true - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.scrollsToTop = false - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.contentInsetAdjustmentBehavior = .never - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.alwaysBounceVertical = true - - self.loadingView = GiftLoadingShimmerView() - - self.edgeEffectView = EdgeEffectView() - super.init(frame: frame) - self.scrollView.delegate = self - self.addSubview(self.scrollView) - - self.addSubview(self.edgeEffectView) self.addSubview(self.loadingView) - - self.scrollView.layer.addSublayer(self.topOverscrollLayer) } required init?(coder: NSCoder) { @@ -145,21 +136,12 @@ final class GiftStoreScreenComponent: Component { } deinit { - self.starsStateDisposable?.dispose() - } - - func scrollToTop() { - self.scrollView.setContentOffset(CGPoint(), animated: true) - } - - var nextScrollTransition: ComponentTransition? - func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) + self.starGiftsDisposable?.dispose() } private var currentGifts: ([StarGift], Set, Set, Set)? private var effectiveGifts: [StarGift]? { - if let gifts = self.state?.starGiftsState?.gifts { + if let gifts = self.starGiftsState?.gifts { return gifts } else { return nil @@ -167,28 +149,31 @@ final class GiftStoreScreenComponent: Component { } private var effectiveIsLoading: Bool { - if self.state?.starGiftsState?.gifts == nil || self.state?.starGiftsState?.dataState == .loading { + if self.starGiftsState?.gifts == nil || self.starGiftsState?.dataState == .loading { return true } return false } - private func updateScrolling(interactive: Bool = false, transition: ComponentTransition) { - guard let environment = self.environment, let component = self.component else { + private var currentBounds: CGRect? + private var contentHeight: CGFloat = 0.0 + public func updateScrolling(bounds: CGRect, interactive: Bool = false, transition: ComponentTransition) { + guard let component = self.component else { return } + self.currentBounds = bounds + + let availableWidth = bounds.width + let availableHeight = bounds.height - let availableWidth = self.scrollView.bounds.width - let availableHeight = self.scrollView.bounds.height - - var topInset = environment.navigationHeight + 53.0 + var topInset = component.navigationHeight + 53.0 if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters { - topInset = environment.navigationHeight + topInset = component.navigationHeight } - let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + let visibleBounds = bounds.insetBy(dx: 0.0, dy: -10.0) if let starGifts = self.effectiveGifts { - let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sideInset: CGFloat = 16.0 + component.safeInsets.left let optionSpacing: CGFloat = 10.0 let optionWidth = (availableWidth - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 @@ -197,7 +182,7 @@ final class GiftStoreScreenComponent: Component { var validIds: [AnyHashable] = [] var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: starsOptionSize) - let controller = environment.controller + let controller = component.controller for gift in starGifts { guard case let .unique(uniqueGift) = gift else { @@ -240,7 +225,7 @@ final class GiftStoreScreenComponent: Component { let subject: GiftItemComponent.Subject = .uniqueGift( gift: uniqueGift, - price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })?.amount.value ?? 0), environment.dateTimeFormat.groupingSeparator))" + price: "# \(presentationStringsFormattedNumber(Int32(uniqueGift.resellAmounts?.first(where: { $0.currency == .stars })?.amount.value ?? 0), component.dateTimeFormat.groupingSeparator))" ) let _ = visibleItem.update( transition: itemTransition, @@ -249,8 +234,9 @@ final class GiftStoreScreenComponent: Component { content: AnyComponent( GiftItemComponent( context: component.context, - theme: environment.theme, - strings: environment.strings, + style: .glass, + theme: component.theme, + strings: component.strings, peer: nil, subject: subject, ribbon: ribbon @@ -258,10 +244,35 @@ final class GiftStoreScreenComponent: Component { ), effectAlignment: .center, action: { [weak self] in - if let self, let component = self.component, let state = self.state { - if let controller = controller() as? GiftStoreScreen { + guard let self, let component = self.component else { + return + } + if component.confirmPurchaseImmediately, let starsTopUpOptions = component.starsTopUpOptions { + buyStarGiftImpl( + context: component.context, + recipientPeerId: component.context.account.peerId, + uniqueGift: uniqueGift, + showAttributes: true, + acceptedPrice: nil, + skipConfirmation: false, + starsTopUpOptions: starsTopUpOptions, + buyGift: { [weak self] slug, peerId, price in + return self?.starGiftsContext?.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete() + }, + getController: controller, + updateProgress: { _ in }, + updateIsBalanceVisible: { _ in }, + completion: { [weak self] in + if let self, let component = self.component { + component.completion?(uniqueGift) + } + } + ) + } else { + if let controller = controller() { let mainController: ViewController - if let parentController = controller.parentController() { + if let controller = controller as? GiftStoreScreen, + let parentController = controller.parentController() { mainController = parentController } else { mainController = controller @@ -269,7 +280,7 @@ final class GiftStoreScreenComponent: Component { let allSubjects: [GiftViewScreen.Subject] = (self.effectiveGifts ?? []).compactMap { gift in if case let .unique(uniqueGift) = gift { - return .uniqueGift(uniqueGift, state.peerId) + return .uniqueGift(uniqueGift, component.peerId) } return nil } @@ -277,14 +288,14 @@ final class GiftStoreScreenComponent: Component { let giftController = GiftViewScreen( context: component.context, - subject: .uniqueGift(uniqueGift, state.peerId), + subject: .uniqueGift(uniqueGift, component.peerId), allSubjects: allSubjects, index: index, buyGift: { slug, peerId, price in - return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete() + return self.starGiftsContext?.buyStarGift(slug: slug, peerId: peerId, price: price) ?? .complete() }, updateResellStars: { _, price in - return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() + return self.starGiftsContext?.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() } ) mainController.push(giftController) @@ -294,12 +305,17 @@ final class GiftStoreScreenComponent: Component { animateAlpha: false ) ), - environment: {}, + environment: { + }, containerSize: starsOptionSize ) if let itemView = visibleItem.view { if itemView.superview == nil { - self.scrollView.addSubview(itemView) + if let _ = self.loadingView.superview { + self.insertSubview(itemView, belowSubview: self.loadingView) + } else { + self.addSubview(itemView) + } } itemTransition.setFrame(view: itemView, frame: itemFrame) } @@ -339,7 +355,7 @@ final class GiftStoreScreenComponent: Component { PlainButtonComponent( content: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ClearFilters, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)), + text: .plain(NSAttributedString(string: component.strings.Gift_Store_ClearFilters, font: Font.regular(17.0), textColor: component.theme.list.itemAccentColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 ) @@ -350,8 +366,8 @@ final class GiftStoreScreenComponent: Component { return } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes([]) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes([]) + component.scrollToTop() }, animateScale: false ) @@ -361,21 +377,21 @@ final class GiftStoreScreenComponent: Component { ) var showClearFilters = false - if let filterAttributes = self.state?.starGiftsState?.filterAttributes, !filterAttributes.isEmpty { + if let filterAttributes = self.starGiftsState?.filterAttributes, !filterAttributes.isEmpty { showClearFilters = true } - let bottomInset: CGFloat = environment.safeInsets.bottom + let bottomInset: CGFloat = component.safeInsets.bottom var emptyResultsActionFrame = CGRect( origin: CGPoint( x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), - y: max(self.scrollView.contentSize.height - 70.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0) + y: max(self.contentHeight - 70.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0) ), size: emptyResultsActionSize ) - if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading { + if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.starGiftsState?.dataState != .loading { let emptyAnimationHeight = 148.0 let visibleHeight = availableHeight let emptyAnimationSpacing: CGFloat = 20.0 @@ -385,7 +401,7 @@ final class GiftStoreScreenComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Gift_Store_EmptyResults, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: component.strings.Gift_Store_EmptyResults, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center ) ), @@ -415,7 +431,7 @@ final class GiftStoreScreenComponent: Component { if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, aboveSubview: self.scrollView) + self.addSubview(view) view.playOnce() } view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) @@ -425,7 +441,7 @@ final class GiftStoreScreenComponent: Component { if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.insertSubview(view, aboveSubview: self.scrollView) + self.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center) @@ -448,12 +464,12 @@ final class GiftStoreScreenComponent: Component { if view.superview == nil { view.alpha = 0.0 fadeTransition.setAlpha(view: view, alpha: 1.0) - self.scrollView.addSubview(view) + self.addSubview(view) } view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size) ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center) - view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 + view.alpha = self.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0 } } else { if let view = self.clearFilters.view { @@ -463,14 +479,14 @@ final class GiftStoreScreenComponent: Component { } } - let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height) + let bottomContentOffset = max(0.0, self.contentHeight - bounds.origin.y - bounds.height) if interactive, bottomContentOffset < 800.0 { - self.state?.starGiftsContext.loadMore() + self.starGiftsContext?.loadMore() } } func openSortContextMenu(sourceView: UIView) { - guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else { + guard let component = self.component, let controller = component.controller(), !self.effectiveIsLoading else { return } @@ -485,8 +501,8 @@ final class GiftStoreScreenComponent: Component { return } self.showLoading = true - self.state?.starGiftsContext.updateSorting(.value) - self.scrollToTop() + self.starGiftsContext?.updateSorting(.value) + component.scrollToTop() }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByDate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor) @@ -496,8 +512,8 @@ final class GiftStoreScreenComponent: Component { return } self.showLoading = true - self.state?.starGiftsContext.updateSorting(.date) - self.scrollToTop() + self.starGiftsContext?.updateSorting(.date) + component.scrollToTop() }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByNumber, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) @@ -507,8 +523,8 @@ final class GiftStoreScreenComponent: Component { return } self.showLoading = true - self.state?.starGiftsContext.updateSorting(.number) - self.scrollToTop() + self.starGiftsContext?.updateSorting(.number) + component.scrollToTop() }))) let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftStoreReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) @@ -523,14 +539,14 @@ final class GiftStoreScreenComponent: Component { } func openModelContextMenu(sourceView: UIView) { - guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else { + guard let component = self.component, let controller = self.component?.controller(), !self.effectiveIsLoading else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let searchQueryPromise = ValuePromise("") - let attributes = self.state?.starGiftsState?.attributes ?? [] + let attributes = self.starGiftsState?.attributes ?? [] let modelAttributes = attributes.filter { attribute in if case .model = attribute { return true @@ -538,14 +554,14 @@ final class GiftStoreScreenComponent: Component { return false } }.sorted(by: { lhs, rhs in - if case let .model(_, lhsFile, _, _) = lhs, case let .model(_, rhsFile, _, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] { + if case let .model(_, lhsFile, _, _) = lhs, case let .model(_, rhsFile, _, _) = rhs, let lhsCount = self.starGiftsState?.attributeCount[.model(lhsFile.fileId.id)], let rhsCount = self.starGiftsState?.attributeCount[.model(rhsFile.fileId.id)] { return lhsCount > rhsCount } else { return false } }) - let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let currentFilterAttributes = self.starGiftsState?.filterAttributes ?? [] let selectedModelAttributes = currentFilterAttributes.filter { attribute in if case .model = attribute { return true @@ -570,7 +586,7 @@ final class GiftStoreScreenComponent: Component { context: component.context, attributes: modelAttributes, selectedAttributes: selectedModelAttributes, - attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + attributeCount: self.starGiftsState?.attributeCount ?? [:], searchQuery: searchQueryPromise.get(), attributeSelected: { [weak self] attribute, exclusive in guard let self else { @@ -594,8 +610,8 @@ final class GiftStoreScreenComponent: Component { } } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() }, selectAll: { [weak self] in guard let self else { @@ -608,8 +624,8 @@ final class GiftStoreScreenComponent: Component { return true } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() } ), false)) @@ -631,14 +647,14 @@ final class GiftStoreScreenComponent: Component { } func openBackdropContextMenu(sourceView: UIView) { - guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else { + guard let component = self.component, let controller = self.component?.controller(), !self.effectiveIsLoading else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let searchQueryPromise = ValuePromise("") - let attributes = self.state?.starGiftsState?.attributes ?? [] + let attributes = self.starGiftsState?.attributes ?? [] let backdropAttributes = attributes.filter { attribute in if case .backdrop = attribute { return true @@ -646,14 +662,14 @@ final class GiftStoreScreenComponent: Component { return false } }.sorted(by: { lhs, rhs in - if case let .backdrop(_, lhsId, _, _, _, _, _) = lhs, case let .backdrop(_, rhsId, _, _, _, _, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.backdrop(lhsId)], let rhsCount = self.state?.starGiftsState?.attributeCount[.backdrop(rhsId)] { + if case let .backdrop(_, lhsId, _, _, _, _, _) = lhs, case let .backdrop(_, rhsId, _, _, _, _, _) = rhs, let lhsCount = self.starGiftsState?.attributeCount[.backdrop(lhsId)], let rhsCount = self.starGiftsState?.attributeCount[.backdrop(rhsId)] { return lhsCount > rhsCount } else { return false } }) - let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let currentFilterAttributes = self.starGiftsState?.filterAttributes ?? [] let selectedBackdropAttributes = currentFilterAttributes.filter { attribute in if case .backdrop = attribute { return true @@ -678,7 +694,7 @@ final class GiftStoreScreenComponent: Component { context: component.context, attributes: backdropAttributes, selectedAttributes: selectedBackdropAttributes, - attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + attributeCount: self.starGiftsState?.attributeCount ?? [:], searchQuery: searchQueryPromise.get(), attributeSelected: { [weak self] attribute, exclusive in guard let self else { @@ -702,8 +718,8 @@ final class GiftStoreScreenComponent: Component { } } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() }, selectAll: { [weak self] in guard let self else { @@ -716,8 +732,8 @@ final class GiftStoreScreenComponent: Component { return true } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() } ), false)) @@ -739,14 +755,14 @@ final class GiftStoreScreenComponent: Component { } func openSymbolContextMenu(sourceView: UIView) { - guard let component = self.component, let controller = self.environment?.controller(), !self.effectiveIsLoading else { + guard let component = self.component, let controller = self.component?.controller(), !self.effectiveIsLoading else { return } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let searchQueryPromise = ValuePromise("") - let attributes = self.state?.starGiftsState?.attributes ?? [] + let attributes = self.starGiftsState?.attributes ?? [] let patternAttributes = attributes.filter { attribute in if case .pattern = attribute { return true @@ -754,14 +770,14 @@ final class GiftStoreScreenComponent: Component { return false } }.sorted(by: { lhs, rhs in - if case let .pattern(_, lhsFile, _) = lhs, case let .pattern(_, rhsFile, _) = rhs, let lhsCount = self.state?.starGiftsState?.attributeCount[.pattern(lhsFile.fileId.id)], let rhsCount = self.state?.starGiftsState?.attributeCount[.pattern(rhsFile.fileId.id)] { + if case let .pattern(_, lhsFile, _) = lhs, case let .pattern(_, rhsFile, _) = rhs, let lhsCount = self.starGiftsState?.attributeCount[.pattern(lhsFile.fileId.id)], let rhsCount = self.starGiftsState?.attributeCount[.pattern(rhsFile.fileId.id)] { return lhsCount > rhsCount } else { return false } }) - let currentFilterAttributes = self.state?.starGiftsState?.filterAttributes ?? [] + let currentFilterAttributes = self.starGiftsState?.filterAttributes ?? [] let selectedPatternAttributes = currentFilterAttributes.filter { attribute in if case .pattern = attribute { return true @@ -786,7 +802,7 @@ final class GiftStoreScreenComponent: Component { context: component.context, attributes: patternAttributes, selectedAttributes: selectedPatternAttributes, - attributeCount: self.state?.starGiftsState?.attributeCount ?? [:], + attributeCount: self.starGiftsState?.attributeCount ?? [:], searchQuery: searchQueryPromise.get(), attributeSelected: { [weak self] attribute, exclusive in guard let self else { @@ -810,8 +826,8 @@ final class GiftStoreScreenComponent: Component { } } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() }, selectAll: { [weak self] in guard let self else { @@ -824,8 +840,8 @@ final class GiftStoreScreenComponent: Component { return true } self.showLoading = true - self.state?.starGiftsContext.updateFilterAttributes(updatedFilterAttributes) - self.scrollToTop() + self.starGiftsContext?.updateFilterAttributes(updatedFilterAttributes) + component.scrollToTop() } ), false)) @@ -846,6 +862,356 @@ final class GiftStoreScreenComponent: Component { controller.presentInGlobalOverlay(contextController) } + func update(component: GiftStoreContentComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + self.state = state + + if self.component == nil { + self.starGiftsContext = component.resaleGiftsContext + self.starGiftsDisposable = (self.starGiftsContext!.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let previousFilterAttributes = self.starGiftsState?.filterAttributes + let previousSorting = self.starGiftsState?.sorting + self.starGiftsState = state + + var transition: ComponentTransition = .immediate + if let previousFilterAttributes, previousFilterAttributes != state.filterAttributes { + transition = .easeInOut(duration: 0.25) + } else if let previousSorting, previousSorting != state.sorting { + transition = .easeInOut(duration: 0.25) + } + if !self.isUpdating { + self.state?.updated(transition: transition) + } + }) + } + self.component = component + + if let count = self.starGiftsState?.count, count > 0 { + if self.initialCount == nil { + self.initialCount = count + } + } + + let isLoading = self.effectiveIsLoading + if case let .ready(loadMore, nextOffset) = self.starGiftsState?.dataState { + if loadMore && nextOffset == nil { + } else { + self.showLoading = false + } + } + + let theme = component.theme + let strings = component.strings + + let bottomContentInset: CGFloat = 56.0 + let sideInset: CGFloat = 16.0 + component.safeInsets.left + + var contentHeight: CGFloat = 0.0 + contentHeight += component.navigationHeight + + var topInset: CGFloat = 0.0 + if component.statusBarHeight > 0.0 { + topInset = component.statusBarHeight - 6.0 + } + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + + var sortingTitle = strings.Gift_Store_Sort_Date + var sortingIcon: String = "GiftFilterDate" + var sortingIndex: Int = 0 + if let sorting = self.starGiftsState?.sorting { + switch sorting { + case .value: + sortingTitle = component.strings.Gift_Store_Sort_Price + sortingIcon = "GiftFilterPrice" + sortingIndex = 0 + case .date: + sortingTitle = component.strings.Gift_Store_Sort_Date + sortingIcon = "GiftFilterDate" + sortingIndex = 1 + case .number: + sortingTitle = component.strings.Gift_Store_Sort_Number + sortingIcon = "GiftFilterNumber" + sortingIndex = 2 + } + } + + enum FilterItemId: Int32 { + case sort + case model + case backdrop + case symbol + } + + var filterItems: [FilterSelectorComponent.Item] = [] + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(FilterItemId.sort), + index: sortingIndex, + iconName: sortingIcon, + title: sortingTitle, + action: { [weak self] view in + if let self { + self.selectedFilterId = AnyHashable(FilterItemId.sort) + self.openSortContextMenu(sourceView: view) + self.state?.updated() + } + } + )) + + var modelTitle = component.strings.Gift_Store_Filter_Model + var backdropTitle = component.strings.Gift_Store_Filter_Backdrop + var symbolTitle = component.strings.Gift_Store_Filter_Symbol + var modelCount: Int32 = 0 + var backdropCount: Int32 = 0 + var symbolCount: Int32 = 0 + if let filterAttributes = self.starGiftsState?.filterAttributes { + for attribute in filterAttributes { + switch attribute { + case .model: + modelCount += 1 + case .backdrop: + backdropCount += 1 + case .pattern: + symbolCount += 1 + } + } + + if modelCount > 0 { + modelTitle = component.strings.Gift_Store_Filter_Selected_Model(modelCount) + } + if backdropCount > 0 { + backdropTitle = component.strings.Gift_Store_Filter_Selected_Backdrop(backdropCount) + } + if symbolCount > 0 { + symbolTitle = component.strings.Gift_Store_Filter_Selected_Symbol(symbolCount) + } + } + + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(FilterItemId.model), + index: Int(modelCount), + title: modelTitle, + action: { [weak self] view in + if let self { + self.selectedFilterId = AnyHashable(FilterItemId.model) + self.openModelContextMenu(sourceView: view) + self.state?.updated() + } + } + )) + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(FilterItemId.backdrop), + index: Int(backdropCount), + title: backdropTitle, + action: { [weak self] view in + if let self { + self.selectedFilterId = AnyHashable(FilterItemId.backdrop) + self.openBackdropContextMenu(sourceView: view) + self.state?.updated() + } + } + )) + filterItems.append(FilterSelectorComponent.Item( + id: AnyHashable(FilterItemId.symbol), + index: Int(symbolCount), + title: symbolTitle, + action: { [weak self] view in + if let self { + self.selectedFilterId = AnyHashable(FilterItemId.symbol) + self.openSymbolContextMenu(sourceView: view) + self.state?.updated() + } + } + )) + + let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) + + var showingFilters = false + let filterSize = self.filterSelector.update( + transition: transition, + component: AnyComponent(FilterSelectorComponent( + context: component.context, + theme: theme, + items: filterItems, + selectedItemId: self.selectedFilterId + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) + ) + if let filterSelectorView = self.filterSelector.view { + if filterSelectorView.superview == nil { + filterSelectorView.alpha = 0.0 + component.overNavigationContainer.addSubview(filterSelectorView) + } + transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: topInset + 60.0 + 18.0), size: filterSize)) + + if let initialCount = self.initialCount, initialCount >= minimumCountToDisplayFilters { + loadingTransition.setAlpha(view: filterSelectorView, alpha: 1.0) + showingFilters = true + } + } + + if let starGifts = self.starGiftsState?.gifts { + let starsOptionSize = CGSize(width: optionWidth, height: 154.0) + let optionSpacing: CGFloat = 10.0 + contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * (starsOptionSize.height + optionSpacing) + contentHeight += -optionSpacing + 66.0 + } + + contentHeight += bottomContentInset + contentHeight += component.safeInsets.bottom + + self.contentHeight = contentHeight + + self.updateScrolling(bounds: self.currentBounds ?? .zero, transition: transition) + + let loadingSize = CGSize(width: availableSize.width, height: min(1000.0, availableSize.height)) + if isLoading && self.showLoading { + self.loadingView.update(size: loadingSize, theme: component.theme, showFilters: !showingFilters, isPlain: true, transition: .immediate) + loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) + } else { + loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) + } + transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: component.navigationHeight + 10.0), size: loadingSize)) + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class GiftStoreScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let resaleGiftsContext: ResaleGiftsContext + let overNavigationContainer: UIView + let starsContext: StarsContext + let peerId: EnginePeer.Id + let gift: StarGift.Gift + + init( + context: AccountContext, + resaleGiftsContext: ResaleGiftsContext, + overNavigationContainer: UIView, + starsContext: StarsContext, + peerId: EnginePeer.Id, + gift: StarGift.Gift + ) { + self.context = context + self.resaleGiftsContext = resaleGiftsContext + self.overNavigationContainer = overNavigationContainer + self.starsContext = starsContext + self.peerId = peerId + self.gift = gift + } + + static func ==(lhs: GiftStoreScreenComponent, rhs: GiftStoreScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peerId != rhs.peerId { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollView + + private let edgeEffectView: EdgeEffectView + + private let balanceBackgroundView: GlassBackgroundView + private let balanceTitle = ComponentView() + private let balanceValue = ComponentView() + private let balanceIcon = ComponentView() + + private let title = ComponentView() + private let subtitle = ComponentView() + private let content = ComponentView() + + private var starsStateDisposable: Disposable? + private var starsState: StarsContext.State? + + private var initialCount: Int32? + + private var component: GiftStoreScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + private var isUpdating: Bool = false + + override init(frame: CGRect) { + self.balanceBackgroundView = GlassBackgroundView() + + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + self.edgeEffectView = EdgeEffectView() + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.addSubview(self.edgeEffectView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.starsStateDisposable?.dispose() + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + var nextScrollTransition: ComponentTransition? + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(bounds: scrollView.bounds, interactive: true, transition: self.nextScrollTransition ?? .immediate) + } + + private func updateScrolling(bounds: CGRect, interactive: Bool = false, transition: ComponentTransition) { + if let contentView = self.content.view as? GiftStoreContentComponent.View { + contentView.updateScrolling(bounds: bounds, interactive: interactive, transition: transition) + } + } + func update(component: GiftStoreScreenComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -870,15 +1236,7 @@ final class GiftStoreScreenComponent: Component { }) } self.component = component - - let isLoading = self.effectiveIsLoading - if case let .ready(loadMore, nextOffset) = self.state?.starGiftsState?.dataState { - if loadMore && nextOffset == nil { - } else { - self.showLoading = false - } - } - + let theme = environment.theme let strings = environment.strings @@ -886,13 +1244,8 @@ final class GiftStoreScreenComponent: Component { self.backgroundColor = environment.theme.list.blocksBackgroundColor } - let bottomContentInset: CGFloat = 56.0 - let sideInset: CGFloat = 16.0 + environment.safeInsets.left let headerSideInset: CGFloat = 24.0 + environment.safeInsets.left - var contentHeight: CGFloat = 0.0 - contentHeight += environment.navigationHeight - var topPanelHeight = environment.navigationHeight + 53.0 if let initialCount = self.initialCount, initialCount < minimumCountToDisplayFilters { topPanelHeight = environment.navigationHeight @@ -988,9 +1341,51 @@ final class GiftStoreScreenComponent: Component { } transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: topInset + 22.0), size: titleSize)) } + + let controller = environment.controller + self.content.parentState = state + let contentSize = self.content.update( + transition: transition, + component: AnyComponent( + GiftStoreContentComponent( + context: component.context, + resaleGiftsContext: component.resaleGiftsContext, + theme: theme, + strings: strings, + dateTimeFormat: environment.dateTimeFormat, + safeInsets: environment.safeInsets, + statusBarHeight: environment.statusBarHeight, + navigationHeight: environment.navigationHeight, + overNavigationContainer: component.overNavigationContainer, + starsContext: component.starsContext, + peerId: component.peerId, + gift: component.gift, + confirmPurchaseImmediately: false, + starsTopUpOptions: nil, + scrollToTop: { [weak self] in + self?.scrollToTop() + }, + controller: { + return controller() + }, + completion: nil + ) + ), + environment: { + }, + containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) + ) + let contentFrame = CGRect(origin: .zero, size: contentSize) + if let contentView = self.content.view { + if contentView.superview == nil { + self.scrollView.addSubview(contentView) + } + transition.setFrame(view: contentView, frame: contentFrame) + } + let effectiveCount: Int32 - if let count = self.state?.starGiftsState?.count, count > 0 || self.initialCount != nil { + if let contentView = self.content.view as? GiftStoreContentComponent.View, let starGiftsState = contentView.starGiftsState, let count = starGiftsState.count, count > 0 || self.initialCount != nil { if self.initialCount == nil { self.initialCount = count } @@ -1019,157 +1414,9 @@ final class GiftStoreScreenComponent: Component { transition.setFrame(view: subtitleView, frame: subtitleFrame) } - let optionSpacing: CGFloat = 10.0 - let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 - - var sortingTitle = environment.strings.Gift_Store_Sort_Date - var sortingIcon: String = "GiftFilterDate" - var sortingIndex: Int = 0 - if let sorting = self.state?.starGiftsState?.sorting { - switch sorting { - case .value: - sortingTitle = environment.strings.Gift_Store_Sort_Price - sortingIcon = "GiftFilterPrice" - sortingIndex = 0 - case .date: - sortingTitle = environment.strings.Gift_Store_Sort_Date - sortingIcon = "GiftFilterDate" - sortingIndex = 1 - case .number: - sortingTitle = environment.strings.Gift_Store_Sort_Number - sortingIcon = "GiftFilterNumber" - sortingIndex = 2 - } - } - - enum FilterItemId: Int32 { - case sort - case model - case backdrop - case symbol - } - - var filterItems: [FilterSelectorComponent.Item] = [] - filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(FilterItemId.sort), - index: sortingIndex, - iconName: sortingIcon, - title: sortingTitle, - action: { [weak self] view in - if let self { - self.selectedFilterId = AnyHashable(FilterItemId.sort) - self.openSortContextMenu(sourceView: view) - self.state?.updated() - } - } - )) - - var modelTitle = environment.strings.Gift_Store_Filter_Model - var backdropTitle = environment.strings.Gift_Store_Filter_Backdrop - var symbolTitle = environment.strings.Gift_Store_Filter_Symbol - var modelCount: Int32 = 0 - var backdropCount: Int32 = 0 - var symbolCount: Int32 = 0 - if let filterAttributes = self.state?.starGiftsState?.filterAttributes { - for attribute in filterAttributes { - switch attribute { - case .model: - modelCount += 1 - case .backdrop: - backdropCount += 1 - case .pattern: - symbolCount += 1 - } - } - - if modelCount > 0 { - modelTitle = environment.strings.Gift_Store_Filter_Selected_Model(modelCount) - } - if backdropCount > 0 { - backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(backdropCount) - } - if symbolCount > 0 { - symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(symbolCount) - } - } - - filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(FilterItemId.model), - index: Int(modelCount), - title: modelTitle, - action: { [weak self] view in - if let self { - self.selectedFilterId = AnyHashable(FilterItemId.model) - self.openModelContextMenu(sourceView: view) - self.state?.updated() - } - } - )) - filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(FilterItemId.backdrop), - index: Int(backdropCount), - title: backdropTitle, - action: { [weak self] view in - if let self { - self.selectedFilterId = AnyHashable(FilterItemId.backdrop) - self.openBackdropContextMenu(sourceView: view) - self.state?.updated() - } - } - )) - filterItems.append(FilterSelectorComponent.Item( - id: AnyHashable(FilterItemId.symbol), - index: Int(symbolCount), - title: symbolTitle, - action: { [weak self] view in - if let self { - self.selectedFilterId = AnyHashable(FilterItemId.symbol) - self.openSymbolContextMenu(sourceView: view) - self.state?.updated() - } - } - )) - - let loadingTransition: ComponentTransition = .easeInOut(duration: 0.25) - - var showingFilters = false - let filterSize = self.filterSelector.update( - transition: transition, - component: AnyComponent(FilterSelectorComponent( - context: component.context, - theme: theme, - items: filterItems, - selectedItemId: self.selectedFilterId - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) - ) - if let filterSelectorView = self.filterSelector.view { - if filterSelectorView.superview == nil { - filterSelectorView.alpha = 0.0 - component.overNavigationContainer.addSubview(filterSelectorView) - } - transition.setFrame(view: filterSelectorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - filterSize.width) / 2.0), y: topInset + 60.0 + 18.0), size: filterSize)) - - if let initialCount = self.initialCount, initialCount >= minimumCountToDisplayFilters { - loadingTransition.setAlpha(view: filterSelectorView, alpha: 1.0) - showingFilters = true - } - } - - if let starGifts = self.state?.starGiftsState?.gifts { - let starsOptionSize = CGSize(width: optionWidth, height: 154.0) - let optionSpacing: CGFloat = 10.0 - contentHeight += ceil(CGFloat(starGifts.count) / 3.0) * (starsOptionSize.height + optionSpacing) - contentHeight += -optionSpacing + 66.0 - } - - contentHeight += bottomContentInset - contentHeight += environment.safeInsets.bottom let previousBounds = self.scrollView.bounds - let contentSize = CGSize(width: availableSize.width, height: contentHeight) if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) } @@ -1192,19 +1439,9 @@ final class GiftStoreScreenComponent: Component { transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) } } - - self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) - - self.updateScrolling(transition: transition) - if isLoading && self.showLoading { - self.loadingView.update(size: availableSize, theme: environment.theme, showFilters: !showingFilters, transition: .immediate) - loadingTransition.setAlpha(view: self.loadingView, alpha: 1.0) - } else { - loadingTransition.setAlpha(view: self.loadingView, alpha: 0.0) - } - transition.setFrame(view: self.loadingView, frame: CGRect(origin: CGPoint(x: 0.0, y: environment.navigationHeight + 10.0), size: availableSize)) - + self.updateScrolling(bounds: self.scrollView.bounds, transition: transition) + return availableSize } } @@ -1212,58 +1449,8 @@ final class GiftStoreScreenComponent: Component { func makeView() -> View { return View() } - - final class State: ComponentState { - private let context: AccountContext - var peerId: EnginePeer.Id - private let gift: StarGift.Gift - - private var disposable: Disposable? - - fileprivate let starGiftsContext: ResaleGiftsContext - fileprivate var starGiftsState: ResaleGiftsContext.State? - - init( - context: AccountContext, - peerId: EnginePeer.Id, - gift: StarGift.Gift - ) { - self.context = context - self.peerId = peerId - self.gift = gift - self.starGiftsContext = ResaleGiftsContext(account: context.account, giftId: gift.id) - super.init() - - self.disposable = (self.starGiftsContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let self else { - return - } - let previousFilterAttributes = self.starGiftsState?.filterAttributes - let previousSorting = self.starGiftsState?.sorting - self.starGiftsState = state - - var transition: ComponentTransition = .immediate - if let previousFilterAttributes, previousFilterAttributes != state.filterAttributes { - transition = .easeInOut(duration: 0.25) - } else if let previousSorting, previousSorting != state.sorting { - transition = .easeInOut(duration: 0.25) - } - self.updated(transition: transition) - }) - } - - deinit { - self.disposable?.dispose() - } - } - - func makeState() -> State { - return State(context: self.context, peerId: self.peerId, gift: self.gift) - } - - func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } @@ -1284,11 +1471,13 @@ public class GiftStoreScreen: ViewControllerComponentContainer { gift: StarGift.Gift ) { self.context = context - self.overNavigationContainer = SparseContainerView() + let resaleGiftsContext = ResaleGiftsContext(account: self.context.account, giftId: gift.id, forCrafting: false) + super.init(context: context, component: GiftStoreScreenComponent( context: context, + resaleGiftsContext: resaleGiftsContext, overNavigationContainer: self.overNavigationContainer, starsContext: starsContext, peerId: peerId, diff --git a/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD new file mode 100644 index 0000000000..87a0e849f6 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "GiftUnpinScreen", + module_name = "GiftUnpinScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/Components/SheetComponent", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/Sources/GiftUnpinScreen.swift similarity index 96% rename from submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift rename to submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/Sources/GiftUnpinScreen.swift index 761d7f8d95..8d1b1578b2 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftUnpinScreen/Sources/GiftUnpinScreen.swift @@ -4,8 +4,6 @@ import Display import ComponentFlow import SwiftSignalKit import TelegramCore -import Markdown -import TextFormat import TelegramPresentationData import ViewControllerComponent import SheetComponent @@ -86,14 +84,14 @@ private final class SheetContent: CombinedComponent { let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor - var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + var contentSize = CGSize(width: context.availableSize.width, height: 20.0) let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -104,11 +102,11 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0)) + .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 38.0)) ) let title = title.update( @@ -239,6 +237,7 @@ private final class SheetContent: CombinedComponent { } contentSize.height += 14.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( @@ -265,7 +264,7 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), transition: context.transition ) context.add(button @@ -273,11 +272,8 @@ private final class SheetContent: CombinedComponent { .cornerRadius(10.0) ) contentSize.height += button.size.height - contentSize.height += 7.0 + contentSize.height += buttonInsets.bottom - let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom - contentSize.height += 5.0 + effectiveBottomInset - appliedSelectedGift = state.selectedGift return contentSize diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 5d86b40c9c..3f8e64ed20 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -70,6 +70,8 @@ swift_library( "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTableComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent", + "//submodules/TelegramUI/Components/GlassControls", + "//submodules/Components/ResizableSheetComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift index bbf99fe9a0..79a62599d8 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift @@ -467,10 +467,10 @@ private final class GiftAuctionAcquiredScreenComponent: Component { let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -485,7 +485,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index c7004ea02e..ace4c3a192 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -382,10 +382,10 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -400,7 +400,7 @@ private final class GiftAuctionActiveBidsScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 3155acb8ad..5f2257cced 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -2682,10 +2682,10 @@ private final class GiftAuctionBidScreenComponent: Component { let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -2700,7 +2700,7 @@ private final class GiftAuctionBidScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: rawSideInset + 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { @@ -2713,10 +2713,10 @@ private final class GiftAuctionBidScreenComponent: Component { let moreButtonSize = self.moreButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "info", component: AnyComponent( LottieComponent( content: LottieComponent.AppBundleContent( @@ -2736,7 +2736,7 @@ private final class GiftAuctionBidScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let infoButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - rawSideInset - 16.0 - moreButtonSize.width, y: 16.0), size: moreButtonSize) if let infoButtonView = self.moreButton.view { @@ -2784,7 +2784,7 @@ private final class GiftAuctionBidScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: isUpcoming ? 27.0 : 19.0), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: isUpcoming ? 29.0 : 21.0), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { self.navigationBarContainer.addSubview(titleView) @@ -2792,7 +2792,7 @@ private final class GiftAuctionBidScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: 40.0), size: subtitleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - subtitleSize.width) * 0.5), y: 42.0), size: subtitleSize) if let subtitleView = self.subtitle.view { if subtitleView.superview == nil { self.navigationBarContainer.addSubview(subtitleView) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift index b4e39c8b51..cd05f30e7b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift @@ -249,10 +249,10 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -266,7 +266,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { state.dismiss(animated: true) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index fd9ee199bc..777b3f5aa4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -1219,7 +1219,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, isDark: false, state: .tintedGlass, @@ -1236,7 +1236,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { state.dismiss(animated: true) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(closeButton @@ -1245,7 +1245,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let moreButton = moreButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, isDark: false, state: .tintedGlass, @@ -1267,7 +1267,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { moreButtonPlayOnce.invoke(Void()) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: context.transition ) context.add(moreButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift index f29f6d3e6a..ce2128889d 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionWearPreviewScreen.swift @@ -301,7 +301,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), + size: CGSize(width: 44.0, height: 44.0), backgroundColor: buttonColor, isDark: false, state: .tintedGlass, @@ -315,7 +315,7 @@ private final class GiftAuctionWearPreviewSheetContent: CombinedComponent { (controller() as? GiftAuctionWearPreviewScreen)?.dismissAnimated() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift index d2cc73af32..603c4f3692 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftPurchaseAlertController.swift @@ -21,12 +21,16 @@ import TooltipUI import AlertComponent import AlertTransferHeaderComponent import AvatarComponent +import AlertTableComponent +import TableComponent public func giftPurchaseAlertController( context: AccountContext, gift: StarGift.UniqueGift, + showAttributes: Bool, peer: EnginePeer, animateBalanceOverlay: Bool = false, + autoDismissOnCommit: Bool = true, navigationController: NavigationController?, commit: @escaping (CurrencyAmount.Currency) -> Void, dismissed: @escaping () -> Void @@ -39,6 +43,8 @@ public func giftPurchaseAlertController( currencyPromise.set(.ton) } + var showAttributeInfoImpl: ((Any, String) -> Void)? + let contentSignal = currencyPromise.get() |> map { currency in var content: [AnyComponentWithIdentity] = [] @@ -126,9 +132,104 @@ public func giftPurchaseAlertController( AlertTextComponent(content: .plain(text)) ) )) + + if showAttributes { + let tableFont = Font.regular(15.0) + let tableTextColor = presentationData.theme.list.itemPrimaryTextColor + + let modelButtonTag = GenericComponentViewTag() + let backdropButtonTag = GenericComponentViewTag() + let symbolButtonTag = GenericComponentViewTag() + + var tableItems: [TableComponent.Item] = [] + let order: [StarGift.UniqueGift.Attribute.AttributeType] = [ + .model, .pattern, .backdrop, .originalInfo + ] + + var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:] + for attribute in gift.attributes { + attributeMap[attribute.attributeType] = attribute + } + + for type in order { + if let attribute = attributeMap[type] { + let id: String? + let title: String? + let value: NSAttributedString + let percentage: Float? + let tag: AnyObject? + + switch attribute { + case let .model(name, _, rarity, _): + id = "model" + title = strings.Gift_Unique_Model + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity.permilleValue) * 0.1 + tag = modelButtonTag + case let .backdrop(name, _, _, _, _, _, rarity): + id = "backdrop" + title = strings.Gift_Unique_Backdrop + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity.permilleValue) * 0.1 + tag = backdropButtonTag + case let .pattern(name, _, rarity): + id = "pattern" + title = strings.Gift_Unique_Symbol + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity.permilleValue) * 0.1 + tag = symbolButtonTag + case .originalInfo: + continue + } + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + MultilineTextComponent(text: .plain(value)) + ) + ) + ) + if let percentage, let tag { + items.append(AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: context, + text: formatPercentage(percentage), + color: presentationData.theme.list.itemAccentColor + )), + action: { + showAttributeInfoImpl?(tag, strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string) + } + ).tagged(tag)) + )) + } + let itemComponent = AnyComponent( + HStack(items, spacing: 4.0) + ) + + tableItems.append(.init( + id: id, + title: title, + hasBackground: false, + component: itemComponent + )) + } + } + content.append(AnyComponentWithIdentity( + id: "table", + component: AnyComponent( + AlertTableComponent(items: tableItems) + ) + )) + } + return content } + let actionProgress = ValuePromise(false) let actionsSignal = currencyPromise.get() |> map { currency in var actions: [AlertScreen.Action] = [] @@ -144,8 +245,11 @@ public func giftPurchaseAlertController( } } actions.append(.init(id: "buy", title: buyString, type: .default, action: { + if !autoDismissOnCommit { + actionProgress.set(true) + } commit(currency) - })) + }, autoDismiss: autoDismissOnCommit, progress: actionProgress.get())) actions.append(.init(title: strings.Common_Cancel)) return actions } @@ -166,6 +270,36 @@ public func giftPurchaseAlertController( alertController.dismissed = { _ in dismissed() } + + var dismissAllTooltipsImpl: (() -> Void)? + showAttributeInfoImpl = { [weak alertController] tag, text in + dismissAllTooltipsImpl?() + guard let alertController, let sourceView = alertController.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: alertController.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: context.account, sharedContext: context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + alertController.present(tooltipController, in: .current) + } + dismissAllTooltipsImpl = { [weak alertController] in + guard let alertController else { + return + } + alertController.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + alertController.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } // if !gift.resellForTonOnly { // Queue.mainQueue().after(0.3) { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift index e3c7712fb4..84680a9b19 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeCostScreen.swift @@ -18,6 +18,9 @@ import ProfileLevelRatingBarComponent import TextFormat import TelegramStringFormatting import TableComponent +import ResizableSheetComponent +import GlassBarButtonComponent +import BundleIconComponent private final class GiftUpgradeCostScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -36,121 +39,23 @@ private final class GiftUpgradeCostScreenComponent: Component { static func ==(lhs: GiftUpgradeCostScreenComponent, rhs: GiftUpgradeCostScreenComponent) -> Bool { return true } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - } - } - - final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let navigationBackgroundView: BlurredBackgroundView - private let navigationBarSeparator: SimpleLayer - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - - private let closeButton = ComponentView() - - private let title = ComponentView() + + final class View: UIView { private let descriptionText = ComponentView() private let bar = ComponentView() private let table = ComponentView() private let additionalDescription = ComponentView() - - private let bottomPanelContainer: UIView - private let bottomPanelSeparator: SimpleLayer - private let actionButton = ComponentView() - - private var isFirstTimeApplyingModalFactor: Bool = true - private var ignoreScrolling: Bool = false - + private var component: GiftUpgradeCostScreenComponent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? private var isUpdating: Bool = false - private var itemLayout: ItemLayout? - private var topOffsetDistance: CGFloat? - - private var cachedCloseImage: UIImage? - private var upgradePreviewTimer: SwiftSignalKit.Timer? private var effectiveUpgradePrice: StarGiftUpgradePreview.Price? override init(frame: CGRect) { - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.navigationBarSeparator = SimpleLayer() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - - self.bottomPanelContainer = UIView() - self.bottomPanelSeparator = SimpleLayer() - super.init(frame: frame) - - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - self.scrollView.contentInsetAdjustmentBehavior = .never - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.addSubview(self.navigationBarContainer) - self.addSubview(self.bottomPanelContainer) - - self.navigationBarContainer.addSubview(self.navigationBackgroundView) - self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) - - self.layer.addSublayer(self.bottomPanelSeparator) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { @@ -160,40 +65,6 @@ private final class GiftUpgradeCostScreenComponent: Component { deinit { } - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - func upgradePreviewTimerTick() { guard let upgradePreview = self.component?.upgradePreview else { return @@ -219,83 +90,6 @@ private final class GiftUpgradeCostScreenComponent: Component { } } - private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - - let titleTransformFraction: CGFloat = max(0.0, min(1.0, -topOffset / 20.0)) - - let navigationAlpha: CGFloat = titleTransformFraction - transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) - transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) - - let bottomPanelAlphaDistance: CGFloat = 20.0 - let bottomPanelDistance: CGFloat = self.scrollView.contentSize.height - self.scrollView.bounds.maxY - let bottomPanelAlphaFraction: CGFloat = max(0.0, min(1.0, bottomPanelDistance / bottomPanelAlphaDistance)) - - let bottomPanelAlpha: CGFloat = bottomPanelAlphaFraction - if self.bottomPanelSeparator.opacity != Float(bottomPanelAlpha) { - let alphaTransition = transition - alphaTransition.setAlpha(layer: self.bottomPanelSeparator, alpha: bottomPanelAlpha) - } - - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = 80.0 - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - var modalOverlayTransition = transition - if self.isFirstTimeApplyingModalFactor { - self.isFirstTimeApplyingModalFactor = false - modalOverlayTransition = .spring(duration: 0.5) - } - if self.isUpdating { - DispatchQueue.main.async { [weak controller] in - guard let controller else { - return - } - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) - } - } else { - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) - } - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomPanelSeparator.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomPanelSeparator.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - - if let environment = self.environment, let controller = environment.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - func update(component: GiftUpgradeCostScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -303,10 +97,7 @@ private final class GiftUpgradeCostScreenComponent: Component { } let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - + let sideInset: CGFloat = 16.0 + environment.safeInsets.left let isFirstTime = self.component == nil @@ -325,74 +116,8 @@ private final class GiftUpgradeCostScreenComponent: Component { } } - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor - - self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - self.bottomPanelSeparator.backgroundColor = environment.theme.rootController.tabBar.separatorColor.cgColor - } + var contentHeight: CGFloat = 56.0 - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - - var contentHeight: CGFloat = 0.0 - - let closeImage: UIImage - if let image = self.cachedCloseImage, !themeUpdated { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))! - self.cachedCloseImage = closeImage - } - - let closeButtonSize = self.closeButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image(image: closeImage, size: closeImage.size)), - action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - } - ).minSize(CGSize(width: 62.0, height: 56.0))), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize) - if let closeButtonView = self.closeButton.view { - if closeButtonView.superview == nil { - self.navigationBarContainer.addSubview(closeButtonView) - } - transition.setFrame(view: closeButtonView, frame: closeButtonFrame) - } - - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - - let clippingY: CGFloat - - let titleSize = self.title.update( - transition: transition, - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_UpgradeCost_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } - contentHeight += 56.0 - - let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) - transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) - self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - var value: CGFloat = 0.0 if let startStars = component.upgradePreview.prices.first?.stars, let endStars = component.upgradePreview.prices.last?.stars { let effectiveValue = self.effectiveUpgradePrice?.stars ?? endStars @@ -420,7 +145,7 @@ private final class GiftUpgradeCostScreenComponent: Component { let barFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - barSize.width) * 0.5), y: contentHeight), size: barSize) if let barView = self.bar.view { if barView.superview == nil { - self.scrollContentView.addSubview(barView) + self.addSubview(barView) } transition.setFrame(view: barView, frame: barFrame) } @@ -445,7 +170,7 @@ private final class GiftUpgradeCostScreenComponent: Component { let descriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize) if let descriptionView = self.descriptionText.view { if descriptionView.superview == nil { - self.scrollContentView.addSubview(descriptionView) + self.addSubview(descriptionView) } transition.setFrame(view: descriptionView, frame: descriptionFrame) } @@ -484,7 +209,7 @@ private final class GiftUpgradeCostScreenComponent: Component { let tableFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - tableSize.width) * 0.5), y: contentHeight), size: tableSize) if let tableView = self.table.view { if tableView.superview == nil { - self.scrollContentView.addSubview(tableView) + self.addSubview(tableView) } transition.setFrame(view: tableView, frame: tableFrame) } @@ -509,14 +234,87 @@ private final class GiftUpgradeCostScreenComponent: Component { let additionalDescriptionFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - additionalDescriptionSize.width) * 0.5), y: contentHeight), size: additionalDescriptionSize) if let additionalDescriptionView = self.additionalDescription.view { if additionalDescriptionView.superview == nil { - self.scrollContentView.addSubview(additionalDescriptionView) + self.addSubview(additionalDescriptionView) } transition.setFrame(view: additionalDescriptionView, frame: additionalDescriptionFrame) } contentHeight += additionalDescriptionSize.height + 15.0 + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + contentHeight += 52.0 + contentHeight += buttonInsets.bottom + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} - let actionButtonTitle: String = environment.strings.Gift_UpgradeCost_Done +private final class SheetContainerComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let upgradePreview: StarGiftUpgradePreview + + init( + context: AccountContext, + upgradePreview: StarGiftUpgradePreview + ) { + self.context = context + self.upgradePreview = upgradePreview + } + + static func ==(lhs: SheetContainerComponent, rhs: SheetContainerComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.upgradePreview != rhs.upgradePreview { + return false + } + return true + } + + final class State: ComponentState { + } + + func makeState() -> State { + return State() + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let component = context.component + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme + + let backgroundColor = environment.theme.list.modalPlainBackgroundColor var buttonTitle: [AnyComponentWithIdentity] = [] let playButtonAnimation = ActionSlot() @@ -528,106 +326,91 @@ private final class GiftUpgradeCostScreenComponent: Component { playOnce: playButtonAnimation )))) buttonTitle.append(AnyComponentWithIdentity(id: 1, component: AnyComponent(ButtonTextContentComponent( - text: actionButtonTitle, + text: environment.strings.Gift_UpgradeCost_Done, badge: 0, textColor: environment.theme.list.itemCheckColors.foregroundColor, badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, badgeForeground: environment.theme.list.itemCheckColors.fillColor )))) - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent( + GiftUpgradeCostScreenComponent( + context: component.context, + upgradePreview: component.upgradePreview + ) ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + titleItem: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_UpgradeCost_Title, font: Font.semibold(17.0), textColor: environment.theme.actionSheet.primaryTextColor))) ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self else { - return + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + rightItem: nil, + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + ), + action: { + dismiss(true) + } + ) + ), + backgroundColor: .color(backgroundColor), + isFullscreen: false, + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) } - self.environment?.controller()?.dismiss() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + }, + availableSize: context.availableSize, + transition: context.transition ) - let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height - - let bottomPanelSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: UIScreenPixel)) - transition.setFrame(layer: self.bottomPanelSeparator, frame: bottomPanelSeparatorFrame) - - let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) - transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) - - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.bottomPanelContainer.addSubview(actionButtonView) - playButtonAnimation.invoke(Void()) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - - contentHeight += bottomPanelHeight - - clippingY = bottomPanelFrame.minY - 8.0 - - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - let previousBounds = self.scrollView.bounds - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } else { - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) - - return availableSize + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize } } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } } public class GiftUpgradeCostScreen: ViewControllerComponentContainer { @@ -640,7 +423,7 @@ public class GiftUpgradeCostScreen: ViewControllerComponentContainer { ) { self.context = context - super.init(context: context, component: GiftUpgradeCostScreenComponent( + super.init(context: context, component: SheetContainerComponent( context: context, upgradePreview: upgradePreview ), navigationBarAppearance: .none, theme: .default) @@ -656,30 +439,4 @@ public class GiftUpgradeCostScreen: ViewControllerComponentContainer { deinit { } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View { - componentView.animateIn() - } - } - - override public func dismiss(completion: (() -> Void)? = nil) { - if !self.isDismissed { - self.isDismissed = true - - if let componentView = self.node.hostView.componentView as? GiftUpgradeCostScreenComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) - } else { - self.dismiss(animated: false) - } - } - } } - diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift index a8abe1e9e1..8c1535be94 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradeVariantsScreen.swift @@ -1356,7 +1356,11 @@ private final class AttributeInfoComponent: Component { //TODO:localize switch rarity { case let .permille(value): - badgeString = formatPercentage(Float(value) * 0.1) + if value == 0 { + badgeString = "<\(formatPercentage(0.1))" + } else { + badgeString = formatPercentage(Float(value) * 0.1) + } case .epic: badgeString = "epic" case .legendary: diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index 77376bdd9e..8a149baec1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -634,10 +634,10 @@ private final class GiftValueSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -651,7 +651,7 @@ private final class GiftValueSheetContent: CombinedComponent { state.dismiss(animated: true) } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift new file mode 100644 index 0000000000..48cbb81e84 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewBuyGift.swift @@ -0,0 +1,250 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import PresentationDataUtils +import TelegramStringFormatting +import BalanceNeededScreen + +public func buyStarGiftImpl( + context: AccountContext, + recipientPeerId: EnginePeer.Id, + uniqueGift: StarGift.UniqueGift, + showAttributes: Bool, + acceptedPrice: CurrencyAmount? = nil, + skipConfirmation: Bool = false, + starsTopUpOptions: Signal<[StarsTopUpOption]?, NoError>, + buyGift: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal)?, + getController: @escaping () -> ViewController?, + updateProgress: @escaping (Bool) -> Void, + updateIsBalanceVisible: @escaping (Bool) -> Void, + completion: @escaping () -> Void +) { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let action: (CurrencyAmount.Currency, @escaping () -> Void) -> Void = { currency, beforeCompletion in + guard let resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == currency }) else { + guard let controller = getController() else { + return + } + let alertController = textAlertController( + context: context, + title: nil, + text: presentationData.strings.Gift_Buy_ErrorUnknown, + actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], + parseMarkdown: true + ) + controller.present(alertController, in: .window(.root)) + return + } + + let proceed: () -> Void = { + updateProgress(true) + + let buyGiftImpl: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal) + if let buyGift { + buyGiftImpl = { slug, peerId, price in + return buyGift(slug, peerId, price) + } + } else { + buyGiftImpl = { slug, peerId, price in + return context.engine.payments.buyStarGift(slug: slug, peerId: peerId, price: price) + } + } + + let finalPrice = acceptedPrice ?? resellAmount + let _ = (buyGiftImpl(uniqueGift.slug, recipientPeerId, finalPrice) + |> deliverOnMainQueue).start(error: { error in + guard let controller = getController() else { + return + } + beforeCompletion() + updateProgress(false) + + HapticFeedback().error() + + switch error { + case .serverProvided: + return + case let .priceChanged(newPrice): + let errorTitle = presentationData.strings.Gift_Buy_ErrorPriceChanged_Title + let originalPriceString: String + switch resellAmount.currency { + case .stars: + originalPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: resellAmount.amount.value)) + case .ton: + originalPriceString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil) + " TON" + } + + let newPriceString: String + let buttonText: String + switch newPrice.currency { + case .stars: + newPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: newPrice.amount.value)) + buttonText = presentationData.strings.Gift_Buy_Confirm_BuyFor(Int32(newPrice.amount.value)) + case .ton: + let tonValueString = formatTonAmountText(newPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil) + newPriceString = tonValueString + " TON" + buttonText = presentationData.strings.Gift_Buy_Confirm_BuyForTon(tonValueString).string + } + let errorText = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text(originalPriceString, newPriceString).string + + let alertController = textAlertController( + context: context, + title: errorTitle, + text: errorText, + actions: [ + TextAlertAction(type: .defaultAction, title: buttonText, action: { + buyStarGiftImpl( + context: context, + recipientPeerId: recipientPeerId, + uniqueGift: uniqueGift, + showAttributes: showAttributes, + acceptedPrice: newPrice, + skipConfirmation: true, + starsTopUpOptions: starsTopUpOptions, + buyGift: buyGift, + getController: getController, + updateProgress: updateProgress, + updateIsBalanceVisible: updateIsBalanceVisible, + completion: completion + ) + }), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}) + ], + actionLayout: .vertical, + parseMarkdown: true + ) + controller.present(alertController, in: .window(.root)) + default: + let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) + controller.present(alertController, in: .window(.root)) + } + }, + completed: { + beforeCompletion() + completion() + + Queue.mainQueue().after(2.5) { + switch finalPrice.currency { + case .stars: + context.starsContext?.load(force: true) + case .ton: + context.tonContext?.load(force: true) + } + } + }) + } + + if resellAmount.currency == .stars, let starsContext = context.starsContext, let starsState = context.starsContext?.currentState, starsState.balance < resellAmount.amount { + let _ = (starsTopUpOptions + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { options in + guard let controller = getController() else { + return + } + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .buyStarGift(requiredStars: resellAmount.amount.value), + targetPeerId: nil, + customTheme: nil, + completion: { stars in + guard let starsContext = context.starsContext else { + return + } + updateProgress(true) + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { + Queue.mainQueue().after(0.1, { + guard let starsContext = context.starsContext, let starsState = starsContext.currentState else { + return + } + if starsState.balance < resellAmount.amount { + updateProgress(false) + + buyStarGiftImpl( + context: context, + recipientPeerId: recipientPeerId, + uniqueGift: uniqueGift, + showAttributes: showAttributes, + skipConfirmation: true, + starsTopUpOptions: starsTopUpOptions, + buyGift: buyGift, + getController: getController, + updateProgress: updateProgress, + updateIsBalanceVisible: updateIsBalanceVisible, + completion: completion + ) + } else { + proceed() + } + }); + }) + } + ) + controller.push(purchaseController) + }) + } else if resellAmount.currency == .ton, let tonState = context.tonContext?.currentState, tonState.balance < resellAmount.amount { + guard let controller = getController() else { + return + } + let needed = resellAmount.amount - tonState.balance + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + controller.push(BalanceNeededScreen( + context: context, + amount: needed, + buttonAction: { + context.sharedContext.applicationBindings.openUrl(fragmentUrl) + } + )) + } else { + proceed() + } + } + + if skipConfirmation { + action(acceptedPrice?.currency ?? .stars, {}) + } else { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer, let controller = getController() else { + return + } + var dismissImpl: (() -> Void)? + let alertController = giftPurchaseAlertController( + context: context, + gift: uniqueGift, + showAttributes: showAttributes, + peer: peer, + animateBalanceOverlay: showAttributes, + autoDismissOnCommit: !showAttributes, + navigationController: controller.navigationController as? NavigationController, + commit: { currency in + action(currency, { + dismissImpl?() + }) + }, + dismissed: { + updateIsBalanceVisible(true) + } + ) + controller.present(alertController, in: .window(.root)) + + dismissImpl = { [weak alertController] in + alertController?.dismiss(animated: true) + } + + updateIsBalanceVisible(false) + }) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 12561d0d3d..0d61acada3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -44,6 +44,8 @@ import ChatMessagePaymentAlertController import TableComponent import PeerTableCellComponent import AvatarComponent +import GlassControls +import GlassBarButtonComponent private final class GiftViewSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -76,6 +78,7 @@ private final class GiftViewSheetContent: CombinedComponent { } final class State: ComponentState { + let controlButtonsTag = GenericComponentViewTag() let modelButtonTag = GenericComponentViewTag() let backdropButtonTag = GenericComponentViewTag() let symbolButtonTag = GenericComponentViewTag() @@ -1094,6 +1097,48 @@ private final class GiftViewSheetContent: CombinedComponent { controller.present(alertController, in: .window(.root)) } + func craftGift() { + guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift else { + return + } + + guard gift.hostPeerId == nil else { + self.presentActionLockedForHostedGift(gift: gift) + return + } + + controller.dismissAllTooltips() + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let canCraftDate = arguments.canCraftDate, currentTime < canCraftDate { + let dateString = stringForFullDate(timestamp: canCraftDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + //TODO:localize + let alertController = textAlertController( + context: self.context, + title: "Try Later", + text: "You will be able to craft this gift on \(dateString).", + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + controller.present(alertController, in: .window(.root)) + return + } + + if let navigationController = controller.navigationController as? NavigationController { + controller.dismissAnimated() + + let craftScreen = self.context.sharedContext.makeGiftCraftScreen( + context: self.context, + gift: gift + ) + navigationController.pushViewController(craftScreen) + } + } + func transferGift() { guard let arguments = self.subject.arguments, let controller = self.getController() as? GiftViewScreen, case let .unique(gift) = arguments.gift, let reference = arguments.reference, let transferStars = arguments.transferStars else { return @@ -1403,7 +1448,15 @@ private final class GiftViewSheetContent: CombinedComponent { controller.present(tooltipController, in: .current) } - func openMore(node: ASDisplayNode, gesture: ContextGesture?) { + func openMore() { + guard let controller = self.getController() as? GiftViewScreen else { + return + } + guard let controlsView = controller.node.hostView.findTaggedView(tag: self.controlButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = controlsView.rightItemView, let sourceView = rightItemView.itemView(id: AnyHashable("more")) else { + return + } + + guard let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else { return } @@ -1443,7 +1496,7 @@ private final class GiftViewSheetContent: CombinedComponent { }) }))) } - + if case let .unique(gift) = arguments.gift, let resellAmount = gift.resellAmounts?.first, resellAmount.amount.value > 0 { if arguments.reference != nil || gift.owner.peerId == context.account.peerId { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in @@ -1507,7 +1560,7 @@ private final class GiftViewSheetContent: CombinedComponent { }))) } } - + if let _ = arguments.resellAmounts, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ViewInProfile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor) @@ -1530,7 +1583,7 @@ private final class GiftViewSheetContent: CombinedComponent { }))) } - let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + let contextController = makeContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) controller.presentInGlobalOverlay(contextController) }) } @@ -1677,17 +1730,15 @@ private final class GiftViewSheetContent: CombinedComponent { } func commitBuy(acceptedPrice: CurrencyAmount? = nil, skipConfirmation: Bool = false) { - guard case let .unique(uniqueGift) = self.subject.arguments?.gift else { + guard case let .unique(uniqueGift) = self.subject.arguments?.gift, let controller = self.getController() as? GiftViewScreen else { return } - + let context = self.context - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))" if let resellTooEarlyTimestamp = self.resellTooEarlyTimestamp { - guard let controller = self.getController() else { - return - } let dateString = stringForFullDate(timestamp: resellTooEarlyTimestamp, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) let alertController = textAlertController( context: context, @@ -1702,287 +1753,103 @@ private final class GiftViewSheetContent: CombinedComponent { return } - let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: presentationData.dateTimeFormat))" - let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId - - let action: (CurrencyAmount.Currency) -> Void = { currency in - guard let resellAmount = uniqueGift.resellAmounts?.first(where: { $0.currency == currency }) else { - guard let controller = self.getController() as? GiftViewScreen else { - return - } - let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) - controller.present(alertController, in: .window(.root)) - return - } - - let proceed: () -> Void = { - guard let controller = self.getController() as? GiftViewScreen else { - return - } - - self.inProgress = true - self.updated() - - let buyGiftImpl: ((String, EnginePeer.Id, CurrencyAmount?) -> Signal) - if let buyGift = controller.buyGift { - buyGiftImpl = { slug, peerId, price in - return buyGift(slug, peerId, price) - } - } else { - buyGiftImpl = { slug, peerId, price in - return self.context.engine.payments.buyStarGift(slug: slug, peerId: peerId, price: price) - } - } - - let finalPrice = acceptedPrice ?? resellAmount - self.buyDisposable = (buyGiftImpl(uniqueGift.slug, recipientPeerId, finalPrice) - |> deliverOnMainQueue).start( - error: { [weak self] error in - guard let self, let controller = self.getController() else { - return - } - - self.inProgress = false - self.updated() - - HapticFeedback().error() - - switch error { - case .serverProvided: - return - case let .priceChanged(newPrice): - let errorTitle = presentationData.strings.Gift_Buy_ErrorPriceChanged_Title - let originalPriceString: String - switch resellAmount.currency { - case .stars: - originalPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: resellAmount.amount.value)) - case .ton: - originalPriceString = formatTonAmountText(resellAmount.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil) + " TON" - } - - let newPriceString: String - let buttonText: String - switch newPrice.currency { - case .stars: - newPriceString = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text_Stars(Int32(clamping: newPrice.amount.value)) - buttonText = presentationData.strings.Gift_Buy_Confirm_BuyFor(Int32(newPrice.amount.value)) - case .ton: - let tonValueString = formatTonAmountText(newPrice.amount.value, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: nil) - newPriceString = tonValueString + " TON" - buttonText = presentationData.strings.Gift_Buy_Confirm_BuyForTon(tonValueString).string - } - let errorText = presentationData.strings.Gift_Buy_ErrorPriceChanged_Text(originalPriceString, newPriceString).string - - let alertController = textAlertController( - context: context, - title: errorTitle, - text: errorText, - actions: [ - TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak self] in - guard let self else { - return - } - self.commitBuy(acceptedPrice: newPrice, skipConfirmation: true) - }), - TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - }) - ], - actionLayout: .vertical, - parseMarkdown: true - ) - controller.present(alertController, in: .window(.root)) - default: - let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) - controller.present(alertController, in: .window(.root)) - } - }, - completed: { [weak self] in - guard let self, let controller = self.getController() as? GiftViewScreen else { - return - } - self.inProgress = false - - var animationFile: TelegramMediaFile? - for attribute in uniqueGift.attributes { - if case let .model(_, file, _, _) = attribute { - animationFile = file - break - } - } - - if let navigationController = controller.navigationController as? NavigationController { - if recipientPeerId == self.context.account.peerId { - controller.dismissAnimated() - - navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) - - Queue.mainQueue().after(0.5, { - if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil), - elevatedLayout: !(lastController is ChatController), - action: { _ in - return true - } - ) - lastController.present(resultController, in: .current) - } - }) - } else { - var controllers = Array(navigationController.viewControllers.prefix(1)) - let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) - chatController.hintPlayNextOutgoingGift() - controllers.append(chatController) - navigationController.setViewControllers(controllers, animated: true) - - Queue.mainQueue().after(0.5, { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) - |> deliverOnMainQueue).start(next: { [weak navigationController] peer in - if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile { - let resultController = UndoOverlayController( - presentationData: presentationData, - content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil), - elevatedLayout: !(lastController is ChatController), - action: { _ in - return true - } - ) - lastController.present(resultController, in: .current) - } - }) - }) - } - } - - self.updated(transition: .spring(duration: 0.4)) - - Queue.mainQueue().after(2.5) { - switch finalPrice.currency { - case .stars: - context.starsContext?.load(force: true) - case .ton: - context.tonContext?.load(force: true) - } - } - } - ) - } - - - if let _ = self.buyForm { - if resellAmount.currency == .stars, let starsContext = context.starsContext, let starsState = context.starsContext?.currentState, starsState.balance < resellAmount.amount { - if self.starsTopUpOptions.isEmpty { - self.inProgress = true - self.updated() - } - let _ = (self.starsTopUpOptionsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] options in - guard let self, let controller = self.getController() else { - return - } - let purchaseController = context.sharedContext.makeStarsPurchaseScreen( - context: context, - starsContext: starsContext, - options: options ?? [], - purpose: .buyStarGift(requiredStars: resellAmount.amount.value), - targetPeerId: nil, - customTheme: nil, - completion: { [weak self, weak starsContext] stars in - guard let self, let starsContext else { - return - } - self.inProgress = true - self.updated() - - starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) - let _ = (starsContext.onUpdate - |> deliverOnMainQueue).start(next: { [weak self] in - guard let self else { - return - } - Queue.mainQueue().after(0.1, { [weak self] in - guard let self, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { - return - } - if starsState.balance < resellAmount.amount { - self.inProgress = false - self.updated() - - self.commitBuy(skipConfirmation: true) - } else { - proceed() - } - }); - }) - } - ) - controller.push(purchaseController) - }) - } else if resellAmount.currency == .ton, let tonState = context.tonContext?.currentState, tonState.balance < resellAmount.amount { - guard let controller = self.getController() else { - return - } - let needed = resellAmount.amount - tonState.balance - var fragmentUrl = "https://fragment.com/ads/topup" - if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { - fragmentUrl = value - } - controller.push(BalanceNeededScreen( - context: self.context, - amount: needed, - buttonAction: { [weak self] in - guard let self else { - return - } - self.context.sharedContext.applicationBindings.openUrl(fragmentUrl) - } - )) - } else { - proceed() - } - } else { - guard let controller = self.getController() else { - return - } - let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.Gift_Buy_ErrorUnknown, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true) - controller.present(alertController, in: .window(.root)) - } + guard let _ = self.buyForm else { + let alertController = textAlertController( + context: context, + title: nil, + text: presentationData.strings.Gift_Buy_ErrorUnknown, + actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], + parseMarkdown: true + ) + controller.present(alertController, in: .window(.root)) + return } - if skipConfirmation { - action(acceptedPrice?.currency ?? .stars) - } else { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { + let recipientPeerId = self.recipientPeerId ?? self.context.account.peerId + buyStarGiftImpl( + context: self.context, + recipientPeerId: recipientPeerId, + uniqueGift: uniqueGift, + showAttributes: false, + acceptedPrice: acceptedPrice, + skipConfirmation: skipConfirmation, + starsTopUpOptions: self.starsTopUpOptionsPromise.get(), + buyGift: controller.buyGift, + getController: self.getController, + updateProgress: { [weak self] progress in + guard let self else { return } - if let controller = self.getController() as? GiftViewScreen { - let alertController = giftPurchaseAlertController( - context: self.context, - gift: uniqueGift, - peer: peer, - navigationController: controller.navigationController as? NavigationController, - commit: { currency in - action(currency) - }, - dismissed: { [weak controller] in - if let balanceView = controller?.balanceOverlay.view { - balanceView.isHidden = false - } - } - ) - controller.present(alertController, in: .window(.root)) - - if let balanceView = controller.balanceOverlay.view { - balanceView.isHidden = true + self.inProgress = progress + self.updated() + }, + updateIsBalanceVisible: { [weak controller] isVisible in + guard let controller else { + return + } + if let balanceView = controller.balanceOverlay.view { + balanceView.isHidden = !isVisible + } + }, + completion: { [weak controller] in + guard let controller else { + return + } + + var animationFile: TelegramMediaFile? + for attribute in uniqueGift.attributes { + if case let .model(_, file, _, _) = attribute { + animationFile = file + break } } - }) - } + + if let navigationController = controller.navigationController as? NavigationController { + if recipientPeerId == context.account.peerId { + controller.dismissAnimated() + + navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) + + Queue.mainQueue().after(0.5, { + if let lastController = navigationController.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil), + elevatedLayout: !(lastController is ChatController), + action: { _ in + return true + } + ) + lastController.present(resultController, in: .current) + } + }) + } else { + var controllers = Array(navigationController.viewControllers.prefix(1)) + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil) + chatController.hintPlayNextOutgoingGift() + controllers.append(chatController) + navigationController.setViewControllers(controllers, animated: true) + + Queue.mainQueue().after(0.5, { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: recipientPeerId)) + |> deliverOnMainQueue).start(next: { [weak navigationController] peer in + if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile { + let resultController = UndoOverlayController( + presentationData: presentationData, + content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil), + elevatedLayout: !(lastController is ChatController), + action: { _ in + return true + } + ) + lastController.present(resultController, in: .current) + } + }) + }) + } + } + } + ) } func skipAnimation() { @@ -2522,7 +2389,7 @@ private final class GiftViewSheetContent: CombinedComponent { static var body: Body { let priceButton = Child(PlainButtonComponent.self) - let buttons = Child(ButtonsComponent.self) + let buttons = Child(GlassControlPanelComponent.self) let animation = Child(GiftCompositionComponent.self) let title = Child(MultilineTextComponent.self) let subtitle = Child(MultilineTextComponent.self) @@ -2549,7 +2416,7 @@ private final class GiftViewSheetContent: CombinedComponent { let upgradeNextButton = Child(PlainButtonComponent.self) let upgradeTitle = Child(MultilineTextComponent.self) - let upgradeDescription = Child(PlainButtonComponent.self) + let upgradeDescription = Child(GlassBarButtonComponent.self) let upgradePerks = Child(List.self) let upgradeKeepName = Child(PlainButtonComponent.self) let upgradePriceButton = Child(PlainButtonComponent.self) @@ -2708,40 +2575,6 @@ private final class GiftViewSheetContent: CombinedComponent { showWearPreview = true } - let buttons = buttons.update( - component: ButtonsComponent( - theme: theme, - isOverlay: showUpgradePreview || uniqueGift != nil, - showMoreButton: uniqueGift != nil && !showWearPreview, - closePressed: { [weak state] in - guard let state else { - return - } - if state.inWearPreview { - if let controller = controller() as? GiftViewScreen { - controller.dismissAllTooltips() - } - state.inWearPreview = false - state.updated(transition: .spring(duration: 0.4)) - } else if state.inUpgradePreview { - state.cancelUpgradePreview() - } else { - state.dismiss(animated: true) - } - }, - morePressed: { [weak state] node, gesture in - if state?.testUpgradeAnimation == true { - state?.requestUpgradePreview() - return - } - - state?.openMore(node: node, gesture: gesture) - } - ), - availableSize: CGSize(width: 30.0, height: 30.0), - transition: context.transition - ) - var originY: CGFloat = 0.0 let headerHeight: CGFloat @@ -3089,62 +2922,62 @@ private final class GiftViewSheetContent: CombinedComponent { .position(CGPoint(x: -10000.0, y: -10000.0)) ) + let variantsButtonSize = CGSize(width: variantsMeasureDescription.size.width + 87.0, height: 24.0) + let upgradeDescription = upgradeDescription.update( - component: PlainButtonComponent( - content: AnyComponent( - ZStack([ - AnyComponentWithIdentity(id: "background", component: AnyComponent( - FilledRoundedRectangleComponent(color: buttonColor, cornerRadius: .minEdge, smoothCorners: false) + component: GlassBarButtonComponent( + size: variantsButtonSize, + backgroundColor: buttonColor, + isDark: true, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "content", component: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "icon1", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant1, + isPlaceholder: false, + mode: .tableIcon + ) )), - AnyComponentWithIdentity(id: "label", component: AnyComponent(HStack([ - AnyComponentWithIdentity(id: "icon1", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant1, - isPlaceholder: false, - mode: .tableIcon - ) - )), - AnyComponentWithIdentity(id: "icon2", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant2, - isPlaceholder: false, - mode: .tableIcon - ) - )), - AnyComponentWithIdentity(id: "icon3", component: AnyComponent( - GiftItemComponent( - context: component.context, - theme: theme, - strings: strings, - peer: nil, - subject: variant3, - isPlaceholder: false, - mode: .tableIcon - ) - )), - AnyComponentWithIdentity(id: "text", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .white))) - )), - AnyComponentWithIdentity(id: "arrow", component: AnyComponent( - BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) - )) - ], spacing: 3.0))) - ]) - ), - action: { [weak state] in + AnyComponentWithIdentity(id: "icon2", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant2, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "icon3", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: variant3, + isPlaceholder: false, + mode: .tableIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Upgrade_ViewAllVariants, font: Font.semibold(13.0), textColor: .white))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Item List/InlineTextRightArrow", tintColor: .white) + )) + ], spacing: 3.0) + )), + action: { [weak state] _ in state?.openUpgradeVariants() - }, - animateScale: false + } ), - availableSize: CGSize(width: variantsMeasureDescription.size.width + 87.0, height: 24.0), + availableSize: variantsButtonSize, transition: context.transition ) @@ -3288,7 +3121,15 @@ private final class GiftViewSheetContent: CombinedComponent { var hasDescriptionButton = false if let uniqueGift { titleString = uniqueGift.title - descriptionText = "\(strings.Gift_Unique_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" + var isCrafted = false + for attribute in uniqueGift.attributes { + if case let .model(_, _, _, crafted) = attribute { + isCrafted = crafted + } + } + //TODO:localize + let prefix: String = isCrafted ? "Crafted Collectible" : strings.Gift_Unique_Collectible + descriptionText = "\(prefix) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" if let releasedBy = uniqueGift.releasedBy, let peer = state.peerMap[releasedBy], let addressName = peer.addressName { descriptionText = strings.Gift_Unique_CollectibleBy("#\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))", "[@\(addressName)]()").string @@ -3649,6 +3490,8 @@ private final class GiftViewSheetContent: CombinedComponent { ) context.add(hiddenText .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) ) originY += hiddenText.size.height @@ -4254,7 +4097,11 @@ private final class GiftViewSheetContent: CombinedComponent { var badgeColor: UIColor = theme.list.itemAccentColor switch rarity { case let .permille(value): - badgeString = formatPercentage(Float(value) * 0.1) + if value == 0 { + badgeString = "<\(formatPercentage(0.1))" + } else { + badgeString = formatPercentage(Float(value) * 0.1) + } case .epic: badgeString = "epic" badgeColor = UIColor(rgb: 0xaf52de) @@ -5275,8 +5122,92 @@ private final class GiftViewSheetContent: CombinedComponent { originY += upgradePriceButton.size.height } + var buttonsBackground: GlassControlGroupComponent.Background = .panel + if let uniqueGift, let backdropAttribute = uniqueGift.attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, _, _, _, _) = backdropAttribute { + buttonsBackground = .color(UIColor(rgb: UInt32(bitPattern: innerColor)).withMultipliedBrightnessBy(1.05)) + } else if showUpgradePreview, let previewPatternColor = giftCompositionExternalState.previewPatternColor { + buttonsBackground = .color(previewPatternColor.withMultipliedBrightnessBy(1.05)) + } + + var isBackButton = false + if state.inWearPreview || state.inUpgradePreview { + isBackButton = true + } + var leftControlItems: [GlassControlGroupComponent.Item] = [] + leftControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("close"), + content: .icon(isBackButton ? "Navigation/Back" : "Navigation/Close"), + action: { [weak state] in + guard let state else { + return + } + if state.inWearPreview { + if let controller = controller() as? GiftViewScreen { + controller.dismissAllTooltips() + } + state.inWearPreview = false + state.updated(transition: .spring(duration: 0.4)) + } else if state.inUpgradePreview { + state.cancelUpgradePreview() + } else { + state.dismiss(animated: true) + } + } + )) + + var rightControlItems: [GlassControlGroupComponent.Item] = [] + if uniqueGift != nil && !showWearPreview { + if let _ = component.subject.arguments?.canCraftDate { + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("craft"), + content: .icon("Premium/Craft"), + action: { [weak state] in + guard let state else { + return + } + state.craftGift() + } + )) + } + + rightControlItems.append(GlassControlGroupComponent.Item( + id: AnyHashable("more"), + content: .animation("anim_morewide"), + action: { [weak state] in + guard let state else { + return + } + state.openMore() + } + )) + } + + let buttons = buttons.update( + component: GlassControlPanelComponent( + theme: theme, + leftItem: GlassControlPanelComponent.Item( + items: leftControlItems, + background: buttonsBackground + ), + centralItem: nil, + rightItem: rightControlItems.isEmpty ? nil : GlassControlPanelComponent.Item( + items: rightControlItems, + background: buttonsBackground + ), + centerAlignmentIfPossible: true, + tag: state.controlButtonsTag + ), + availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 44.0), + transition: context.transition + ) context.add(buttons - .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - 16.0 - buttons.size.width / 2.0, y: 31.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + buttons.size.height / 2.0)) ) let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom @@ -6107,15 +6038,15 @@ private struct GiftConfiguration { private final class GiftViewContextReferenceContentSource: ContextReferenceContentSource { private let controller: ViewController - private let sourceNode: ASDisplayNode + private let sourceView: UIView - init(controller: ViewController, sourceNode: ASDisplayNode) { + init(controller: ViewController, sourceView: UIView) { self.controller = controller - self.sourceNode = sourceNode + self.sourceView = sourceView } func transitionInfo() -> ContextControllerReferenceViewInfo? { - return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds) + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } diff --git a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift index 15a5f94a64..127d4f4160 100644 --- a/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift +++ b/submodules/TelegramUI/Components/GlassBarButtonComponent/Sources/GlassBarButtonComponent.swift @@ -17,6 +17,7 @@ public final class GlassBarButtonComponent: Component { public let isDark: Bool public let state: DisplayState? public let isEnabled: Bool + public let isVisible: Bool public let animateScale: Bool public let component: AnyComponentWithIdentity public let action: ((UIView) -> Void)? @@ -28,6 +29,7 @@ public final class GlassBarButtonComponent: Component { isDark: Bool, state: DisplayState? = nil, isEnabled: Bool = true, + isVisible: Bool = true, animateScale: Bool = true, component: AnyComponentWithIdentity, action: ((UIView) -> Void)?, @@ -38,6 +40,7 @@ public final class GlassBarButtonComponent: Component { self.isDark = isDark self.state = state self.isEnabled = isEnabled + self.isVisible = isVisible self.animateScale = animateScale self.component = component self.action = action @@ -60,6 +63,9 @@ public final class GlassBarButtonComponent: Component { if lhs.isEnabled != rhs.isEnabled { return false } + if lhs.isVisible != rhs.isVisible { + return false + } if lhs.animateScale != rhs.animateScale { return false } @@ -113,6 +119,10 @@ public final class GlassBarButtonComponent: Component { guard let self, let component = self.component, component.animateScale else { return } + if [.glass, .tintedGlass].contains(component.state) { + return + } + if highlighted { self.containerView.layer.animateSpring(from: CGFloat((self.containerView.layer.presentation()?.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0) as NSNumber, to: 1.3636 as NSNumber, keyPath: "transform.scale", duration: 0.5, removeOnCompletion: false) } else { @@ -256,7 +266,7 @@ public final class GlassBarButtonComponent: Component { transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0) } - glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: effectiveState == .tintedGlass ? .custom(style: .default, color: backgroundColor.withMultipliedAlpha(effectiveState == .tintedGlass ? 1.0 : 0.7)) : .panel), isInteractive: true, transition: glassBackgroundTransition) + glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: effectiveState == .tintedGlass ? .custom(style: .default, color: backgroundColor.withMultipliedAlpha(effectiveState == .tintedGlass ? 1.0 : 0.7)) : .panel), isInteractive: true, isVisible: component.isVisible, transition: glassBackgroundTransition) glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds) } else if case .glass = component.state { let glassBackgroundView: GlassBackgroundView @@ -273,7 +283,7 @@ public final class GlassBarButtonComponent: Component { transition.animateAlpha(view: glassBackgroundView, from: 0.0, to: 1.0) } - glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: .panel), isInteractive: true, transition: glassBackgroundTransition) + glassBackgroundView.update(size: containerSize, cornerRadius: cornerRadius, isDark: component.isDark, tintColor: .init(kind: .panel), isInteractive: true, isVisible: component.isVisible, transition: glassBackgroundTransition) glassBackgroundTransition.setFrame(view: glassBackgroundView, frame: bounds) } else if let glassBackgroundView = self.glassBackgroundView { self.glassBackgroundView = nil diff --git a/submodules/TelegramUI/Components/GlassControls/BUILD b/submodules/TelegramUI/Components/GlassControls/BUILD index 0b4fa6a2e7..241fd0c45d 100644 --- a/submodules/TelegramUI/Components/GlassControls/BUILD +++ b/submodules/TelegramUI/Components/GlassControls/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift index 3ba9ab6695..1c99e3468e 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlGroup.swift @@ -7,12 +7,14 @@ import GlassBackgroundComponent import PlainButtonComponent import BundleIconComponent import MultilineTextComponent +import LottieComponent public final class GlassControlGroupComponent: Component { public final class Item: Equatable { public enum Content: Hashable { case icon(String) case text(String) + case animation(String) } public let id: AnyHashable @@ -39,9 +41,10 @@ public final class GlassControlGroupComponent: Component { } } - public enum Background { + public enum Background: Equatable { case panel case activeTint + case color(UIColor) } public let theme: PresentationTheme @@ -90,6 +93,7 @@ public final class GlassControlGroupComponent: Component { public final class View: UIView { private let backgroundView: GlassBackgroundView private var itemViews: [ItemId: ComponentView] = [:] + private var animations: [ItemId: ActionSlot] = [:] private var component: GlassControlGroupComponent? private weak var state: EmptyComponentState? @@ -121,6 +125,20 @@ public final class GlassControlGroupComponent: Component { self.component = component self.state = state + let foregroundColor: UIColor + let tintColor: GlassBackgroundView.TintColor + switch component.background { + case .panel: + foregroundColor = component.theme.chat.inputPanel.panelControlColor + tintColor = .init(kind: .panel) + case .activeTint: + foregroundColor = component.theme.list.itemCheckColors.foregroundColor + tintColor = .init(kind: .panel, innerColor: component.theme.list.itemCheckColors.fillColor) + case let .color(color): + foregroundColor = .white + tintColor = .init(kind: .custom(style: .default, color: color)) + } + var contentsWidth: CGFloat = 0.0 var validIds: [AnyHashable] = [] var isInteractive = false @@ -142,21 +160,35 @@ public final class GlassControlGroupComponent: Component { if item.action != nil { isInteractive = true } - + let content: AnyComponent var itemInsets = UIEdgeInsets() switch item.content { case let .icon(name): content = AnyComponent(BundleIconComponent( name: name, - tintColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor + tintColor: foregroundColor )) case let .text(string): content = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: string, font: Font.medium(17.0), textColor: component.background == .activeTint ? component.theme.list.itemCheckColors.foregroundColor : component.theme.chat.inputPanel.panelControlColor)) + text: .plain(NSAttributedString(string: string, font: Font.medium(17.0), textColor: foregroundColor)) )) itemInsets.left = 10.0 itemInsets.right = itemInsets.left + case let .animation(name): + let playOnce: ActionSlot + if let current = self.animations[itemId] { + playOnce = current + } else { + playOnce = ActionSlot() + self.animations[itemId] = playOnce + } + content = AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: name), + color: foregroundColor, + size: CGSize(width: 32.0, height: 32.0), + playOnce: playOnce + )) } var minItemWidth: CGFloat = availableSize.height @@ -170,8 +202,12 @@ public final class GlassControlGroupComponent: Component { content: content, minSize: CGSize(width: minItemWidth, height: availableSize.height), contentInsets: itemInsets, - action: { + action: { [weak self] in item.action?() + + if case .animation = item.content { + self?.animations[itemId]?.invoke(Void()) + } }, isEnabled: item.action != nil, animateAlpha: false, @@ -211,6 +247,7 @@ public final class GlassControlGroupComponent: Component { }) alphaTransition.animateBlur(layer: itemComponentView.layer, fromRadius: 0.0, toRadius: 8.0, removeOnCompletion: false) } + self.animations[id] = nil } } for id in removeIds { @@ -218,13 +255,6 @@ public final class GlassControlGroupComponent: Component { } let size = CGSize(width: contentsWidth, height: availableSize.height) - let tintColor: GlassBackgroundView.TintColor - switch component.background { - case .panel: - tintColor = .init(kind: .panel) - case .activeTint: - tintColor = .init(kind: .panel, innerColor: component.theme.list.itemCheckColors.fillColor) - } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) isInteractive = true self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: tintColor, isInteractive: isInteractive, transition: transition) diff --git a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift index 09861db223..44e6dcb841 100644 --- a/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift +++ b/submodules/TelegramUI/Components/GlassControls/Sources/GlassControlPanel.swift @@ -36,19 +36,22 @@ public final class GlassControlPanelComponent: Component { public let rightItem: Item? public let centralItem: Item? public let centerAlignmentIfPossible: Bool + public let tag: AnyObject? public init( theme: PresentationTheme, leftItem: Item?, centralItem: Item?, rightItem: Item?, - centerAlignmentIfPossible: Bool = false + centerAlignmentIfPossible: Bool = false, + tag: AnyObject? = nil ) { self.theme = theme self.leftItem = leftItem self.centralItem = centralItem self.rightItem = rightItem self.centerAlignmentIfPossible = centerAlignmentIfPossible + self.tag = tag } public static func ==(lhs: GlassControlPanelComponent, rhs: GlassControlPanelComponent) -> Bool { @@ -67,10 +70,23 @@ public final class GlassControlPanelComponent: Component { if lhs.centerAlignmentIfPossible != rhs.centerAlignmentIfPossible { return false } + if lhs.tag !== rhs.tag { + return false + } return true } - public final class View: UIView { + public final class View: UIView, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + private let glassContainerView: GlassBackgroundContainerView private var leftItemComponent: ComponentView? diff --git a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift index d613ad86a6..7c60e22cc6 100644 --- a/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift +++ b/submodules/TelegramUI/Components/JoinSubjectScreen/Sources/JoinSubjectScreen.swift @@ -926,6 +926,7 @@ private final class JoinSubjectScreenComponent: Component { } } + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) let actionButtonTitle: String switch component.mode { case .group: @@ -937,6 +938,7 @@ private final class JoinSubjectScreenComponent: Component { transition: transition, component: AnyComponent(ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: environment.theme.list.itemCheckColors.fillColor, foreground: environment.theme.list.itemCheckColors.foregroundColor, pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -961,7 +963,7 @@ private final class JoinSubjectScreenComponent: Component { } )), environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + containerSize: CGSize(width: availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) ) let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height @@ -969,7 +971,7 @@ private final class JoinSubjectScreenComponent: Component { let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) + let actionButtonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: 0.0), size: actionButtonSize) if let actionButtonView = self.actionButton.view { if actionButtonView.superview == nil { self.bottomPanelContainer.addSubview(actionButtonView) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 435802b919..4eaa0da386 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -178,6 +178,7 @@ public final class ListActionItemComponent: Component { public let action: ((UIView) -> Void)? public let highlighting: Highlighting public let updateIsHighlighted: ((UIView, Bool) -> Void)? + public let tag: AnyObject? public init( theme: PresentationTheme, @@ -192,7 +193,8 @@ public final class ListActionItemComponent: Component { contextOptions: [ContextOption] = [], action: ((UIView) -> Void)?, highlighting: Highlighting = .default, - updateIsHighlighted: ((UIView, Bool) -> Void)? = nil + updateIsHighlighted: ((UIView, Bool) -> Void)? = nil, + tag: AnyObject? = nil ) { self.theme = theme self.style = style @@ -207,6 +209,7 @@ public final class ListActionItemComponent: Component { self.action = action self.highlighting = highlighting self.updateIsHighlighted = updateIsHighlighted + self.tag = tag } public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { @@ -246,6 +249,9 @@ public final class ListActionItemComponent: Component { if lhs.highlighting != rhs.highlighting { return false } + if lhs.tag !== rhs.tag { + return false + } return true } @@ -322,7 +328,17 @@ public final class ListActionItemComponent: Component { } } - public final class View: UIView, ListSectionComponent.ChildView { + public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + private let container: ContentContainer private let button: HighlightTrackingButton private var background: ComponentView? @@ -413,6 +429,10 @@ public final class ListActionItemComponent: Component { return result } + public func displayHighlight() { + + } + func update(component: ListActionItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component self.component = component diff --git a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift index a4f0bbb857..bc14c09f7e 100644 --- a/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift +++ b/submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift @@ -161,6 +161,8 @@ public final class LottieComponent: Component { private var currentTemplateFrameImage: UIImage? + public var onFrameUpdate: (Int) -> Void = { _ in } + public var externalShouldPlay: Bool? { didSet { if self.externalShouldPlay != oldValue { @@ -407,6 +409,7 @@ public final class LottieComponent: Component { var effectiveFrameIndex = self.currentFrame effectiveFrameIndex = max(animationFrameRange.lowerBound, min(animationFrameRange.upperBound, effectiveFrameIndex)) + self.onFrameUpdate(effectiveFrameIndex) animationInstance.renderFrame(with: Int32(effectiveFrameIndex), into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(currentDisplaySize.width), height: Int32(currentDisplaySize.height), bytesPerRow: Int32(context.bytesPerRow)) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift index eb682e3ad7..0a99c74e5c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -99,13 +99,13 @@ private final class SheetContent: CombinedComponent { let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButton = cancelButton.update( component: GlassBarButtonComponent( size: barButtonSize, - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift index 84a436a1c8..19ee9be430 100644 --- a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -114,11 +114,15 @@ public final class NavigationStackComponent: Compon override init(frame: CGRect) { super.init(frame: frame) - + self.dimView.alpha = 0.0 self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) self.dimView.isUserInteractionEnabled = false self.addSubview(self.dimView) + + self.clipsToBounds = true + self.layer.cornerRadius = 40.0 + self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } required init?(coder: NSCoder) { diff --git a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift index 2323498da8..131a83ec67 100644 --- a/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/MessagePriceItem/Sources/MessagePriceItem.swift @@ -30,8 +30,9 @@ public final class MessagePriceItem: Equatable, ListViewItem, ItemListItem, List let updated: (Int64, Bool) -> Void let openSetCustom: (() -> Void)? let openPremiumInfo: (() -> Void)? + public let tag: ItemListItemTag? - public init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64, Bool) -> Void, openSetCustom: (() -> Void)? = nil, openPremiumInfo: (() -> Void)? = nil) { + public init(theme: PresentationTheme, strings: PresentationStrings, systemStyle: ItemListSystemStyle = .legacy, isEnabled: Bool, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64, Bool) -> Void, openSetCustom: (() -> Void)? = nil, openPremiumInfo: (() -> Void)? = nil, tag: ItemListItemTag? = nil) { self.theme = theme self.strings = strings self.systemStyle = systemStyle @@ -44,6 +45,7 @@ public final class MessagePriceItem: Equatable, ListViewItem, ItemListItem, List self.updated = updated self.openSetCustom = openSetCustom self.openPremiumInfo = openPremiumInfo + self.tag = tag } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -84,7 +86,6 @@ public final class MessagePriceItem: Equatable, ListViewItem, ItemListItem, List } public static func ==(lhs: MessagePriceItem, rhs: MessagePriceItem) -> Bool { - if lhs.theme !== rhs.theme { return false } @@ -114,7 +115,7 @@ public final class MessagePriceItem: Equatable, ListViewItem, ItemListItem, List } } -private class MessagePriceItemNode: ListViewItemNode { +private class MessagePriceItemNode: ListViewItemNode, ItemListItemNode { private struct Amount: Equatable { private let sliderSteps: [Int] private let minRealValue: Int @@ -216,6 +217,10 @@ private class MessagePriceItemNode: ListViewItemNode { private var item: MessagePriceItem? private var layoutParams: ListViewItemLayoutParams? + public var tag: ItemListItemTag? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift index dccc9e198b..1aacc73113 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoCoverComponent/Sources/PeerInfoCoverComponent.swift @@ -119,6 +119,7 @@ public final class PeerInfoCoverComponent: Component { public let gradientCenter: CGPoint public let avatarTransitionFraction: CGFloat public let patternTransitionFraction: CGFloat + public let patternIconScale: CGFloat public init( context: AccountContext, @@ -132,7 +133,8 @@ public final class PeerInfoCoverComponent: Component { gradientOnTop: Bool = false, gradientCenter: CGPoint = CGPoint(x: 0.5, y: 0.5), avatarTransitionFraction: CGFloat, - patternTransitionFraction: CGFloat + patternTransitionFraction: CGFloat, + patternIconScale: CGFloat = 1.0 ) { self.context = context self.subject = subject @@ -146,6 +148,7 @@ public final class PeerInfoCoverComponent: Component { self.gradientCenter = gradientCenter self.avatarTransitionFraction = avatarTransitionFraction self.patternTransitionFraction = patternTransitionFraction + self.patternIconScale = patternIconScale } public static func ==(lhs: PeerInfoCoverComponent, rhs: PeerInfoCoverComponent) -> Bool { @@ -185,6 +188,9 @@ public final class PeerInfoCoverComponent: Component { if lhs.patternTransitionFraction != rhs.patternTransitionFraction { return false } + if lhs.patternIconScale != rhs.patternIconScale { + return false + } return true } @@ -533,7 +539,7 @@ public final class PeerInfoCoverComponent: Component { var baseDistance: CGFloat = component.avatarSize.width / 2.0 + 22.0 var baseRowDistance: CGFloat = 28.0 - var baseItemSize: CGFloat = 26.0 + var baseItemSize: CGFloat = 26.0 * component.patternIconScale if availableSize.width <= 60.0 { baseDistance *= 0.35 baseRowDistance *= 0.3 @@ -581,7 +587,8 @@ public final class PeerInfoCoverComponent: Component { self.avatarPatternContentLayers.append(itemLayer) } - itemLayer.frame = itemFrame + //itemLayer.frame = itemFrame + transition.setFrame(layer: itemLayer, frame: itemFrame) itemLayer.layerTintColor = UIColor(white: 0.0, alpha: 0.8).cgColor transition.setAlpha(layer: itemLayer, alpha: 1.0 - itemScaleFraction) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index eb55763bbe..638ab2ec2a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -311,7 +311,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let twoStepAuthData = Promise(nil) let supportPeerDisposable = MetaDisposable() let tipsPeerDisposable = MetaDisposable() + let cachedFaq = Promise(nil) + private var didSetCachedFaq = false weak var copyProtectionTooltipController: TooltipController? weak var emojiStatusSelectionController: ViewController? @@ -1590,35 +1592,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro strongSelf.controller?.push(controller) } } else { - (strongSelf.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear)) - strongSelf.state = strongSelf.state.withIsEditing(true) - var updateOnCompletion = false - if strongSelf.headerNode.isAvatarExpanded { - updateOnCompletion = true - strongSelf.headerNode.skipCollapseCompletion = true - strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false - strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = false - strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = true - strongSelf.headerNode.updateIsAvatarExpanded(false, transition: .immediate) - strongSelf.updateNavigationExpansionPresentation(isExpanded: false, animated: true) - } - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.scrollNode.view.setContentOffset(CGPoint(), animated: false) - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } - UIView.transition(with: strongSelf.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { - }, completion: { _ in - if updateOnCompletion { - strongSelf.headerNode.skipCollapseCompletion = false - strongSelf.headerNode.avatarListNode.listContainerNode.isCollapsing = false - strongSelf.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true - strongSelf.headerNode.editingContentNode.avatarNode.canAttachVideo = true - strongSelf.headerNode.editingContentNode.avatarNode.reset() - if let (layout, navigationHeight) = strongSelf.validLayout { - strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) - } - } - }) + strongSelf.activateEdit() } case .done, .cancel: strongSelf.view.endEditing(true) @@ -2095,9 +2069,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro |> map { data -> Bool in return data?.hasSecretValues ?? false } - - self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) - + screenData = peerInfoScreenSettingsData(context: context, peerId: peerId, accountsAndPeers: self.accountsAndPeers.get(), activeSessionsContextAndCount: self.activeSessionsContextAndCount.get(), notificationExceptions: self.notificationExceptions.get(), privacySettings: self.privacySettings.get(), archivedStickerPacks: self.archivedPacks.get(), hasPassport: hasPassport, starsContext: starsContext, tonContext: tonContext) @@ -2646,6 +2618,38 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro var canAttachVideo: Bool? + func activateEdit() { + (self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.3, curve: .linear)) + self.state = self.state.withIsEditing(true) + var updateOnCompletion = false + if self.headerNode.isAvatarExpanded { + updateOnCompletion = true + self.headerNode.skipCollapseCompletion = true + self.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = false + self.headerNode.editingContentNode.avatarNode.canAttachVideo = false + self.headerNode.avatarListNode.listContainerNode.isCollapsing = true + self.headerNode.updateIsAvatarExpanded(false, transition: .immediate) + self.updateNavigationExpansionPresentation(isExpanded: false, animated: true) + } + if let (layout, navigationHeight) = self.validLayout { + self.scrollNode.view.setContentOffset(CGPoint(), animated: false) + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + UIView.transition(with: self.view, duration: 0.3, options: [.transitionCrossDissolve], animations: { + }, completion: { _ in + if updateOnCompletion { + self.headerNode.skipCollapseCompletion = false + self.headerNode.avatarListNode.listContainerNode.isCollapsing = false + self.headerNode.avatarListNode.avatarContainerNode.canAttachVideo = true + self.headerNode.editingContentNode.avatarNode.canAttachVideo = true + self.headerNode.editingContentNode.avatarNode.reset() + if let (layout, navigationHeight) = self.validLayout { + self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate, additive: false) + } + } + }) + } + private func updateData(_ data: PeerInfoScreenData) { let previousData = self.data var previousMemberCount: Int? @@ -4661,7 +4665,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(customTarget: self.peerId == self.context.account.peerId ? nil : .peer(self.peerId), resumeLiveStream: false, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) + let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: self.peerId == self.context.account.peerId ? nil : .peer(self.peerId), resumeLiveStream: false, transitionIn: cameraTransitionIn, transitionedIn: {}, transitionOut: self.storyCameraTransitionOut()) coordinator?.animateIn() } case .channelBoostRequired: @@ -4800,35 +4804,88 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.headerNode.navigationButtonContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) if self.isSettings { + if !self.didSetCachedFaq { + self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init))) + self.didSetCachedFaq = true + } + if let settings = self.data?.globalSettings { - self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .navigation, placeholder: self.presentationData.strings.Settings_Search, hasBackground: true, hasSeparator: true, contentNode: SettingsSearchContainerNode(context: self.context, openResult: { [weak self] result in - if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { - result.present(strongSelf.context, navigationController, { [weak self] mode, controller in - if let strongSelf = self { - switch mode { - case .push: - if let controller = controller { - strongSelf.controller?.push(controller) + self.searchDisplayController = SearchDisplayController( + presentationData: self.presentationData, + mode: .navigation, + placeholder: self.presentationData.strings.Settings_Search, + hasBackground: true, + hasSeparator: true, + contentNode: SettingsSearchContainerNode( + context: self.context, + openResult: { [weak self] result in + if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController { + result.present(strongSelf.context, navigationController, { [weak self] mode, controller in + if let strongSelf = self { + switch mode { + case .push: + if let controller = controller { + strongSelf.controller?.push(controller) + } + case .modal: + if let controller = controller { + strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in + self?.deactivateSearch() + })) + } + case .immediate: + if let controller = controller { + strongSelf.controller?.present(controller, in: .window(.root), with: nil) + } + case .dismiss: + strongSelf.deactivateSearch() } - case .modal: - if let controller = controller { - strongSelf.controller?.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet, completion: { [weak self] in - self?.deactivateSearch() - })) - } - case .immediate: - if let controller = controller { - strongSelf.controller?.present(controller, in: .window(.root), with: nil) - } - case .dismiss: - strongSelf.deactivateSearch() - } + } + }) } - }) - } - }, resolvedFaqUrl: self.cachedFaq.get(), exceptionsList: .single(settings.notificationExceptions), archivedStickerPacks: .single(settings.archivedStickerPacks), privacySettings: .single(settings.privacySettings), hasTwoStepAuth: self.hasTwoStepAuth.get(), twoStepAuthData: self.twoStepAccessConfiguration.get(), activeSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.0 }, webSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.2 }), cancel: { [weak self] in - self?.deactivateSearch() - }, searchBarIsExternal: true) + }, + openContextMenu: { item, sourceNode, rect, gesture in + let link = "tg://settings/\(item.id)" + let items: [ContextMenuItem] = [ + .action( ContextMenuActionItem( + text: "Copy Link", + icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, + action: { [weak self] _, f in + f(.default) + + UIPasteboard.general.string = link + guard let self else { + return + } + self.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + )) + ] + let contextController = makeContextController( + presentationData: self.presentationData, + source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), + items: .single(ContextController.Items(content: .list(items))), + recognizer: nil, + gesture: gesture as? ContextGesture + ) + self.context.sharedContext.mainWindow?.presentInGlobalOverlay(contextController) + }, + resolvedFaqUrl: self.cachedFaq.get(), + exceptionsList: .single(settings.notificationExceptions), + archivedStickerPacks: .single(settings.archivedStickerPacks), + privacySettings: .single(settings.privacySettings), + hasTwoStepAuth: self.hasTwoStepAuth.get(), + twoStepAuthData: self.twoStepAccessConfiguration.get(), + activeSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.0 }, + webSessionsContext: self.activeSessionsContextAndCount.get() |> map { $0?.2 } + ), + cancel: { [weak self] in + self?.deactivateSearch() + }, + searchBarIsExternal: true + ) } } else if let currentPaneKey = self.paneContainerNode.currentPaneKey, case .members = currentPaneKey { self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .navigation, placeholder: self.presentationData.strings.Common_Search, hasBackground: true, hasSeparator: true, contentNode: ChannelMembersSearchContainerNode(context: self.context, forceTheme: nil, peerId: self.peerId, mode: .searchMembers, filters: [], searchContext: self.groupMembersSearchContext, openPeer: { [weak self] peer, participant in @@ -4972,6 +5029,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.searchDisplayController = nil searchDisplayController.deactivate(placeholder: nil) + controller.dismissAllTooltips() + if self.isSettings { (self.controller?.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) controller.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .animated(duration: 0.4, curve: .spring)) @@ -6215,6 +6274,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc return self.controllerNode.privacySettings } + public var twoStepAuthData: Promise { + return self.controllerNode.twoStepAuthData + } + override public var customNavigationData: CustomViewControllerNavigationData? { get { if !self.isSettings { @@ -6657,7 +6720,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } - private func dismissAllTooltips() { + fileprivate func dismissAllTooltips() { self.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController, !controller.keepOnParentDismissal { controller.dismissWithCommitAction() @@ -6688,6 +6751,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } + public func activateEdit() { + self.controllerNode.activateEdit() + } + public func openAvatarSetup(completedWithUploadingImage: @escaping (UIImage, Signal) -> UIView?) { let proceed = { [weak self] in self?.openAvatarForEditing(completedWithUploadingImage: completedWithUploadingImage) @@ -6965,6 +7032,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } } + public func openEmojiStatusSetup() { + self.controllerNode.openSettings(section: .emojiStatus) + } + public func openBirthdaySetup() { self.controllerNode.interaction.updateIsEditingBirthdate(true) self.controllerNode.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift index 4705dc95e5..3b933f278a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift @@ -375,6 +375,10 @@ extension PeerInfoScreenImpl { return resource } + public func updateProfilePhoto(_ image: UIImage) { + self.updateProfilePhoto(image, mode: .generic, uploadStatus: nil) + } + public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise?) { guard let resource = setupProfilePhotoUpload(image: image, mode: mode, indefiniteProgress: false) else { uploadStatus?.set(.single(.done)) @@ -463,7 +467,7 @@ extension PeerInfoScreenImpl { case .accept: (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedPhotoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in if case .info = action { - self?.parentController?.openSettings() + self?.parentController?.openSettings(edit: false) } return false }), in: .current) @@ -475,6 +479,10 @@ extension PeerInfoScreenImpl { } })) } + + public func updateProfileVideo(_ image: UIImage, video: Any?, values: Any?, markup: UploadPeerPhotoMarkup?) { + self.updateProfileVideo(image, video: video as? MediaEditorScreenImpl.MediaResult.VideoResult, values: values as? MediaEditorValues, markup: markup, mode: .generic, uploadStatus: nil) + } public func updateProfileVideo(_ image: UIImage, video: MediaEditorScreenImpl.MediaResult.VideoResult?, values: MediaEditorValues?, markup: UploadPeerPhotoMarkup?, mode: PeerInfoAvatarEditingMode, uploadStatus: Promise?) { var uploadVideo = true @@ -662,7 +670,7 @@ extension PeerInfoScreenImpl { case .accept: (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in if case .info = action { - self?.parentController?.openSettings() + self?.parentController?.openSettings(edit: false) } return false }), in: .current) @@ -874,7 +882,7 @@ extension PeerInfoScreenImpl { case .accept: (strongSelf.parentController?.topViewController as? ViewController)?.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .image(image: image, title: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccess, text: strongSelf.presentationData.strings.Conversation_SuggestedVideoSuccessText, round: true, undoText: nil), elevatedLayout: false, animateInAsReplacement: true, action: { [weak self] action in if case .info = action { - self?.parentController?.openSettings() + self?.parentController?.openSettings(edit: false) } return false }), in: .current) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift index 05f0178ac0..18f7209276 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift @@ -750,7 +750,7 @@ extension PeerInfoScreenNode { guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.legacy(.autoremoveMessages)), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -966,7 +966,7 @@ extension PeerInfoScreenNode { guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.legacy(.autoremoveMessages)), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } @@ -1096,7 +1096,7 @@ extension PeerInfoScreenNode { guard let self else { return } - self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in + self.context.sharedContext.openResolvedUrl(.settings(.legacy(.autoremoveMessages)), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in guard let self else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 7534cbb812..3b3a3c0082 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -310,7 +310,7 @@ final class PeerInfoStoryGridScreenComponent: Component { return } if let rootController = component.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - let coordinator = rootController.openStoryCamera(customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil }) + let coordinator = rootController.openStoryCamera(mode: .photo, customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil }) coordinator?.animateIn() } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD index 3719417c55..ef6fe4b4a9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/BUILD @@ -51,6 +51,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/LanguageSelectionScreen", "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", + "//submodules/TelegramUI/Components/Gifts/GiftUnpinScreen", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/BalancedTextComponent", "//submodules/TelegramUI/Components/CheckComponent", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift index 08c6b85a34..280848e31e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/GiftsListView.swift @@ -540,6 +540,7 @@ final class GiftsListView: UIView { component: AnyComponent( GiftItemComponent( context: self.context, + style: .glass, theme: params.presentationData.theme, strings: params.presentationData.strings, peer: peer, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 5796ee4cfe..e407168dcb 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -24,6 +24,7 @@ import PeerInfoPaneNode import GiftItemComponent import PlainButtonComponent import GiftViewScreen +import GiftUnpinScreen import ButtonComponent import UndoUI import CheckComponent diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD index a86c1d099c..5c0909d530 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/BUILD @@ -31,6 +31,8 @@ swift_library( "//submodules/Components/HierarchyTrackingLayer", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/PeerInfo/ProfileLevelRatingBarComponent", + "//submodules/Components/ResizableSheetComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift index c24f054aeb..a5ab5c2553 100644 --- a/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/ProfileLevelInfoScreen/Sources/ProfileLevelInfoScreen.swift @@ -19,8 +19,10 @@ import PremiumUI import LottieComponent import AnimatedTextComponent import ProfileLevelRatingBarComponent +import ResizableSheetComponent +import GlassBarButtonComponent -private final class ProfileLevelInfoScreenComponent: Component { +private final class SheetContent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext @@ -40,7 +42,7 @@ private final class ProfileLevelInfoScreenComponent: Component { self.pendingStarRating = pendingStarRating } - static func ==(lhs: ProfileLevelInfoScreenComponent, rhs: ProfileLevelInfoScreenComponent) -> Bool { + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { return true } @@ -51,39 +53,8 @@ private final class ProfileLevelInfoScreenComponent: Component { self.isChangingPreview = isChangingPreview } } - - private final class ScrollView: UIScrollView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - return super.hitTest(point, with: event) - } - } - - private struct ItemLayout: Equatable { - var containerSize: CGSize - var containerInset: CGFloat - var bottomInset: CGFloat - var topInset: CGFloat - - init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { - self.containerSize = containerSize - self.containerInset = containerInset - self.bottomInset = bottomInset - self.topInset = topInset - } - } - + final class View: UIView, UIScrollViewDelegate { - private let dimView: UIView - private let backgroundLayer: SimpleLayer - private let navigationBarContainer: SparseContainerView - private let navigationBackgroundView: BlurredBackgroundView - private let navigationBarSeparator: SimpleLayer - private let scrollView: ScrollView - private let scrollContentClippingView: SparseContainerView - private let scrollContentView: UIView - - private let closeButton = ComponentView() - private let peerAvatar = ComponentView() private let title = ComponentView() @@ -92,78 +63,17 @@ private final class ProfileLevelInfoScreenComponent: Component { private let descriptionText = ComponentView() private var items: [ComponentView] = [] - - private let bottomPanelContainer: UIView - private let actionButton = ComponentView() - - private var isFirstTimeApplyingModalFactor: Bool = true - private var ignoreScrolling: Bool = false - - private var component: ProfileLevelInfoScreenComponent? + + private var component: SheetContent? private weak var state: EmptyComponentState? private var environment: ViewControllerComponentContainer.Environment? private var isUpdating: Bool = false private var isPreviewingPendingRating: Bool = false - - private var itemLayout: ItemLayout? - private var topOffsetDistance: CGFloat? - + private var cachedChevronImage: UIImage? - private var cachedCloseImage: UIImage? - override init(frame: CGRect) { - self.dimView = UIView() - - self.backgroundLayer = SimpleLayer() - self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - self.backgroundLayer.cornerRadius = 10.0 - - self.navigationBarContainer = SparseContainerView() - - self.navigationBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.navigationBarSeparator = SimpleLayer() - - self.scrollView = ScrollView() - - self.scrollContentClippingView = SparseContainerView() - self.scrollContentClippingView.clipsToBounds = true - - self.scrollContentView = UIView() - - self.bottomPanelContainer = UIView() - + override init(frame: CGRect) { super.init(frame: frame) - - self.addSubview(self.dimView) - self.layer.addSublayer(self.backgroundLayer) - - self.scrollView.delaysContentTouches = false - self.scrollView.canCancelContentTouches = true - self.scrollView.clipsToBounds = false - self.scrollView.contentInsetAdjustmentBehavior = .never - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = false - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - - self.addSubview(self.scrollContentClippingView) - self.scrollContentClippingView.addSubview(self.scrollView) - - self.scrollView.addSubview(self.scrollContentView) - - self.addSubview(self.navigationBarContainer) - self.addSubview(self.bottomPanelContainer) - - self.navigationBarContainer.addSubview(self.navigationBackgroundView) - self.navigationBarContainer.layer.addSublayer(self.navigationBarSeparator) - - self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } required init?(coder: NSCoder) { @@ -173,106 +83,7 @@ private final class ProfileLevelInfoScreenComponent: Component { deinit { } - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateScrolling(transition: .immediate) - } - } - - func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - } - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.bounds.contains(point) { - return nil - } - if !self.backgroundLayer.frame.contains(point) { - return self.dimView - } - - if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { - return result - } - - let result = super.hitTest(point, with: event) - return result - } - - @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - guard let environment = self.environment, let controller = environment.controller() else { - return - } - controller.dismiss() - } - } - - private func updateScrolling(transition: ComponentTransition) { - guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else { - return - } - var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset - - let titleTransformFraction: CGFloat = max(0.0, min(1.0, -topOffset / 20.0)) - - let navigationAlpha: CGFloat = titleTransformFraction - transition.setAlpha(view: self.navigationBackgroundView, alpha: navigationAlpha) - transition.setAlpha(layer: self.navigationBarSeparator, alpha: navigationAlpha) - - topOffset = max(0.0, topOffset) - transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) - - transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) - - let topOffsetDistance: CGFloat = 80.0 - self.topOffsetDistance = topOffsetDistance - var topOffsetFraction = topOffset / topOffsetDistance - topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) - - let transitionFactor: CGFloat = 1.0 - topOffsetFraction - var modalOverlayTransition = transition - if self.isFirstTimeApplyingModalFactor { - self.isFirstTimeApplyingModalFactor = false - modalOverlayTransition = .spring(duration: 0.5) - } - if self.isUpdating { - DispatchQueue.main.async { [weak controller] in - guard let controller else { - return - } - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) - } - } else { - controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: modalOverlayTransition.containedViewLayoutTransition) - } - } - - func animateIn() { - self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.bottomPanelContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - } - - func animateOut(completion: @escaping () -> Void) { - let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY - - self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in - completion() - }) - self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - self.bottomPanelContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - - if let environment = self.environment, let controller = environment.controller() { - controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .easeInOut)) - } - } - - func update(component: ProfileLevelInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + func update(component: SheetContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { self.isUpdating = false @@ -283,62 +94,15 @@ private final class ProfileLevelInfoScreenComponent: Component { let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.16) let environment = environment[ViewControllerComponentContainer.Environment.self].value - let themeUpdated = self.environment?.theme !== environment.theme - - let resetScrolling = self.scrollView.bounds.width != availableSize.width - + let sideInset: CGFloat = 16.0 + environment.safeInsets.left self.component = component self.state = state self.environment = environment - - if themeUpdated { - self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) - self.backgroundLayer.backgroundColor = environment.theme.actionSheet.opaqueItemBackgroundColor.cgColor - - self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.navigationBarSeparator.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor - } - - transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) - + var contentHeight: CGFloat = 0.0 - - let closeImage: UIImage - if let image = self.cachedCloseImage, !themeUpdated { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.05), foregroundColor: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.4))! - self.cachedCloseImage = closeImage - } - - let closeButtonSize = self.closeButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent(Image(image: closeImage, size: closeImage.size)), - action: { [weak self] in - guard let self, let controller = self.environment?.controller() else { - return - } - controller.dismiss() - } - ).minSize(CGSize(width: 62.0, height: 56.0))), - environment: {}, - containerSize: CGSize(width: 100.0, height: 100.0) - ) - let closeButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - closeButtonSize.width, y: 0.0), size: closeButtonSize) - if let closeButtonView = self.closeButton.view { - if closeButtonView.superview == nil { - self.navigationBarContainer.addSubview(closeButtonView) - } - transition.setFrame(view: closeButtonView, frame: closeButtonFrame) - } - - let containerInset: CGFloat = environment.statusBarHeight + 10.0 - - let clippingY: CGFloat - + let titleString: String = environment.strings.ProfileLevelInfo_Title let descriptionTextString: String var secondaryDescriptionTextString: String? @@ -429,20 +193,15 @@ private final class ProfileLevelInfoScreenComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((56.0 - titleSize.height) * 0.5)), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) * 0.5), y: floorToScreenPixels((72.0 - titleSize.height) * 0.5)), size: titleSize) if let titleView = self.title.view { if titleView.superview == nil { - self.navigationBarContainer.addSubview(titleView) + self.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } - contentHeight += 56.0 - - let navigationBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 54.0)) - transition.setFrame(view: self.navigationBackgroundView, frame: navigationBackgroundFrame) - self.navigationBackgroundView.update(size: navigationBackgroundFrame.size, cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.navigationBarSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - + contentHeight += 72.0 + var levelFraction: CGFloat let badgeText: String @@ -497,7 +256,7 @@ private final class ProfileLevelInfoScreenComponent: Component { ) if let levelInfoView = self.levelInfo.view { if levelInfoView.superview == nil { - self.scrollContentView.addSubview(levelInfoView) + self.addSubview(levelInfoView) } levelInfoView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - levelInfoSize.width) * 0.5), y: contentHeight - 6.0), size: levelInfoSize) } @@ -568,6 +327,7 @@ private final class ProfileLevelInfoScreenComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: environment.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0), highlightAction: { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") @@ -591,7 +351,7 @@ private final class ProfileLevelInfoScreenComponent: Component { let secondaryDescriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - secondaryDescriptionTextSize.width) * 0.5), y: contentHeight), size: secondaryDescriptionTextSize) if let secondaryDescriptionTextView = secondaryDescriptionText.view { if secondaryDescriptionTextView.superview == nil { - self.scrollContentView.addSubview(secondaryDescriptionTextView) + self.addSubview(secondaryDescriptionTextView) if isChangingPreview { transition.animatePosition(view: secondaryDescriptionTextView, from: CGPoint(x: -changingPreviewAnimationOffset, y: 0.0), to: CGPoint(), additive: true) alphaTransition.animateAlpha(view: secondaryDescriptionTextView, from: 0.0, to: 1.0) @@ -639,7 +399,7 @@ private final class ProfileLevelInfoScreenComponent: Component { let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) if let descriptionTextView = self.descriptionText.view { if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) + self.addSubview(descriptionTextView) } transition.setPosition(view: descriptionTextView, position: descriptionTextFrame.center) descriptionTextView.bounds = CGRect(origin: CGPoint(), size: descriptionTextFrame.size) @@ -711,7 +471,7 @@ private final class ProfileLevelInfoScreenComponent: Component { let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize) if let itemComponentView = itemView.view { if itemComponentView.superview == nil { - self.scrollContentView.addSubview(itemComponentView) + self.addSubview(itemComponentView) } itemComponentView.frame = itemFrame } @@ -721,10 +481,78 @@ private final class ProfileLevelInfoScreenComponent: Component { contentHeight += 31.0 + contentHeight += 82.0 + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + + + +private final class ProfileLevelInfoSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let peer: EnginePeer + private let starRating: TelegramStarRating + private let pendingStarRating: TelegramStarPendingRating? + + init( + context: AccountContext, + peer: EnginePeer, + starRating: TelegramStarRating, + pendingStarRating: TelegramStarPendingRating? + ) { + self.context = context + self.peer = peer + self.starRating = starRating + self.pendingStarRating = pendingStarRating + } + + static func ==(lhs: ProfileLevelInfoSheetComponent, rhs: ProfileLevelInfoSheetComponent) -> Bool { + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + let playButtonAnimation = ActionSlot() + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme.withModalBlocksBackground() + let actionButtonTitle: String = environment.strings.ProfileLevelInfo_CloseButton var buttonTitle: [AnyComponentWithIdentity] = [] - let playButtonAnimation = ActionSlot() buttonTitle.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieComponent( content: LottieComponent.AppBundleContent(name: "anim_ok"), color: environment.theme.list.itemCheckColors.foregroundColor, @@ -740,101 +568,85 @@ private final class ProfileLevelInfoScreenComponent: Component { badgeForeground: environment.theme.list.itemCheckColors.fillColor )))) - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + peer: context.component.peer, + starRating: context.component.starRating, + pendingStarRating: context.component.pendingStarRating + )), + titleItem: nil, + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + hasTopEdgeEffect: false, + bottomItem: AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(HStack(buttonTitle, spacing: 2.0)) + ), + action: { + dismiss(true) + } + ) ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self else { - return + backgroundColor: .color(theme.actionSheet.opaqueItemBackgroundColor), + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) } - self.environment?.controller()?.dismiss() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + }, + availableSize: context.availableSize, + transition: context.transition ) - let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) - let bottomPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight), size: CGSize(width: availableSize.width, height: bottomPanelHeight)) - transition.setFrame(view: self.bottomPanelContainer, frame: bottomPanelFrame) - - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.bottomPanelContainer.addSubview(actionButtonView) - playButtonAnimation.invoke(Void()) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - - contentHeight += bottomPanelHeight - - clippingY = bottomPanelFrame.minY - 8.0 - - let topInset: CGFloat = max(0.0, availableSize.height - containerInset - contentHeight) - - let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) - - self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset) - - transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) - - transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) - transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) - transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) - transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) - - self.ignoreScrolling = true - let previousBounds = self.scrollView.bounds - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) - let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) - if contentSize != self.scrollView.contentSize { - self.scrollView.contentSize = contentSize - } - if resetScrolling { - self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) - } else { - if !previousBounds.isEmpty, !transition.animation.isImmediate { - let bounds = self.scrollView.bounds - if bounds.maxY != previousBounds.maxY { - let offsetY = previousBounds.maxY - bounds.maxY - transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) - } - } - } - self.ignoreScrolling = false - self.updateScrolling(transition: transition) - - return availableSize + return context.availableSize } } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } } -public class ProfileLevelInfoScreen: ViewControllerComponentContainer { +public final class ProfileLevelInfoScreen: ViewControllerComponentContainer { private let context: AccountContext - private var isDismissed: Bool = false public init( context: AccountContext, @@ -851,69 +663,33 @@ public class ProfileLevelInfoScreen: ViewControllerComponentContainer { } else { theme = .default } - super.init(context: context, component: ProfileLevelInfoScreenComponent( + super.init( context: context, - peer: peer, - starRating: starRating, - pendingStarRating: pendingStarRating - ), navigationBarAppearance: .none, theme: theme) + component: ProfileLevelInfoSheetComponent( + context: context, + peer: peer, + starRating: starRating, + pendingStarRating: pendingStarRating + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: theme + ) self.statusBar.statusBarStyle = .Ignore self.navigationPresentation = .flatModal self.blocksBackgroundWhenInOverlay = true } - + required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - deinit { - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.view.disablesInteractiveModalDismiss = true - - if let componentView = self.node.hostView.componentView as? ProfileLevelInfoScreenComponent.View { - componentView.animateIn() + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } } - - override public func dismiss(completion: (() -> Void)? = nil) { - if !self.isDismissed { - self.isDismissed = true - - if let componentView = self.node.hostView.componentView as? ProfileLevelInfoScreenComponent.View { - componentView.animateOut(completion: { [weak self] in - completion?() - self?.dismiss(animated: false) - }) - } else { - self.dismiss(animated: false) - } - } - } -} - -private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.beginPath() - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) } private final class ItemComponent: Component { @@ -1026,7 +802,7 @@ private final class ItemComponent: Component { component: AnyComponent(FilledRoundedRectangleComponent( color: component.isBadgeAccent ? component.theme.chatList.unreadBadgeActiveBackgroundColor : component.theme.chatList.unreadBadgeInactiveBackgroundColor, cornerRadius: .value(6.0), - smoothCorners: true + smoothCorners: false )), environment: {}, containerSize: badgeSize diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD new file mode 100644 index 0000000000..a74ad1276b --- /dev/null +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ProxyServerPreviewScreen", + module_name = "ProxyServerPreviewScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/SheetComponent", + "//submodules/TelegramUI/Components/Gifts/TableComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/BundleIconComponent", + "//submodules/OverlayStatusController", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift new file mode 100644 index 0000000000..d7953bc77b --- /dev/null +++ b/submodules/TelegramUI/Components/ProxyServerPreviewScreen/Sources/ProxyServerPreviewScreen.swift @@ -0,0 +1,546 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import GlassBarButtonComponent +import ButtonComponent +import TableComponent +import PresentationDataUtils +import BundleIconComponent +import OverlayStatusController + +private final class ProxyServerPreviewSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let server: ProxyServerSettings + let cancel: (Bool) -> Void + + init( + context: AccountContext, + server: ProxyServerSettings, + cancel: @escaping (Bool) -> Void + ) { + self.context = context + self.server = server + self.cancel = cancel + } + + static func ==(lhs: ProxyServerPreviewSheetContent, rhs: ProxyServerPreviewSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.server != rhs.server { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private let server: ProxyServerSettings + + private var disposable = MetaDisposable() + private var statusDisposable = MetaDisposable() + fileprivate var status: ProxyServerStatus? + private var statusesContext: ProxyServersStatuses? + + fileprivate var inProgress = false + + fileprivate weak var controller: ProxyServerPreviewScreen? + + private var revertSettings: ProxySettings? + + init(context: AccountContext, server: ProxyServerSettings) { + self.context = context + self.server = server + + super.init() + } + + deinit { + self.disposable.dispose() + self.statusDisposable.dispose() + + if let revertSettings = self.revertSettings { + let _ = updateProxySettingsInteractively(accountManager: self.context.sharedContext.accountManager, { _ in + return revertSettings + }) + } + } + + var isChecked: Bool { + return self.statusesContext != nil + } + + func check() { + guard self.statusesContext == nil else { + return + } + + self.displayWarningIfNeeded { [weak self] in + guard let self else { + return + } + + let statusesContext = ProxyServersStatuses(network: self.context.account.network, servers: .single([self.server])) + self.statusesContext = statusesContext + + self.status = .checking + self.updated() + + self.statusDisposable.set((statusesContext.statuses() + |> map { return $0.first?.value } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] status in + if let self, let status { + self.status = status + self.updated() + } + })) + } + } + + func connect() { + guard !self.inProgress else { + return + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + self.displayWarningIfNeeded { [weak self] in + guard let self else { + return + } + let accountManager = self.context.sharedContext.accountManager + let proxyServerSettings = self.server + let _ = (accountManager.transaction { transaction -> ProxySettings in + var currentSettings: ProxySettings? + let _ = updateProxySettingsInteractively(transaction: transaction, { settings in + currentSettings = settings + var settings = settings + if let index = settings.servers.firstIndex(of: proxyServerSettings) { + settings.servers[index] = proxyServerSettings + settings.activeServer = proxyServerSettings + } else { + settings.servers.insert(proxyServerSettings, at: 0) + settings.activeServer = proxyServerSettings + } + settings.enabled = true + return settings + }) + return currentSettings ?? ProxySettings.defaultSettings + } |> deliverOnMainQueue).start(next: { [weak self] previousSettings in + if let self { + self.revertSettings = previousSettings + + self.inProgress = true + self.updated() + + let signal = self.context.account.network.connectionStatus + |> filter { status in + switch status { + case let .online(proxyAddress): + if proxyAddress == proxyServerSettings.host { + return true + } else { + return false + } + default: + return false + } + } + |> map { _ -> Bool in + return true + } + |> distinctUntilChanged + |> timeout(15.0, queue: Queue.mainQueue(), alternate: .single(false)) + |> deliverOnMainQueue + self.disposable.set(signal.start(next: { [weak self] value in + if let self { + self.inProgress = false + self.updated() + + self.revertSettings = nil + if value { + if let navigationController = self.controller?.navigationController as? NavigationController { + Queue.mainQueue().after(0.5) { + (navigationController.topViewController as? ViewController)?.present(OverlayStatusController(theme: presentationData.theme, type: .shieldSuccess(presentationData.strings.SocksProxySetup_ProxyEnabled, false)), in: .window(.root)) + } + } + self.controller?.dismissAnimated() + } else { + let _ = updateProxySettingsInteractively(accountManager: accountManager, { _ in + return previousSettings + }).start() + self.controller?.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + } + } + })) + } + }) + } + } + + func displayWarningIfNeeded(commit: @escaping () -> Void) { + guard !self.isChecked else { + commit() + return + } + //TODO:localize + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController( + context: context, + title: "Warning", + text: "This action will expose your IP address to the admin of the proxy server.", + actions: [ + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .defaultAction, title: "Proceed", action: { + commit() + }) + ] + ) + self.controller?.present(alertController, in: .window(.root)) + } + } + + func makeState() -> State { + return State(context: self.context, server: self.server) + } + + static var body: Body { + let closeButton = Child(GlassBarButtonComponent.self) + let title = Child(MultilineTextComponent.self) + let table = Child(TableComponent.self) + let button = Child(ButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let state = context.state + if state.controller == nil { + state.controller = environment.controller() as? ProxyServerPreviewScreen + } + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let closeButton = closeButton.update( + component: GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + component.cancel(true) + } + ), + availableSize: CGSize(width: 44.0, height: 44.0), + transition: .immediate + ) + + let titleText: String = "Proxy" + let buttonText: String = "Connect Proxy" + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleText, + font: Font.semibold(17.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor + var tableItems: [TableComponent.Item] = [] + + tableItems.append(.init( + id: "server", + title: strings.SocksProxySetup_Hostname, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: component.server.host, font: tableFont, textColor: tableTextColor))) + ) + )) + + tableItems.append(.init( + id: "port", + title: strings.SocksProxySetup_Port, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "\(component.server.port)", font: tableFont, textColor: tableTextColor))) + ) + )) + + switch component.server.connection { + case let .socks5(username, password): + if let username { + tableItems.append(.init( + id: "username", + title: strings.SocksProxySetup_Username, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: username, font: tableFont, textColor: tableTextColor))) + ) + )) + } + if let password { + tableItems.append(.init( + id: "password", + title: strings.SocksProxySetup_Password, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: password, font: tableFont, textColor: tableTextColor))) + ) + )) + } + case .mtp: + tableItems.append(.init( + id: "secret", + title: strings.SocksProxySetup_Secret, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "•••••", font: tableFont, textColor: tableTextColor))) + ) + )) + } + + var statusText = "Check Status" + var statusColor = tableLinkColor + var statusIsActive = true + if let status = state.status { + statusIsActive = false + switch status { + case let .available(rtt): + let pingTime = Int(rtt * 1000.0) + statusText = strings.SocksProxySetup_ProxyStatusPing("\(pingTime)").string + statusColor = tableTextColor + case .checking: + statusText = strings.SocksProxySetup_ProxyStatusChecking + statusColor = tableTextColor + case .notAvailable: + statusText = strings.SocksProxySetup_ProxyStatusUnavailable + statusColor = environment.theme.list.itemDestructiveColor + } + } + + tableItems.append(.init( + id: "status", + title: strings.SocksProxySetup_Status, + component: AnyComponent( + Button( + content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: statusText, font: tableFont, textColor: statusColor)))), + automaticHighlight: statusIsActive, + action: { + if statusIsActive { + state.check() + } + } + ) + ) + )) + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0, + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: buttonText, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + displaysProgress: state.inProgress, + action: { + state.connect() + } + ), + availableSize: CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0)) + ) + + var originY: CGFloat = 88.0 + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 28.0 + + context.add(button + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + button.size.height / 2.0)) + ) + originY += button.size.height + originY += buttonInsets.bottom + + context.add(closeButton + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: originY) + return contentSize + } + } +} + +private final class ProxyServerPreviewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let server: ProxyServerSettings + + init( + context: AccountContext, + server: ProxyServerSettings + ) { + self.context = context + self.server = server + } + + static func ==(lhs: ProxyServerPreviewSheetComponent, rhs: ProxyServerPreviewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.server != rhs.server { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(ProxyServerPreviewSheetContent( + context: context.component.context, + server: context.component.server, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + } + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public class ProxyServerPreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + server: ProxyServerSettings + ) { + self.context = context + + super.init( + context: context, + component: ProxyServerPreviewSheetComponent( + context: context, + server: server + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift index 7c92a69b53..1c60ff19c9 100644 --- a/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BirthdayPickerScreen/Sources/BirthdayPickerScreen.swift @@ -120,10 +120,10 @@ private final class BirthdayPickerSheetContentComponent: Component { transition: transition, component: AnyComponent( GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -138,7 +138,7 @@ private final class BirthdayPickerSheetContentComponent: Component { ) ), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize) if let cancelView = self.cancel.view { diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index 02ee5142e8..656a5b8410 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -21,6 +21,7 @@ final class PasskeysScreenComponent: Component { let context: AccountContext let displaySkip: Bool let initialPasskeysData: [TelegramPasskey]? + let forceCreate: Bool let passkeysDataUpdated: ([TelegramPasskey]) -> Void let completion: () -> Void let cancel: () -> Void @@ -29,6 +30,7 @@ final class PasskeysScreenComponent: Component { context: AccountContext, displaySkip: Bool, initialPasskeysData: [TelegramPasskey]?, + forceCreate: Bool, passkeysDataUpdated: @escaping ([TelegramPasskey]) -> Void, completion: @escaping () -> Void, cancel: @escaping () -> Void @@ -36,6 +38,7 @@ final class PasskeysScreenComponent: Component { self.context = context self.displaySkip = displaySkip self.initialPasskeysData = initialPasskeysData + self.forceCreate = forceCreate self.passkeysDataUpdated = passkeysDataUpdated self.completion = completion self.cancel = cancel @@ -248,6 +251,12 @@ final class PasskeysScreenComponent: Component { self.state?.updated(transition: .easeInOut(duration: 0.25)) }) } + + if component.forceCreate { + Queue.mainQueue().justDispatch { + self.createPasskey() + } + } } self.component = component @@ -401,10 +410,18 @@ final class PasskeysScreenComponent: Component { public final class PasskeysScreen: ViewControllerComponentContainer { private let context: AccountContext - public init(context: AccountContext, displaySkip: Bool, initialPasskeysData: [TelegramPasskey]?, passkeysDataUpdated: @escaping ([TelegramPasskey]) -> Void, completion: @escaping () -> Void, cancel: @escaping () -> Void) { + public init( + context: AccountContext, + displaySkip: Bool, + initialPasskeysData: [TelegramPasskey]?, + forceCreate: Bool = false, + passkeysDataUpdated: @escaping ([TelegramPasskey]) -> Void, + completion: @escaping () -> Void, + cancel: @escaping () -> Void + ) { self.context = context - super.init(context: context, component: PasskeysScreenComponent(context: context, displaySkip: displaySkip, initialPasskeysData: initialPasskeysData, passkeysDataUpdated: passkeysDataUpdated, completion: completion, cancel: cancel), navigationBarAppearance: .transparent) + super.init(context: context, component: PasskeysScreenComponent(context: context, displaySkip: displaySkip, initialPasskeysData: initialPasskeysData, forceCreate: forceCreate, passkeysDataUpdated: passkeysDataUpdated, completion: completion, cancel: cancel), navigationBarAppearance: .transparent) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift index 4f3676d001..47e5749eb0 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/GiftListItemComponent.swift @@ -168,7 +168,7 @@ final class GiftListItemComponent: Component { if let current = self.resaleGiftsContexts[id] { resaleGiftsContext = current } else { - resaleGiftsContext = ResaleGiftsContext(account: component.context.account, giftId: id) + resaleGiftsContext = ResaleGiftsContext(account: component.context.account, giftId: id, forCrafting: false) self.resaleGiftsContexts[id] = resaleGiftsContext } @@ -473,6 +473,7 @@ final class GiftListItemComponent: Component { content: AnyComponent( GiftItemComponent( context: component.context, + style: .glass, theme: component.theme, strings: presentationData.strings, peer: nil, diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index e37b3431cd..c4ec862dc0 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -40,6 +40,17 @@ import GiftViewScreen import BalanceNeededScreen private let giftListTag = GenericComponentViewTag() +private let addIconsTag = GenericComponentViewTag() +private let useGiftTag = GenericComponentViewTag() + +public enum UserAppearanceEntryTag { + case profile + case profileAddIcons + case profileUseGift + case name + case nameAddIcons + case nameUseGift +} final class UserAppearanceScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -656,6 +667,7 @@ final class UserAppearanceScreenComponent: Component { let alertController = giftPurchaseAlertController( context: component.context, gift: uniqueGift, + showAttributes: true, peer: peer, animateBalanceOverlay: true, navigationController: controller.navigationController as? NavigationController, @@ -959,6 +971,32 @@ final class UserAppearanceScreenComponent: Component { return false } + func openEmojiSetup() { + guard let component = self.component, let environment = self.environment, let resolvedState = self.resolveState() else { + return + } + + switch self.currentSection { + case .profile: + if let view = self.profileColorSection.findTaggedView(tag: addIconsTag) as? ListActionItemComponent.View, let iconView = view.iconView { + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.backgroundFileId, color: resolvedState.profileColor.flatMap { + component.context.peerNameColors.getProfile($0, dark: environment.theme.overallDarkAppearance, subject: .palette).main + } ?? environment.theme.list.itemAccentColor, subject: .profile) + } + case .name: + var replyColor: UIColor + switch resolvedState.nameColor { + case let .preset(nameColor): + replyColor = component.context.peerNameColors.get(nameColor, dark: environment.theme.overallDarkAppearance).main + case let .collectible(collectibleColor): + replyColor = collectibleColor.mainColor(dark: environment.theme.overallDarkAppearance) + } + if let view = self.nameColorSection.findTaggedView(tag: addIconsTag) as? ListActionItemComponent.View, let iconView = view.iconView { + self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: replyColor, subject: .reply) + } + } + } + func update(component: UserAppearanceScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.isUpdating = true defer { @@ -968,7 +1006,32 @@ final class UserAppearanceScreenComponent: Component { let environment = environment[EnvironmentType.self].value let themeUpdated = self.environment?.theme !== environment.theme self.environment = environment - + + if self.component == nil { + if let controller = environment.controller() as? UserAppearanceScreen, let focusOnItemTag = controller.focusOnItemTag { + switch focusOnItemTag { + case .profile: + self.currentSection = .profile + case .profileAddIcons: + self.currentSection = .profile + Queue.mainQueue().after(0.1) { + self.openEmojiSetup() + } + case .profileUseGift: + self.currentSection = .profile + case .name: + self.currentSection = .name + case .nameAddIcons: + self.currentSection = .name + Queue.mainQueue().after(0.1) { + self.openEmojiSetup() + } + case .nameUseGift: + self.currentSection = .name + } + } + } + self.component = component self.state = state @@ -1320,7 +1383,8 @@ final class UserAppearanceScreenComponent: Component { self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.backgroundFileId, color: resolvedState.profileColor.flatMap { component.context.peerNameColors.getProfile($0, dark: environment.theme.overallDarkAppearance, subject: .palette).main } ?? environment.theme.list.itemAccentColor, subject: .profile) - } + }, + tag: addIconsTag ))) ], displaySeparators: true, @@ -1649,7 +1713,8 @@ final class UserAppearanceScreenComponent: Component { } self.openEmojiSetup(sourceView: iconView, currentFileId: resolvedState.replyFileId, color: replyColor, subject: .reply) - } + }, + tag: addIconsTag ))) ], displaySeparators: true, @@ -1932,6 +1997,7 @@ final class UserAppearanceScreenComponent: Component { public class UserAppearanceScreen: ViewControllerComponentContainer { private let context: AccountContext + fileprivate let focusOnItemTag: UserAppearanceEntryTag? private let overNavigationContainer: UIView @@ -1939,9 +2005,11 @@ public class UserAppearanceScreen: ViewControllerComponentContainer { public init( context: AccountContext, - updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + focusOnItemTag: UserAppearanceEntryTag? = nil ) { self.context = context + self.focusOnItemTag = focusOnItemTag self.overNavigationContainer = SparseContainerView() diff --git a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift index 7d0375134e..697a6a5d6e 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReactionSetupController/Sources/QuickReactionSetupController.swift @@ -36,6 +36,18 @@ private enum QuickReactionSetupControllerSection: Int32 { case items } +public enum QuickReactionSetupEntryTag: ItemListItemTag, Equatable { + case choose + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? QuickReactionSetupEntryTag, self == other { + return true + } else { + return false + } + } +} + private enum QuickReactionSetupControllerEntry: ItemListNodeEntry { enum StableId: Hashable { case demoHeader @@ -203,7 +215,8 @@ private func quickReactionSetupControllerEntries( public func quickReactionSetupController( context: AccountContext, - updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil + updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, + focusOnItemTag: QuickReactionSetupEntryTag? = nil ) -> ViewController { let statePromise = ValuePromise(QuickReactionSetupControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: QuickReactionSetupControllerState()) @@ -372,6 +385,12 @@ public func quickReactionSetupController( controller.dismiss() } + if focusOnItemTag == .choose { + Queue.mainQueue().after(0.1) { + arguments.openQuickReaction() + } + } + return controller } diff --git a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift index af7ea2f949..55d0c49b87 100644 --- a/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift +++ b/submodules/TelegramUI/Components/Settings/WallpaperGridScreen/Sources/ThemeGridController.swift @@ -40,6 +40,7 @@ public final class ThemeGridController: ViewController { private let context: AccountContext private let mode: Mode + private let forceEdit: Bool private var presentationData: PresentationData private let presentationDataPromise = Promise() @@ -58,9 +59,10 @@ public final class ThemeGridController: ViewController { public var completion: (WallpaperSelectionResult) -> Void = { _ in } - public init(context: AccountContext, mode: Mode = .generic) { + public init(context: AccountContext, mode: Mode = .generic, forceEdit: Bool = false) { self.context = context self.mode = mode + self.forceEdit = forceEdit self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise.set(.single(self.presentationData)) @@ -419,6 +421,10 @@ public final class ThemeGridController: ViewController { self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) self.displayNodeDidLoad() + + if self.forceEdit { + self.editPressed() + } } private func shareWallpapers(_ wallpapers: [TelegramWallpaper]) { diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift index d1ccccce3d..3a3a03ca1c 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/LiveStreamSettingsScreen.swift @@ -878,7 +878,7 @@ final class LiveStreamSettingsScreenComponent: Component { transition.setFrame(view: titleView, frame: titleFrame) } - let barButtonSize = CGSize(width: 40.0, height: 40.0) + let barButtonSize = CGSize(width: 44.0, height: 44.0) let cancelButtonSize = self.cancelButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( diff --git a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift index e9f6392579..da0799013c 100644 --- a/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift +++ b/submodules/TelegramUI/Components/SpaceWarpView/Sources/SpaceWarpView.swift @@ -566,8 +566,8 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode { } #endif - let resolutionX = max(2, Int(size.width / 40.0)) - let resolutionY = max(2, Int(size.height / 40.0)) + let resolutionX = max(2, Int(size.width / 50.0)) + let resolutionY = max(2, Int(size.height / 50.0)) self.updateGrid(resolutionX: resolutionX, resolutionY: resolutionY) guard let resolution = self.resolution, let meshView = self.meshView else { return diff --git a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift index 56e8473c6c..10ffdbb804 100644 --- a/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift +++ b/submodules/TelegramUI/Components/Stars/BalanceNeededScreen/Sources/BalanceNeededScreen.swift @@ -73,10 +73,10 @@ private final class BalanceNeededSheetContentComponent: Component { let closeButtonSize = self.closeButton.update( transition: .immediate, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -88,7 +88,7 @@ private final class BalanceNeededSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) if let closeButtonView = self.closeButton.view { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 7677a5af7e..7279b03dca 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -672,10 +672,10 @@ private final class StarsTransactionSheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -686,7 +686,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { component.cancel(true) } ), - availableSize: CGSize(width: 30.0, height: 30.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index d5157826f0..c8a11b6ce4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -341,10 +341,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -355,7 +355,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 13cd9ed470..d748f5eecc 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -88,10 +88,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -102,7 +102,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton @@ -244,7 +244,7 @@ private final class SheetContent: CombinedComponent { transition: .immediate ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 36.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 38.0)) ) contentSize.height += title.size.height contentSize.height += 56.0 diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift index ff169817fe..84c8d24435 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataButtonComponent.swift @@ -18,15 +18,18 @@ final class DataButtonComponent: Component { let theme: PresentationTheme let title: String let action: () -> Void + let tag: AnyObject? init( theme: PresentationTheme, title: String, - action: @escaping () -> Void + action: @escaping () -> Void, + tag: AnyObject? = nil ) { self.theme = theme self.title = title self.action = action + self.tag = tag } static func ==(lhs: DataButtonComponent, rhs: DataButtonComponent) -> Bool { @@ -36,10 +39,23 @@ final class DataButtonComponent: Component { if lhs.title != rhs.title { return false } + if lhs.tag !== rhs.tag { + return false + } return true } - class View: HighlightTrackingButton { + class View: HighlightTrackingButton, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + private let title = ComponentView() private var component: DataButtonComponent? @@ -95,6 +111,10 @@ final class DataButtonComponent: Component { component.action() } + func displayHighlight() { + + } + func update(component: DataButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let themeUpdated = self.component?.theme !== component.theme diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index 50ed55b13d..7ab5fa46ec 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -26,6 +26,14 @@ import TelegramUIPreferences import SegmentControlComponent import GlassBackgroundComponent +public enum DataUsageEntryTag { + case mobile + case wifi + case reset +} + +private let resetTag = GenericComponentViewTag() + final class DataUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -454,6 +462,25 @@ final class DataUsageScreenComponent: Component { } func update(component: DataUsageScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let environment = environment[ViewControllerComponentContainer.Environment.self].value + if self.component == nil { + if let controller = environment.controller() as? DataUsageScreen, let focusOnItemTag = controller.focusOnItemTag { + switch focusOnItemTag { + case .mobile: + self.selectedStats = .mobile + case .wifi: + self.selectedStats = .wifi + case .reset: + Queue.mainQueue().after(0.1, { + if let view = self.clearButtonView.view as? DataButtonComponent.View { + view.displayHighlight() + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentSize.height - self.scrollView.bounds.height), animated: true) + } + }) + } + } + } + self.component = component self.state = state @@ -482,7 +509,6 @@ final class DataUsageScreenComponent: Component { }) } - let environment = environment[ViewControllerComponentContainer.Environment.self].value let animationHint = transition.userData(AnimationHint.self) @@ -876,7 +902,7 @@ final class DataUsageScreenComponent: Component { SegmentControlComponent.Item(id: AnyHashable(SelectedStats.mobile), title: environment.strings.DataUsage_TopSectionMobile), SegmentControlComponent.Item(id: AnyHashable(SelectedStats.wifi), title: environment.strings.DataUsage_TopSectionWifi) ], - selectedId: "total", + selectedId: AnyHashable(self.selectedStats), action: { [weak self] id in guard let self, let id = id.base as? SelectedStats else { return @@ -1087,7 +1113,8 @@ final class DataUsageScreenComponent: Component { title: environment.strings.DataUsage_Reset, action: { [weak self] in self?.requestClear() - } + }, + tag: resetTag )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) @@ -1210,6 +1237,7 @@ final class DataUsageScreenComponent: Component { public final class DataUsageScreen: ViewControllerComponentContainer { private let context: AccountContext + fileprivate let focusOnItemTag: DataUsageEntryTag? private let overNavigationContainer: UIView @@ -1218,8 +1246,9 @@ public final class DataUsageScreen: ViewControllerComponentContainer { return self.readyValue } - public init(context: AccountContext, stats: NetworkUsageStats, mediaAutoDownloadSettings: MediaAutoDownloadSettings, makeAutodownloadSettingsController: @escaping (Bool) -> ViewController) { + public init(context: AccountContext, stats: NetworkUsageStats, mediaAutoDownloadSettings: MediaAutoDownloadSettings, makeAutodownloadSettingsController: @escaping (Bool) -> ViewController, focusOnItemTag: DataUsageEntryTag? = nil) { self.context = context + self.focusOnItemTag = focusOnItemTag self.overNavigationContainer = SparseContainerView() diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift index 07c8dac8b2..254f31d999 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift @@ -90,6 +90,13 @@ private final class SignpostContextImpl: SignpostContext { #endif +public enum StorageUsageEntryTag { + case edit + case autoRemove + case clearCache + case maxCache +} + private extension StorageUsageScreenComponent.Category { init(_ category: StorageUsageStats.CategoryKey) { switch category { @@ -3317,7 +3324,7 @@ public final class StorageUsageScreen: ViewControllerComponentContainer { fileprivate var childCompleted: ((@escaping () -> Void) -> Void)? - public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) { + public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil, focusOnItemTag: StorageUsageEntryTag? = nil) { self.context = context self.overNavigationContainer = SparseContainerView() diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 6512a4d558..f5b2a60cb2 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -629,6 +629,13 @@ public final class StoryPeerListComponent: Component { } } + public func openEmojiStatusSetup() { + guard let component = self.component, let titleIconView = self.titleIconView?.view else { + return + } + component.openStatusSetup(titleIconView) + } + private func updateScrolling(transition: ComponentTransition) { guard let component = self.component, let itemLayout = self.itemLayout else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift index d7ab24789a..73217796ea 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -220,10 +220,10 @@ private final class StoryStealthModeSheetContentComponent: Component { let cancelButtonSize = cancelButton.update( transition: transition, component: AnyComponent(GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: environment.theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -235,7 +235,7 @@ private final class StoryStealthModeSheetContentComponent: Component { } )), environment: {}, - containerSize: CGSize(width: 40.0, height: 40.0) + containerSize: CGSize(width: 44.0, height: 44.0) ) if let cancelButtonView = cancelButton.view { if cancelButtonView.superview == nil { diff --git a/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/Contents.json new file mode 100644 index 0000000000..d771721693 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bg.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/bg.pdf b/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/bg.pdf new file mode 100644 index 0000000000..01bd3b43a3 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Components/ColorMask.imageset/bg.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/Contents.json new file mode 100644 index 0000000000..e84d4476e0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cubeside.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/cubeside.png b/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/cubeside.png new file mode 100644 index 0000000000..0ea2c30f46 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Components/CubeSide.imageset/cubeside.png differ diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/Contents.json new file mode 100644 index 0000000000..de1020ae57 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "question_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/question_30.pdf b/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/question_30.pdf new file mode 100644 index 0000000000..2dd78321e0 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Navigation/Question.imageset/question_30.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/Contents.json new file mode 100644 index 0000000000..37867ecf54 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cocoonlogo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/cocoonlogo.pdf b/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/cocoonlogo.pdf new file mode 100644 index 0000000000..519328ad06 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/CocoonLogo.imageset/cocoonlogo.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/Contents.json new file mode 100644 index 0000000000..305014724d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "craft_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/craft_30.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/craft_30.pdf new file mode 100644 index 0000000000..631b08cf2d Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Craft.imageset/craft_30.pdf differ diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index aba541ad98..fa8fbefa5f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2866,10 +2866,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedTitlePanelContext { if !$0.contains(where: { switch $0 { - case .requestInProgress: - return true - default: - return false + case .requestInProgress: + return true + default: + return false } }) { var updatedContexts = $0 @@ -2887,10 +2887,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedTitlePanelContext { if let index = $0.firstIndex(where: { switch $0 { - case .requestInProgress: - return true - default: - return false + case .requestInProgress: + return true + default: + return false } }) { var updatedContexts = $0 @@ -2905,12 +2905,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) |> deliverOnMainQueue).startStrict(next: { [weak self] result in if let strongSelf = self { switch result { - case let .accepted(url): - if let url { - strongSelf.openUrl(url, concealed: false, skipUrlAuth: true) - } - default: - strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true) + case let .accepted(url): + if let url { + strongSelf.openUrl(url, concealed: false, skipUrlAuth: true) + } + default: + strongSelf.openUrl(defaultUrl, concealed: false, skipUrlAuth: true) } } })) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 0a99f2c908..a0f5eb924f 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -5,6 +5,7 @@ import AsyncDisplayKit import TelegramCore import Postbox import Display +import ComponentFlow import SwiftSignalKit import TelegramUIPreferences import TelegramPresentationData @@ -39,6 +40,10 @@ import BrowserUI import MediaEditorScreen import GiftSetupScreen import AlertComponent +import ContactListUI +import DeviceAccess +import ProxyServerPreviewScreen +import AuthConfirmationScreen private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatControllerInteractionNavigateToPeer) -> ChatControllerInteractionNavigateToPeer { if case .default = navigation { @@ -482,7 +487,9 @@ func openResolvedUrlImpl( } dismissInput() - present(ProxyServerActionSheetController(context: context, server: server), nil) + + let controller = ProxyServerPreviewScreen(context: context, server: server) + navigationController?.pushViewController(controller) case let .confirmationCode(code): if let topController = navigationController?.topViewController as? AuthorizationSequenceCodeEntryController { topController.applyConfirmationCode(code) @@ -754,87 +761,232 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) })) dismissInput() + case let .contacts(section): + if case .new = section { + context.sharedContext.openAddContact( + context: context, + firstName: "", + lastName: "", + phoneNumber: "", + label: "", + present: { c, a in + present(c, a) + }, + pushController: { [weak navigationController] c in + navigationController?.pushViewController(c) + }, + completed: {} + ) + } else if case .invite = section { + let _ = (DeviceAccess.authorizationStatus(subject: .contacts) + |> take(1) + |> deliverOnMainQueue).start(next: { value in + switch value { + case .allowed: + let controller = InviteContactsController(context: context) + navigationController?.pushViewController(controller) + default: + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: { + context.sharedContext.applicationBindings.openSettings() + })]), nil) + } + }) + } else { + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.popToRoot(animated: true) + rootController.openContacts() + + switch section { + case .search: + Queue.mainQueue().after(0.1) { + rootController.getContactsController()?.tabBarActivateSearch() + } + case .sort: + Queue.mainQueue().after(0.1) { + if let contactsController = rootController.getContactsController() as? ContactsController { + contactsController.sortPressed() + } + } + case .manage: + presentContactAccessPicker(context: context) + default: + break + } + } + } + case let .chats(section): + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootController { + rootController.popToRoot(animated: true) + rootController.openChats() + Queue.mainQueue().after(0.1) { + switch section { + case .search: + rootController.getChatsController()?.tabBarActivateSearch() + case .edit: + if let chatsController = rootController.getChatsController() as? ChatListController { + chatsController.activateEdit() + } + case .emojiStatus: + if let chatsController = rootController.getChatsController() as? ChatListController { + chatsController.openEmojiStatusSetup() + } + default: + break + } + } + } + case let .compose(section): + switch section { + case .contact: + context.sharedContext.openAddContact( + context: context, + firstName: "", + lastName: "", + phoneNumber: "", + label: "", + present: { c, a in + present(c, a) + }, + pushController: { [weak navigationController] c in + navigationController?.pushViewController(c) + }, + completed: {} + ) + case .group: + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .groupCreation(isCall: false), onlyWriteable: true)) + navigationController?.pushViewController(controller) + let _ = (controller.result + |> deliverOnMainQueue).startStandalone(next: { [weak navigationController] result in + var peerIds: [ContactListPeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue + } + let createGroup = context.sharedContext.makeCreateGroupController(context: context, peerIds: peerIds.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }), initialTitle: nil, mode: .generic, completion: nil) + navigationController?.pushViewController(createGroup) + }) + case .channel: + let controller = createChannelController(context: context) + navigationController?.pushViewController(controller) + default: + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootController { + rootController.popToRoot(animated: true) + rootController.openRootCompose() + } + } + case let .postStory(section): + let mode: StoryCameraMode + switch section { + case .video: + mode = .video + case .live: + mode = .live + default: + mode = .photo + } + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.popToRoot(animated: true) + let coordinator = rootController.openStoryCamera(mode: mode, customTarget: nil, resumeLiveStream: false, transitionIn: nil, transitionedIn: {}, transitionOut: { _, _ in return nil }) + coordinator?.animateIn() + } case let .settings(section): dismissInput() switch section { - case .theme: - if let navigationController = navigationController { - let controller = themeSettingsController(context: context) - controller.navigationPresentation = .modal - - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ThemeSettingsController) } - controllers.append(controller) - - navigationController.setViewControllers(controllers, animated: true) - } - case .devices: - if let navigationController = navigationController { - let activeSessions = deferred { () -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in - let activeSessionsContext = context.engine.privacy.activeSessions() - let webSessionsContext = context.engine.privacy.webSessions() - let otherSessionCount = activeSessionsContext.state - |> map { state -> Int in - return state.sessions.filter({ !$0.isCurrent }).count - } - |> distinctUntilChanged - - return otherSessionCount - |> map { value in - return (activeSessionsContext, value, webSessionsContext) + case let .path(path): + if let navigationController { + if path.isEmpty { + if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootController { + rootController.openSettings(edit: false) } + return } - - let _ = (activeSessions - |> take(1) - |> deliverOnMainQueue).start(next: { activeSessionsContext, count, webSessionsContext in - let controller = recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false) + handleSettingsPathUrl(context: context, path: path, navigationController: navigationController) + } + case let .legacy(legacySection): + switch legacySection { + case .theme: + if let navigationController { + let controller = themeSettingsController(context: context) controller.navigationPresentation = .modal var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is RecentSessionsController) } - controllers.append(controller) - - navigationController.setViewControllers(controllers, animated: true) - }) - } - case .autoremoveMessages: - let _ = (context.engine.privacy.requestAccountPrivacySettings() - |> take(1) - |> deliverOnMainQueue).start(next: { settings in - navigationController?.pushViewController(globalAutoremoveScreen(context: context, initialValue: settings.messageAutoremoveTimeout ?? 0, updated: { _ in }), animated: true) - }) - case .twoStepAuth: - break - case .enableLog: - if let navigationController = navigationController { - let _ = updateLoggingSettings(accountManager: context.sharedContext.accountManager, { - $0.withUpdatedLogToFile(true) - }).start() - - if let controller = context.sharedContext.makeDebugSettingsController(context: context) { - var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ThemeSettingsController) } controllers.append(controller) navigationController.setViewControllers(controllers, animated: true) } - } - case .phonePrivacy: - let privacySignal = context.engine.privacy.requestAccountPrivacySettings() - let _ = (privacySignal - |> deliverOnMainQueue).start(next: { info in - let current: SelectivePrivacySettings = info.phoneNumber - if let navigationController = navigationController { - let controller = selectivePrivacySettingsController(context: context, kind: .phoneNumber, current: current, phoneDiscoveryEnabled: info.phoneDiscoveryEnabled, updated: { _, _, _, _ in + case .devices: + if let navigationController { + let activeSessions = deferred { () -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in + let activeSessionsContext = context.engine.privacy.activeSessions() + let webSessionsContext = context.engine.privacy.webSessions() + let otherSessionCount = activeSessionsContext.state + |> map { state -> Int in + return state.sessions.filter({ !$0.isCurrent }).count + } + |> distinctUntilChanged + + return otherSessionCount + |> map { value in + return (activeSessionsContext, value, webSessionsContext) + } + } + + let _ = (activeSessions + |> take(1) + |> deliverOnMainQueue).start(next: { activeSessionsContext, count, webSessionsContext in + let controller = recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false) + controller.navigationPresentation = .modal + + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is RecentSessionsController) } + controllers.append(controller) + + navigationController.setViewControllers(controllers, animated: true) }) - controller.navigationPresentation = .modal + } + case .autoremoveMessages: + let _ = (context.engine.privacy.requestAccountPrivacySettings() + |> take(1) + |> deliverOnMainQueue).start(next: { settings in + navigationController?.pushViewController(globalAutoremoveScreen(context: context, initialValue: settings.messageAutoremoveTimeout ?? 0, updated: { _ in }), animated: true) + }) + case .enableLog: + if let navigationController = navigationController { + let _ = updateLoggingSettings(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedLogToFile(true) + }).start() + + if let controller = context.sharedContext.makeDebugSettingsController(context: context) { + var controllers = navigationController.viewControllers + controllers.append(controller) + + navigationController.setViewControllers(controllers, animated: true) + } + } + case .phonePrivacy: + let privacySignal = context.engine.privacy.requestAccountPrivacySettings() + let _ = (privacySignal + |> deliverOnMainQueue).start(next: { info in + let current: SelectivePrivacySettings = info.phoneNumber + if let navigationController = navigationController { + let controller = selectivePrivacySettingsController(context: context, kind: .phoneNumber, current: current, phoneDiscoveryEnabled: info.phoneDiscoveryEnabled, updated: { _, _, _, _ in + }) + controller.navigationPresentation = .modal + navigationController.pushViewController(controller) + } + }) + case .loginEmail: + if let navigationController { + let controller = loginEmailSetupController(context: context, blocking: false, emailPattern: nil, navigationController: navigationController, completion: {}, dismiss: {}) navigationController.pushViewController(controller) } - }) - case .loginEmail: - if let navigationController { - let controller = loginEmailSetupController(context: context, blocking: false, emailPattern: nil, navigationController: navigationController, completion: {}, dismiss: {}) - navigationController.pushViewController(controller) } } case let .premiumOffer(reference): @@ -853,12 +1005,12 @@ func openResolvedUrlImpl( dismissInput() if let starsContext = context.starsContext { let proceed = { - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: .topUp(requiredStars: amount, purpose: purpose), targetPeerId: nil, customTheme: nil, completion: { _ in }) + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: [], purpose: amount.flatMap { .topUp(requiredStars: $0, purpose: purpose) } ?? .generic, targetPeerId: nil, customTheme: nil, completion: { _ in }) if let navigationController = navigationController { navigationController.pushViewController(controller, animated: true) } } - if let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) { + if let amount, let currentState = starsContext.currentState, currentState.balance >= StarsAmount(value: amount, nanos: 0) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = UndoOverlayController( presentationData: presentationData, @@ -1628,5 +1780,53 @@ func openResolvedUrlImpl( navigationController?.pushViewController(controller) } } + case let .unknownDeepLink(path): + let _ = (context.engine.resolve.getDeepLinkInfo(path: path) + |> deliverOnMainQueue).start(next: { result in + guard let result else { + return + } + + let actions: [AlertScreen.Action] + if result.updateApp { + actions = [ + .init(title: presentationData.strings.Common_NotNow, type: .generic), + .init(title: presentationData.strings.Application_Update, type: .default, action: { + context.sharedContext.applicationBindings.openAppStorePage() + }) + ] + } else { + actions = [ + .init(title: presentationData.strings.Common_OK, type: .default) + ] + } + + let content: [AnyComponentWithIdentity] = [ + AnyComponentWithIdentity(id: "text", component: AnyComponent(AlertTextComponent(content: .textWithEntities(context, result.message, result.entities)))) + ] + + let alertController = AlertScreen( + context: context, + content: content, + actions: actions + ) + present(alertController, nil) + }) + case let .oauth(url): + let _ = (context.engine.messages.requestMessageActionUrlAuth(subject: .url(url)) + |> deliverOnMainQueue).start(next: { result in + if case .request = result { + var dismissImpl: (() -> Void)? + let controller = AuthConfirmationScreen(context: context, subject: result, completion: { allowWriteAccess, sharePhoneNumber in + let _ = context.engine.messages.acceptMessageActionUrlAuth(subject: .url(url), allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber).start(next: { _ in + dismissImpl?() + }) + }) + navigationController?.pushViewController(controller) + dismissImpl = { + controller.dismissAnimated() + } + } + }) } } diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index bc32dcc759..e2c63e3479 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -36,71 +36,71 @@ public func parseProxyUrl(sharedContext: SharedAccountContext, url: URL) -> Prox } } +public func isOAuthUrl(_ url: URL) -> Bool { + guard let query = url.query, let params = QueryParameters(query), ["oauth", "resolve"].contains(url.host) else { + return false + } + + let domain = params["domain"] + let startApp = params["startapp"] + let token = params["token"] + + var valid = false + if url.host == "resolve" { + if domain == "oauth", let _ = startApp { + valid = true + } + } else { + if let _ = token { + valid = true + } + } + + return valid +} + public func parseSecureIdUrl(_ url: URL) -> ParsedSecureIdUrl? { - guard let query = url.query else { + guard let query = url.query, let params = QueryParameters(query), ["passport", "resolve"].contains(url.host) else { return nil } - if url.host == "passport" || url.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var botId: Int64? - var scope: String? - var publicKey: String? - var callbackUrl: String? - var opaquePayload = Data() - var opaqueNonce = Data() - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "bot_id" { - botId = Int64(value) - } else if queryItem.name == "scope" { - scope = value - } else if queryItem.name == "public_key" { - publicKey = value - } else if queryItem.name == "callback_url" { - callbackUrl = value - } else if queryItem.name == "payload" { - if let data = value.data(using: .utf8) { - opaquePayload = data - } - } else if queryItem.name == "nonce" { - if let data = value.data(using: .utf8) { - opaqueNonce = data - } - } - } + let domain = params["domain"] + let botId = params["bot_id"].flatMap(Int64.init) + let scope = params["scope"] + let publicKey = params["public_key"] + let callbackUrl = params["callback_url"] + var opaquePayload = Data() + var opaqueNonce = Data() + if let payloadValue = params["payload"], let data = payloadValue.data(using: .utf8) { + opaquePayload = data + } + if let nonceValue = params["nonce"], let data = nonceValue.data(using: .utf8) { + opaqueNonce = data + } + + let valid: Bool + if url.host == "resolve" { + if domain == "telegrampassport" { + valid = true + } else { + valid = false + } + } else { + valid = true + } + + if valid { + if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl { + if scope.hasPrefix("{") && scope.hasSuffix("}") { + opaquePayload = Data() + if opaqueNonce.isEmpty { + return nil } + } else if opaquePayload.isEmpty { + return nil } - let valid: Bool - if url.host == "resolve" { - if domain == "telegrampassport" { - valid = true - } else { - valid = false - } - } else { - valid = true - } - - if valid { - if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl { - if scope.hasPrefix("{") && scope.hasSuffix("}") { - opaquePayload = Data() - if opaqueNonce.isEmpty { - return nil - } - } else if opaquePayload.isEmpty { - return nil - } - - return ParsedSecureIdUrl(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce) - } - } + return ParsedSecureIdUrl(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce) } } @@ -116,10 +116,10 @@ public func parseConfirmationCodeUrl(sharedContext: SharedAccountContext, url: U if url.scheme == "tg" { if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query) { switch parsedUrl { - case let .confirmationCode(code): - return code - default: - break + case let .confirmationCode(code): + return code + default: + break } } } @@ -139,6 +139,232 @@ func formattedConfirmationCode(_ code: Int) -> String { return result } +private func canonicalExternalUrl(from url: String) -> URL? { + var urlWithScheme = url + if !url.contains("://") && !url.hasPrefix("mailto:") { + urlWithScheme = "http://" + url + } + if let parsed = URL(string: urlWithScheme) { + return parsed + } else if let encoded = (urlWithScheme as NSString).addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) { + return URL(string: encoded) + } + return nil +} + +private func makeResolvedUrlHandler( + context: AccountContext, + presentationData: PresentationData, + navigationController: NavigationController?, + dismissInput: @escaping () -> Void +) -> (ResolvedUrl) -> Void { + return { resolved in + if case let .externalUrl(value) = resolved { + context.sharedContext.applicationBindings.openUrl(value) + } else { + context.sharedContext.openResolvedUrl( + resolved, + context: context, + urlContext: .generic, + navigationController: navigationController, + forceExternal: false, + forceUpdate: false, + openPeer: { peer, navigation in + switch navigation { + case .info: + if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + context.sharedContext.applicationBindings.dismissNativeController() + navigationController?.pushViewController(infoController) + } + case let .chat(textInputState, subject, peekData): + context.sharedContext.applicationBindings.dismissNativeController() + if let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, updateTextInputState: !peer.id.isGroupOrChannel ? textInputState : nil, peekData: peekData)) + } + case let .withBotStartPayload(payload): + context.sharedContext.applicationBindings.dismissNativeController() + if let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botStart: payload)) + } + case let .withAttachBot(attachBotStart): + context.sharedContext.applicationBindings.dismissNativeController() + if let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + } + case let .withBotApp(botAppStart): + context.sharedContext.applicationBindings.dismissNativeController() + if let navigationController { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } + default: + break + } + }, + sendFile: nil, + sendSticker: nil, + sendEmoji: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: { _, _, _ in }, + present: { c, a in + context.sharedContext.applicationBindings.dismissNativeController() + c.presentationArguments = a + context.sharedContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {}) + }, + dismissInput: { + dismissInput() + }, + contentContext: nil, + progress: nil, + completion: nil + ) + } + } +} + +private func makeInternalUrlHandler( + context: AccountContext, + resolvedHandler: @escaping (ResolvedUrl) -> Void +) -> (String) -> Void { + return { url in + let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: url, skipUrlAuth: true) + |> deliverOnMainQueue).startStandalone(next: resolvedHandler) + } +} + +private let internetSchemes: [String] = ["http", "https"] +private let telegramMeHosts: [String] = ["t.me", "telegram.me", "telegram.dog"] + +private func handleInternetUrl( + parsedUrl: URL, + originalUrl: String, + context: AccountContext, + presentationData: PresentationData, + navigationController: NavigationController?, + handleInternalUrl: @escaping (String) -> Void +) { + let urlScheme = (parsedUrl.scheme ?? "").lowercased() + var isInternetUrl = false + if internetSchemes.contains(urlScheme) { + isInternetUrl = true + } + if urlScheme == "tonsite" { + isInternetUrl = true + } + + if isInternetUrl { + if let host = parsedUrl.host, telegramMeHosts.contains(host) { + handleInternalUrl(parsedUrl.absoluteString) + } else { + let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData()) + |> take(1) + |> map { sharedData, accessChallengeData -> WebBrowserSettings in + let passcodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]?.get(PresentationPasscodeSettings.self) ?? PresentationPasscodeSettings.defaultSettings + + var settings: WebBrowserSettings + if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) { + settings = current + } else { + settings = .defaultSettings + } + if accessChallengeData.data.isLockable { + if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { + settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) + } + } + return settings + } + + let _ = (settings + |> deliverOnMainQueue).startStandalone(next: { settings in + var isTonSite = false + if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") { + isTonSite = true + } else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") { + isTonSite = true + } + + if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite { + let openInOptions = availableOpenInOptions(context: context, item: .url(url: originalUrl)) + if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { + if case let .openUrl(openInUrl) = option.action() { + context.sharedContext.applicationBindings.openUrl(openInUrl) + } else { + context.sharedContext.applicationBindings.openUrl(originalUrl) + } + } else { + context.sharedContext.applicationBindings.openUrl(originalUrl) + } + } else { + var isExceptedDomain = false + let host = ".\((parsedUrl.host ?? "").lowercased())" + for exception in settings.exceptions { + if host.hasSuffix(".\(exception.domain)") { + isExceptedDomain = true + break + } + } + + if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { + let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) + navigationController?.pushViewController(controller) + } else { + if let window = navigationController?.view.window, !isExceptedDomain { + let controller = SFSafariViewController(url: parsedUrl) + controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor + controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor + window.rootViewController?.present(controller, animated: true) + } else { + context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) + } + } + } + }) + } + } else { + context.sharedContext.applicationBindings.openUrl(originalUrl) + } +} + +private struct QueryParameters { + private let map: [String: [String?]] + let items: [URLQueryItem] + + init?(_ query: String) { + guard let components = URLComponents(string: "/?" + query) else { + return nil + } + let queryItems = components.queryItems ?? [] + self.items = queryItems + + var map: [String: [String?]] = [:] + for item in queryItems { + map[item.name, default: []].append(item.value) + } + self.map = map + } + + subscript(_ name: String) -> String? { + return self.map[name]?.first ?? nil + } +} + +private func appendQueryItems(to base: String, items: [URLQueryItem]) -> String { + guard !items.isEmpty else { + return base + } + var components = URLComponents() + components.queryItems = items + guard let query = components.percentEncodedQuery, !query.isEmpty else { + return base + } + let separator = base.contains("?") ? "&" : "?" + return base + separator + query +} + +private func makeTelegramUrl(_ path: String, queryItems: [URLQueryItem] = []) -> String { + return appendQueryItems(to: "https://t.me\(path)", items: queryItems) +} + func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { if forceExternal || url.lowercased().hasPrefix("tel:") || url.lowercased().hasPrefix("calshow:") { if url.lowercased().hasPrefix("tel:+888") { @@ -152,25 +378,16 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur return } - var parsedUrlValue: URL? - var urlWithScheme = url - if !url.contains("://") && !url.hasPrefix("mailto:") { - urlWithScheme = "http://" + url - } - if let parsed = URL(string: urlWithScheme) { - parsedUrlValue = parsed - } else if let encoded = (urlWithScheme as NSString).addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), let parsed = URL(string: encoded) { - parsedUrlValue = parsed + guard let canonicalUrl = canonicalExternalUrl(from: url) else { + return } - if let parsedUrlValue = parsedUrlValue, parsedUrlValue.scheme == "mailto" { + if canonicalUrl.scheme == "mailto" { context.sharedContext.applicationBindings.openUrl(url) return } - guard var parsedUrl = parsedUrlValue else { - return - } + var parsedUrl = canonicalUrl if let host = parsedUrl.host?.lowercased() { if host == "itunes.apple.com" { @@ -192,65 +409,18 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } } + let handleResolvedUrl = makeResolvedUrlHandler( + context: context, + presentationData: presentationData, + navigationController: navigationController, + dismissInput: dismissInput + ) + let handleInternalUrl = makeInternalUrlHandler( + context: context, + resolvedHandler: handleResolvedUrl + ) + let continueHandling: () -> Void = { - let handleResolvedUrl: (ResolvedUrl) -> Void = { resolved in - if case let .externalUrl(value) = resolved { - context.sharedContext.applicationBindings.openUrl(value) - } else { - context.sharedContext.openResolvedUrl(resolved, context: context, urlContext: .generic, navigationController: navigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in - switch navigation { - case .info: - if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { - context.sharedContext.applicationBindings.dismissNativeController() - navigationController?.pushViewController(infoController) - } - case let .chat(textInputState, subject, peekData): - context.sharedContext.applicationBindings.dismissNativeController() - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, updateTextInputState: !peer.id.isGroupOrChannel ? textInputState : nil, peekData: peekData)) - } - case let .withBotStartPayload(payload): - context.sharedContext.applicationBindings.dismissNativeController() - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botStart: payload)) - } - case let .withAttachBot(attachBotStart): - context.sharedContext.applicationBindings.dismissNativeController() - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) - } - case let .withBotApp(botAppStart): - context.sharedContext.applicationBindings.dismissNativeController() - if let navigationController = navigationController { - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart)) - } - default: - break - } - }, - sendFile: nil, - sendSticker: nil, - sendEmoji: nil, - requestMessageActionUrlAuth: nil, - joinVoiceChat: { peerId, invite, call in - - }, present: { c, a in - context.sharedContext.applicationBindings.dismissNativeController() - - c.presentationArguments = a - - context.sharedContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {}) - }, dismissInput: { - dismissInput() - }, contentContext: nil, progress: nil, completion: nil) - } - } - - let handleInternalUrl: (String) -> Void = { url in - let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: url, skipUrlAuth: true) - |> deliverOnMainQueue).startStandalone(next: handleResolvedUrl) - } - if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { if parsedUrl.host == "tonsite" { if let value = URL(string: "tonsite:/" + parsedUrl.path) { @@ -261,821 +431,239 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) { var convertedUrl: String? - if let query = parsedUrl.query { - if parsedUrl.host == "localpeer" { - if let components = URLComponents(string: "/?" + query) { - var peerId: PeerId? - var accountId: Int64? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "id", let intValue = Int64(value) { - peerId = PeerId(intValue) - } else if queryItem.name == "accountId", let intValue = Int64(value) { - accountId = intValue - } - } + let host = parsedUrl.host?.lowercased() ?? "" + if let query = parsedUrl.query, let params = QueryParameters(query) { + switch host { + case "localpeer": + if let peerIdValue = params["id"].flatMap(Int64.init), let accountId = params["accountId"].flatMap(Int64.init) { + let peerId = PeerId(peerIdValue) + context.sharedContext.applicationBindings.dismissNativeController() + context.sharedContext.navigateToChat(accountId: AccountRecordId(rawValue: accountId), peerId: peerId, messageId: nil) + } + case "join": + if let invite = params["invite"] { + convertedUrl = makeTelegramUrl("/joinchat/\(invite)") + } + case "addstickers": + if let set = params["set"] { + convertedUrl = makeTelegramUrl("/addstickers/\(set)") + } + case "addemoji": + if let set = params["set"] { + convertedUrl = makeTelegramUrl("/addemoji/\(set)") + } + case "invoice": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/invoice/\(slug)") + } + case "setlanguage": + if let lang = params["lang"] { + convertedUrl = makeTelegramUrl("/setlanguage/\(lang)") + } + case "msg": + let sharePhoneNumber = params["to"] + let shareText = params["text"] + if sharePhoneNumber != nil || shareText != nil { + handleResolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber)) + return + } + case "msg_url": + if let shareUrl = params["url"] { + var queryItems: [URLQueryItem] = [URLQueryItem(name: "url", value: shareUrl)] + if let shareText = params["text"] { + queryItems.append(URLQueryItem(name: "text", value: shareText)) + } + convertedUrl = makeTelegramUrl("/share/url", queryItems: queryItems) + } + case "socks", "proxy": + let server = params["server"] ?? params["proxy"] + let port = params["port"] + let user = params["user"] + let pass = params["pass"] + let secret = params["secret"] + let secretHost = params["host"] + + if let server, !server.isEmpty, let port, let _ = Int32(port) { + var queryItems: [URLQueryItem] = [ + URLQueryItem(name: "proxy", value: server), + URLQueryItem(name: "port", value: port) + ] + if let user { + queryItems.append(URLQueryItem(name: "user", value: user)) + if let pass { + queryItems.append(URLQueryItem(name: "pass", value: pass)) } } - if let peerId = peerId, let accountId = accountId { + if let secret { + queryItems.append(URLQueryItem(name: "secret", value: secret)) + } + if let secretHost { + queryItems.append(URLQueryItem(name: "host", value: secretHost)) + } + convertedUrl = makeTelegramUrl("/proxy", queryItems: queryItems) + } + case "passport", "oauth", "resolve": + if isOAuthUrl(parsedUrl) { + handleResolvedUrl(.oauth(url: url)) + return + } else if let secureId = parseSecureIdUrl(parsedUrl) { + if case .chat = urlContext { + return + } + let controller = SecureIdAuthController(context: context, mode: .form(peerId: secureId.peerId, scope: secureId.scope, publicKey: secureId.publicKey, callbackUrl: secureId.callbackUrl, opaquePayload: secureId.opaquePayload, opaqueNonce: secureId.opaqueNonce)) + + if let navigationController = navigationController { context.sharedContext.applicationBindings.dismissNativeController() - context.sharedContext.navigateToChat(accountId: AccountRecordId(rawValue: accountId), peerId: peerId, messageId: nil) + + navigationController.view.window?.endEditing(true) + context.sharedContext.applicationBindings.getWindowHost()?.present(controller, on: .root, blockInteraction: false, completion: {}) + } + return + } + case "user": + if let idValue = params["id"].flatMap(Int64.init), idValue > 0 { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(idValue)))) + |> deliverOnMainQueue).startStandalone(next: { peer in + if let peer = peer, let controller = context.sharedContext.makePeerInfoController( + context: context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .generic, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController?.pushViewController(controller) + } + }) + return + } + case "login": + if let _ = params["token"] { + let alertController = textAlertController( + context: context, + title: nil, + text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint, + actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})], + parseMarkdown: true + ) + context.sharedContext.presentGlobalController(alertController, nil) + return + } + if let code = params["code"] { + convertedUrl = makeTelegramUrl("/login/\(code)") + } + case "contact": + if let token = params["token"] { + convertedUrl = makeTelegramUrl("/contact/\(token)") + } + case "confirmphone": + if let phone = params["phone"], let hash = params["hash"] { + let queryItems = [ + URLQueryItem(name: "phone", value: phone), + URLQueryItem(name: "hash", value: hash) + ] + convertedUrl = makeTelegramUrl("/confirmphone", queryItems: queryItems) + } + case "bg": + var parameter: String? + var queryItems: [URLQueryItem] = [] + for item in params.items { + guard let value = item.value else { + continue + } + switch item.name { + case "slug", "color", "gradient": + parameter = value + case "mode", "bg_color", "intensity", "rotation": + queryItems.append(URLQueryItem(name: item.name, value: value)) + default: + break } } - } else if parsedUrl.host == "join" { - if let components = URLComponents(string: "/?" + query) { - var invite: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "invite" { - invite = value - } - } - } - } - if let invite = invite { - convertedUrl = "https://t.me/joinchat/\(invite)" - } + if let parameter = parameter { + convertedUrl = makeTelegramUrl("/bg/\(parameter)", queryItems: queryItems) } - } else if parsedUrl.host == "addstickers" { - if let components = URLComponents(string: "/?" + query) { - var set: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "set" { - set = value - } - } - } - } - if let set = set { - convertedUrl = "https://t.me/addstickers/\(set)" - } + case "addtheme": + if let parameter = params["slug"] { + convertedUrl = makeTelegramUrl("/addtheme/\(parameter)") } - } else if parsedUrl.host == "addemoji" { - if let components = URLComponents(string: "/?" + query) { - var set: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "set" { - set = value - } - } - } - } - if let set = set { - convertedUrl = "https://t.me/addemoji/\(set)" - } + case "nft": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/nft/\(slug)") } - } else if parsedUrl.host == "invoice" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug = slug { - convertedUrl = "https://t.me/invoice/\(slug)" - } + case "stargift_auction": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/auction/\(slug)") } - } else if parsedUrl.host == "setlanguage" { - if let components = URLComponents(string: "/?" + query) { - var lang: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "lang" { - lang = value - } - } - } - } - if let lang = lang { - convertedUrl = "https://t.me/setlanguage/\(lang)" - } - } - } else if parsedUrl.host == "msg" { - if let components = URLComponents(string: "/?" + query) { - var sharePhoneNumber: String? - var shareText: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "to" { - sharePhoneNumber = value - } else if queryItem.name == "text" { - shareText = value - } - } - } - } - if sharePhoneNumber != nil || shareText != nil { - handleResolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber)) - return - } - } - } else if parsedUrl.host == "msg_url" { - if let components = URLComponents(string: "/?" + query) { - var shareUrl: String? - var shareText: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "url" { - shareUrl = value - } else if queryItem.name == "text" { - shareText = value - } - } - } - } - if let shareUrl = shareUrl { - var resultUrl = "https://t.me/share/url?url=\(urlEncodedStringFromString(shareUrl))" - if let shareText = shareText { - resultUrl += "&text=\(urlEncodedStringFromString(shareText))" - } - convertedUrl = resultUrl - } - } - } else if parsedUrl.host == "socks" || parsedUrl.host == "proxy" { - if let components = URLComponents(string: "/?" + query) { - var server: String? - var port: String? - var user: String? - var pass: String? - var secret: String? - var secretHost: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "server" || queryItem.name == "proxy" { - server = value - } else if queryItem.name == "port" { - port = value - } else if queryItem.name == "user" { - user = value - } else if queryItem.name == "pass" { - pass = value - } else if queryItem.name == "secret" { - secret = value - } else if queryItem.name == "host" { - secretHost = value - } - } - } - } - - if let server = server, !server.isEmpty, let port = port, let _ = Int32(port) { - var result = "https://t.me/proxy?proxy=\(server)&port=\(port)" - if let user = user { - result += "&user=\((user as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - if let pass = pass { - result += "&pass=\((pass as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - } - } - if let secret = secret { - result += "&secret=\((secret as NSString).addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" - } - if let secretHost = secretHost?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) { - result += "&host=\(secretHost)" - } - convertedUrl = result - } - } - } else if parsedUrl.host == "passport" || parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var botId: Int64? - var scope: String? - var publicKey: String? - var callbackUrl: String? - var opaquePayload = Data() - var opaqueNonce = Data() - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "bot_id" { - botId = Int64(value) - } else if queryItem.name == "scope" { - scope = value - } else if queryItem.name == "public_key" { - publicKey = value - } else if queryItem.name == "callback_url" { - callbackUrl = value - } else if queryItem.name == "payload" { - if let data = value.data(using: .utf8) { - opaquePayload = data - } - } else if queryItem.name == "nonce" { - if let data = value.data(using: .utf8) { - opaqueNonce = data - } - } - } - } - } - - let valid: Bool - if parsedUrl.host == "resolve" { - if domain == "telegrampassport" { - valid = true - } else { - valid = false - } - } else { - valid = true - } - - if valid { - if let botId = botId, let scope = scope, let publicKey = publicKey { - if scope.hasPrefix("{") && scope.hasSuffix("}") { - opaquePayload = Data() - if opaqueNonce.isEmpty { - return - } - } else if opaquePayload.isEmpty { - return - } - if case .chat = urlContext { - return - } - let controller = SecureIdAuthController(context: context, mode: .form(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce)) - - if let navigationController = navigationController { - context.sharedContext.applicationBindings.dismissNativeController() - - navigationController.view.window?.endEditing(true) - context.sharedContext.applicationBindings.getWindowHost()?.present(controller, on: .root, blockInteraction: false, completion: {}) - } - } - return - } - } - } else if parsedUrl.host == "user" { - if let components = URLComponents(string: "/?" + query) { - var id: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "id" { - id = value - } - } - } - } - - if let id = id, !id.isEmpty, let idValue = Int64(id), idValue > 0 { - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(idValue)))) - |> deliverOnMainQueue).startStandalone(next: { peer in - if let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { - navigationController?.pushViewController(controller) - } - }) - return - } - } - } else if parsedUrl.host == "login" { - if let components = URLComponents(string: "/?" + query) { - var code: String? - var isToken: Bool = false - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "code" { - code = value - } - } - if queryItem.name == "token" { - isToken = true - } - } - } - if isToken { - context.sharedContext.presentGlobalController(textAlertController(context: context, title: nil, text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}), - ], parseMarkdown: true), nil) - return - } - if let code = code { - convertedUrl = "https://t.me/login/\(code)" - } - } - } else if parsedUrl.host == "contact" { - if let components = URLComponents(string: "/?" + query) { - var token: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "token" { - token = value - } - } - } - } - if let token = token { - convertedUrl = "https://t.me/contact/\(token)" - } - } - } else if parsedUrl.host == "confirmphone" { - if let components = URLComponents(string: "/?" + query) { - var phone: String? - var hash: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "phone" { - phone = value - } else if queryItem.name == "hash" { - hash = value - } - } - } - } - if let phone = phone, let hash = hash { - convertedUrl = "https://t.me/confirmphone?phone=\(phone)&hash=\(hash)" - } - } - } else if parsedUrl.host == "bg" { - if let components = URLComponents(string: "/?" + query) { - var parameter: String? - var query: [String] = [] - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - parameter = value - } else if queryItem.name == "color" { - parameter = value - } else if queryItem.name == "gradient" { - parameter = value - } else if queryItem.name == "mode" { - query.append("mode=\(value)") - } else if queryItem.name == "bg_color" { - query.append("bg_color=\(value)") - } else if queryItem.name == "intensity" { - query.append("intensity=\(value)") - } else if queryItem.name == "rotation" { - query.append("rotation=\(value)") - } - } - } - } - var queryString = "" - if !query.isEmpty { - queryString = "?\(query.joined(separator: "&"))" - } - if let parameter = parameter { - convertedUrl = "https://t.me/bg/\(parameter)\(queryString)" - } - } - } else if parsedUrl.host == "addtheme" { - if let components = URLComponents(string: "/?" + query) { - var parameter: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - parameter = value - } - } - } - } - if let parameter = parameter { - convertedUrl = "https://t.me/addtheme/\(parameter)" - } - } - } else if parsedUrl.host == "nft" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug { - convertedUrl = "https://t.me/nft/\(slug)" - } - } - } else if parsedUrl.host == "stargift_auction" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug { - convertedUrl = "https://t.me/auction/\(slug)" - } - } - } else if parsedUrl.host == "privatepost" { - if let components = URLComponents(string: "/?" + query) { - var channelId: Int64? - var postId: Int32? - var threadId: Int64? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "channel" { - channelId = Int64(value) - } else if queryItem.name == "post" { - postId = Int32(value) - } else if queryItem.name == "thread" { - threadId = Int64(value) - } - } - } - } - if let channelId = channelId { - if let postId = postId { - if let threadId = threadId { - convertedUrl = "https://t.me/c/\(channelId)/\(threadId)/\(postId)" - } else { - convertedUrl = "https://t.me/c/\(channelId)/\(postId)" - } - } else if let threadId = threadId { - convertedUrl = "https://t.me/c/\(channelId)/\(threadId)" - } - } - } - } else if parsedUrl.host == "giftcode" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug { - convertedUrl = "https://t.me/giftcode/\(slug)" - } - } - } else if parsedUrl.host == "message" { - if let components = URLComponents(string: "/?" + query) { - var parameter: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - parameter = value - } - } - } - } - if let parameter { - convertedUrl = "https://t.me/m/\(parameter)" - } - } - } - - if parsedUrl.host == "resolve" { - if let components = URLComponents(string: "/?" + query) { - var phone: String? - var domain: String? - var start: String? - var startGroup: String? - var startChannel: String? - var admin: String? - var game: String? - var post: String? - var voiceChat: String? - var attach: String? - var startAttach: String? - var choose: String? - var threadId: Int64? - var appName: String? - var startApp: String? - var text: String? - var profile: Bool = false - var direct: Bool = false - var referrer: String? - var albumId: Int64? - var collectionId: Int64? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "phone" { - phone = value - } else if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "start" { - start = value - } else if queryItem.name == "startgroup" { - startGroup = value - } else if queryItem.name == "admin" { - admin = value - } else if queryItem.name == "game" { - game = value - } else if queryItem.name == "post" { - post = value - } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { - voiceChat = value - } else if queryItem.name == "attach" { - attach = value - } else if queryItem.name == "startattach" { - startAttach = value - } else if queryItem.name == "choose" { - choose = value - } else if queryItem.name == "thread" { - threadId = Int64(value) - } else if queryItem.name == "appname" { - appName = value - } else if queryItem.name == "startapp" { - startApp = value - } else if queryItem.name == "text" { - text = value - } else if queryItem.name == "ref" { - referrer = value - } else if queryItem.name == "album" { - albumId = Int64(value) - } else if queryItem.name == "collection" { - collectionId = Int64(value) - } - } else if ["voicechat", "videochat", "livestream"].contains(queryItem.name) { - voiceChat = "" - } else if queryItem.name == "startattach" { - startAttach = "" - } else if queryItem.name == "startgroup" { - startGroup = "" - } else if queryItem.name == "startchannel" { - startChannel = "" - } else if queryItem.name == "profile" { - profile = true - } else if queryItem.name == "direct" { - direct = true - } else if queryItem.name == "startapp" { - startApp = "" - } - } - } - - if let phone = phone { - var result = "https://t.me/+\(phone)" - if let text = text { - result += "?text=\(text)" - } - convertedUrl = result - } else if let domain = domain { - var result = "https://t.me/\(domain)" - if let appName { - result += "/\(appName)" - } - if let startApp { - result += "?startapp=\(startApp)" - } + case "privatepost": + let channelId = params["channel"].flatMap(Int64.init) + let postId = params["post"].flatMap(Int32.init) + let threadId = params["thread"].flatMap(Int64.init) + + if let channelId { + if let postId { if let threadId { - result += "/\(threadId)" - if let post, let postValue = Int(post) { - result += "/\(postValue)" - } + convertedUrl = makeTelegramUrl("/c/\(channelId)/\(threadId)/\(postId)") } else { - if let post, let postValue = Int(post) { - result += "/\(postValue)" - } - } - if let start = start { - result += "?start=\(start)" - } else if let startGroup = startGroup { - if !startGroup.isEmpty { - result += "?startgroup=\(startGroup)" - } else { - result += "?startgroup" - } - if let admin = admin { - result += "&admin=\(admin)" - } - } else if let startChannel = startChannel { - if !startChannel.isEmpty { - result += "?startchannel=\(startChannel)" - } else { - result += "?startchannel" - } - if let admin = admin { - result += "&admin=\(admin)" - } - } else if let game = game { - result += "?game=\(game)" - } else if let voiceChat = voiceChat { - if !voiceChat.isEmpty { - result += "?voicechat=\(voiceChat)" - } else { - result += "?voicechat=" - } - } else if let attach = attach { - result += "?attach=\(attach)" - } else if let albumId { - result += "/a/\(albumId)" - } else if let collectionId { - result += "/c/\(collectionId)" - } - if let startAttach = startAttach { - if attach == nil { - result += "?" - } else { - result += "&" - } - if !startAttach.isEmpty { - result += "startattach=\(startAttach)" - } else { - result += "startattach" - } - if let choose = choose { - result += "&choose=\(choose)" - } - } - if let text = text { - result += "?text=\(text)" - } - if let referrer { - result += "?ref=\(referrer)" - } - convertedUrl = result - } - if profile, let current = convertedUrl { - if current.contains("?") { - convertedUrl = current + "&profile" - } else { - convertedUrl = current + "?profile" - } - } - if direct, let current = convertedUrl { - if current.contains("?") { - convertedUrl = current + "&direct" - } else { - convertedUrl = current + "?direct" + convertedUrl = makeTelegramUrl("/c/\(channelId)/\(postId)") } + } else if let threadId { + convertedUrl = makeTelegramUrl("/c/\(channelId)/\(threadId)") } } - } else if parsedUrl.host == "hostOverride" { - if let components = URLComponents(string: "/?" + query) { - var host: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "host" { - host = value - } - } - } - } - if let host = host { - let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in - var settings = settings - settings.backupHostOverride = host - return settings - }).startStandalone() - return - } + case "giftcode": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/giftcode/\(slug)") } - } else if parsedUrl.host == "premium_offer" { - var reference: String? - if let components = URLComponents(string: "/?" + query) { - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "ref" { - reference = value - } - } - } - } + case "message": + if let parameter = params["slug"] { + convertedUrl = makeTelegramUrl("/m/\(parameter)") } + case "hostoverride": + if let override = params["host"] { + let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in + var settings = settings + settings.backupHostOverride = override + return settings + }).startStandalone() + return + } + case "premium_offer": + let reference = params["ref"] handleResolvedUrl(.premiumOffer(reference: reference)) - } else if parsedUrl.host == "premium_multigift" { - var reference: String? - if let components = URLComponents(string: "/?" + query) { - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "ref" { - reference = value - } - } - } - } - } + case "premium_multigift": + let reference = params["ref"] handleResolvedUrl(.premiumMultiGift(reference: reference)) - } else if parsedUrl.host == "stars_topup" { - var amount: Int64? - var purpose: String? - if let components = URLComponents(string: "/?" + query) { - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "balance", let amountValue = Int64(value), amountValue > 0 && amountValue < Int32.max { - amount = amountValue - } else if queryItem.name == "purpose" { - purpose = value - } - } - } - } - } - if let amount { + case "stars_topup": + let amount = params["balance"].flatMap(Int64.init) + let purpose = params["purpose"] + if let amount, amount > 0 && amount < Int64(Int32.max) { handleResolvedUrl(.starsTopup(amount: amount, purpose: purpose)) + } else { + handleResolvedUrl(.starsTopup(amount: nil, purpose: purpose)) } - } else if parsedUrl.host == "addlist" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug = slug { - convertedUrl = "https://t.me/addlist/\(slug)" - } + case "addlist": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/addlist/\(slug)") } - } else if parsedUrl.host == "boost" { - if let components = URLComponents(string: "/?" + query) { - var domain: String? - var channel: Int64? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "domain" { - domain = value - } else if queryItem.name == "channel" { - channel = Int64(value) - } - } - } - } - if let domain { - convertedUrl = "https://t.me/\(domain)?boost" - } else if let channel { - convertedUrl = "https://t.me/c/\(channel)?boost" - } + case "boost": + if let domain = params["domain"] { + convertedUrl = makeTelegramUrl("/\(domain)", queryItems: [URLQueryItem(name: "boost", value: nil)]) + } else if let channel = params["channel"].flatMap(Int64.init) { + convertedUrl = makeTelegramUrl("/c/\(channel)", queryItems: [URLQueryItem(name: "boost", value: nil)]) } - } else if parsedUrl.host == "call" { - if let components = URLComponents(string: "/?" + query) { - var slug: String? - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "slug" { - slug = value - } - } - } - } - if let slug = slug { - convertedUrl = "https://t.me/call/\(slug)" - } + case "call": + if let slug = params["slug"] { + convertedUrl = makeTelegramUrl("/call/\(slug)") } - } else if parsedUrl.host == "shareStory" { - if let components = URLComponents(string: "/?" + query) { - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "session", let sessionId = Int64(value) { - handleResolvedUrl(.shareStory(sessionId)) - break - } - } - } - } + case "sharestory": + if let session = params["session"].flatMap(Int64.init) { + handleResolvedUrl(.shareStory(session)) + return } - } else if parsedUrl.host == "send_gift" { - var recipient: String? - if let components = URLComponents(string: "/?" + query) { - if let queryItems = components.queryItems { - for queryItem in queryItems { - if let value = queryItem.value { - if queryItem.name == "to" { - recipient = value - break - } - } - } - } - } - if let recipient { + case "send_gift": + if let recipient = params["to"] { if let id = Int64(recipient) { handleResolvedUrl(.sendGift(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)))) } else { @@ -1090,40 +678,188 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur } else { handleResolvedUrl(.sendGift(peerId: nil)) } + default: + break } - } else { - if parsedUrl.host == "stars" { - handleResolvedUrl(.stars) - } else if parsedUrl.host == "ton" { - handleResolvedUrl(.ton) - } else if parsedUrl.host == "importStickers" { - handleResolvedUrl(.importStickers) - } else if parsedUrl.host == "settings" { - if let path = parsedUrl.pathComponents.last { - var section: ResolvedUrlSettingsSection? - switch path { - case "themes": - section = .theme - case "devices": - section = .devices - case "password": - section = .twoStepAuth - case "enable_log": - section = .enableLog - case "phone_privacy": - section = .phonePrivacy - case "login_email": - section = .loginEmail - default: - break - } - if let section = section { - handleResolvedUrl(.settings(section)) + + if host == "resolve" { + var phone: String? + var domain: String? + var start: String? + var startGroup: String? + var startChannel: String? + var admin: String? + var game: String? + var post: String? + var voiceChat: String? + var attach: String? + var startAttach: String? + var choose: String? + var threadId: Int64? + var appName: String? + var startApp: String? + var text: String? + var profile = false + var direct = false + var referrer: String? + var albumId: Int64? + var collectionId: Int64? + + for queryItem in params.items { + if let value = queryItem.value { + switch queryItem.name { + case "phone": + phone = value + case "domain": + domain = value + case "start": + start = value + case "startgroup": + startGroup = value + case "admin": + admin = value + case "game": + game = value + case "post": + post = value + case "voicechat", "videochat", "livestream": + voiceChat = value + case "attach": + attach = value + case "startattach": + startAttach = value + case "choose": + choose = value + case "thread": + threadId = Int64(value) + case "appname": + appName = value + case "startapp": + startApp = value + case "text": + text = value + case "ref": + referrer = value + case "album": + albumId = Int64(value) + case "collection": + collectionId = Int64(value) + default: + break + } + } else { + switch queryItem.name { + case "voicechat", "videochat", "livestream": + voiceChat = "" + case "startattach": + startAttach = "" + case "startgroup": + startGroup = "" + case "startchannel": + startChannel = "" + case "profile": + profile = true + case "direct": + direct = true + case "startapp": + startApp = "" + default: + break + } } } - } else if parsedUrl.host == "premium_offer" { + + if let phone = phone { + var queryItems: [URLQueryItem] = [] + if let text { + queryItems.append(URLQueryItem(name: "text", value: text)) + } + if let referrer { + queryItems.append(URLQueryItem(name: "ref", value: referrer)) + } + if profile { + queryItems.append(URLQueryItem(name: "profile", value: nil)) + } + if direct { + queryItems.append(URLQueryItem(name: "direct", value: nil)) + } + convertedUrl = makeTelegramUrl("/+\(phone)", queryItems: queryItems) + } else if let domain = domain { + var path = "/\(domain)" + if let appName { + path += "/\(appName)" + } + if let threadId { + path += "/\(threadId)" + if let post, let postValue = Int(post) { + path += "/\(postValue)" + } + } else if let post, let postValue = Int(post) { + path += "/\(postValue)" + } + if let albumId { + path += "/a/\(albumId)" + } else if let collectionId { + path += "/c/\(collectionId)" + } + + var queryItems: [URLQueryItem] = [] + if let startApp { + queryItems.append(URLQueryItem(name: "startapp", value: startApp.isEmpty ? "" : startApp)) + } + if let start { + queryItems.append(URLQueryItem(name: "start", value: start)) + } else if let startGroup { + queryItems.append(URLQueryItem(name: "startgroup", value: startGroup.isEmpty ? nil : startGroup)) + if let admin { + queryItems.append(URLQueryItem(name: "admin", value: admin)) + } + } else if let startChannel { + queryItems.append(URLQueryItem(name: "startchannel", value: startChannel.isEmpty ? nil : startChannel)) + if let admin = admin { + queryItems.append(URLQueryItem(name: "admin", value: admin)) + } + } else if let game { + queryItems.append(URLQueryItem(name: "game", value: game)) + } else if let voiceChat { + queryItems.append(URLQueryItem(name: "voicechat", value: voiceChat.isEmpty ? "" : voiceChat)) + } else if let attach { + queryItems.append(URLQueryItem(name: "attach", value: attach)) + } + + if let startAttach { + queryItems.append(URLQueryItem(name: "startattach", value: startAttach.isEmpty ? nil : startAttach)) + if let choose { + queryItems.append(URLQueryItem(name: "choose", value: choose)) + } + } + if let text { + queryItems.append(URLQueryItem(name: "text", value: text)) + } + if let referrer { + queryItems.append(URLQueryItem(name: "ref", value: referrer)) + } + if profile { + queryItems.append(URLQueryItem(name: "profile", value: nil)) + } + if direct { + queryItems.append(URLQueryItem(name: "direct", value: nil)) + } + + convertedUrl = makeTelegramUrl(path, queryItems: queryItems) + } + } + } else { + switch host { + case "stars": + handleResolvedUrl(.stars) + case "ton": + handleResolvedUrl(.ton) + case "importstickers": + handleResolvedUrl(.importStickers) + case "premium_offer": handleResolvedUrl(.premiumOffer(reference: nil)) - } else if parsedUrl.host == "restore_purchases" { + case "restore_purchases": let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) context.sharedContext.presentGlobalController(statusController, nil) @@ -1132,120 +868,142 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur let text: String? switch result { - case let .succeed(serverProvided): - text = serverProvided ? nil : presentationData.strings.Premium_Restore_Success - case .failed: - text = presentationData.strings.Premium_Restore_ErrorUnknown + case let .succeed(serverProvided): + text = serverProvided ? nil : presentationData.strings.Premium_Restore_Success + case .failed: + text = presentationData.strings.Premium_Restore_ErrorUnknown } - if let text = text { + if let text { let alertController = textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) context.sharedContext.presentGlobalController(alertController, nil) } }) - } else if parsedUrl.host == "send_gift" { + case "send_gift": handleResolvedUrl(.sendGift(peerId: nil)) + case "contacts": + var section: ResolvedUrl.ContactsSection? + if let path = parsedUrl.pathComponents.last { + switch path { + case "search": + section = .search + case "sort": + section = .sort + case "new": + section = .new + case "invite": + section = .invite + case "manage": + section = .manage + default: + break + } + } + handleResolvedUrl(.contacts(section)) + case "chats": + var section: ResolvedUrl.ChatsSection? + if let path = parsedUrl.pathComponents.last { + switch path { + case "search": + section = .search + case "edit": + section = .edit + case "emoji-status": + section = .emojiStatus + default: + break + } + } + handleResolvedUrl(.chats(section)) + case "new": + var section: ResolvedUrl.ComposeSection? + if let path = parsedUrl.pathComponents.last { + switch path { + case "group": + section = .group + case "channel": + section = .channel + case "contact": + section = .contact + default: + break + } + } + handleResolvedUrl(.compose(section)) + case "post": + var section: ResolvedUrl.PostStorySection? + if let path = parsedUrl.pathComponents.last { + switch path { + case "photo": + section = .photo + case "video": + section = .video + case "live": + section = .live + default: + break + } + } + handleResolvedUrl(.postStory(section)) + case "settings": + if let lastComponent = parsedUrl.pathComponents.last { + var section: ResolvedUrl.SettingsSection? + switch lastComponent { + case "themes": + section = .legacy(.theme) + case "devices": + section = .legacy(.devices) + case "enable_log": + section = .legacy(.enableLog) + case "phone_privacy": + section = .legacy(.phonePrivacy) + case "login_email": + section = .legacy(.loginEmail) + default: + let fullPath = parsedUrl.pathComponents.joined(separator: "/").replacingOccurrences(of: "//", with: "") + section = .path(fullPath) + } + if let section { + handleResolvedUrl(.settings(section)) + } + } else { + handleResolvedUrl(.settings(.path(""))) + } + default: + break } } - if let convertedUrl = convertedUrl { + if let convertedUrl { handleInternalUrl(convertedUrl) + } else if let path = parsedUrl.host { + handleResolvedUrl(.unknownDeepLink(path: path)) } return } - let urlScheme = (parsedUrl.scheme ?? "").lowercased() - var isInternetUrl = false - if ["http", "https"].contains(urlScheme) { - isInternetUrl = true - } - if urlScheme == "tonsite" { - isInternetUrl = true - } - - if isInternetUrl { - if parsedUrl.host == "t.me" || parsedUrl.host == "telegram.me" || parsedUrl.host == "telegram.dog" { - handleInternalUrl(parsedUrl.absoluteString) - } else { - let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData()) - |> take(1) - |> map { sharedData, accessChallengeData -> WebBrowserSettings in - let passcodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]?.get(PresentationPasscodeSettings.self) ?? PresentationPasscodeSettings.defaultSettings - - var settings: WebBrowserSettings - if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) { - settings = current - } else { - settings = .defaultSettings - } - if accessChallengeData.data.isLockable { - if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" { - settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: []) - } - } - return settings - } - - let _ = (settings - |> deliverOnMainQueue).startStandalone(next: { settings in - var isTonSite = false - if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") { - isTonSite = true - } else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") { - isTonSite = true - } - - if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite { - let openInOptions = availableOpenInOptions(context: context, item: .url(url: url)) - if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) { - if case let .openUrl(openInUrl) = option.action() { - context.sharedContext.applicationBindings.openUrl(openInUrl) - } else { - context.sharedContext.applicationBindings.openUrl(url) - } - } else { - context.sharedContext.applicationBindings.openUrl(url) - } - } else { - var isExceptedDomain = false - let host = ".\((parsedUrl.host ?? "").lowercased())" - for exception in settings.exceptions { - if host.hasSuffix(".\(exception.domain)") { - isExceptedDomain = true - break - } - } - - if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite { - let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString)) - navigationController?.pushViewController(controller) - } else { - if let window = navigationController?.view.window, !isExceptedDomain { - let controller = SFSafariViewController(url: parsedUrl) - controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor - controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor - window.rootViewController?.present(controller, animated: true) - } else { - context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString) - } - } - } - }) - } - } else { - context.sharedContext.applicationBindings.openUrl(url) - } + handleInternetUrl( + parsedUrl: parsedUrl, + originalUrl: url, + context: context, + presentationData: presentationData, + navigationController: navigationController, + handleInternalUrl: handleInternalUrl + ) } - if parsedUrl.scheme == "http" || parsedUrl.scheme == "https" { - let nativeHosts = ["t.me", "telegram.me", "telegram.dog"] - if let host = parsedUrl.host, nativeHosts.contains(host) { + if let scheme = parsedUrl.scheme, internetSchemes.contains(scheme) { + if let host = parsedUrl.host, telegramMeHosts.contains(host) { continueHandling() } else { - context.sharedContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in - if !success { - continueHandling() - } - })) + if isTelegraPhLink(parsedUrl.absoluteString) { + continueHandling() + } else { + context.sharedContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in + if !success { + continueHandling() + } + })) + } } } else { continueHandling() diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 547e03bf40..55403ba2bb 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -95,6 +95,7 @@ import PasskeysScreen import GiftDemoScreen import ChatTextLinkEditUI import CocoonInfoScreen +import GiftCraftScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -3883,6 +3884,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionWearPreviewScreen(context: context, auctionContext: auctionContext, attributes: attributes, completion: completion) } + public func makeGiftCraftScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController { + return GiftCraftScreen(context: context, gift: gift) + } + public func makeGiftDemoScreen(context: AccountContext) -> ViewController { return GiftDemoScreen(context: context) } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 3ce46245e4..2199dbfe8a 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -157,10 +157,18 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return self.chatListController } + public func getSettingsController() -> ViewController? { + return self.accountSettingsController + } + public func getPrivacySettings() -> Promise? { return self.accountSettingsController?.privacySettings } + public func getTwoStepAuthData() -> Promise? { + return self.accountSettingsController?.twoStepAuthData + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let needsRootWallpaperBackgroundNode: Bool if case .regular = layout.metrics.widthClass { @@ -301,7 +309,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } @discardableResult - public func openStoryCamera(customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { + public func openStoryCamera(mode: StoryCameraMode, customTarget: Stories.PendingTarget?, resumeLiveStream: Bool, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { return nil } @@ -327,6 +335,16 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } + let cameraMode: CameraScreenImpl.CameraMode + switch mode { + case .photo: + cameraMode = .photo + case .video: + cameraMode = .video + case .live: + cameraMode = .live + } + var presentImpl: ((ViewController) -> Void)? var returnToCameraImpl: (() -> Void)? var dismissCameraImpl: (() -> Void)? @@ -334,6 +352,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let cameraController = CameraScreenImpl( context: context, mode: .story, + cameraMode: cameraMode, customTarget: mediaEditorCustomTarget, resumeLiveStream: resumeLiveStream, transitionIn: transitionIn.flatMap { @@ -759,7 +778,31 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } } - public func openSettings() { + public func openChats() { + guard let rootTabController = self.rootTabController else { + return + } + + self.popToRoot(animated: false) + + if let index = rootTabController.controllers.firstIndex(where: { $0 is ChatListController }) { + rootTabController.selectedIndex = index + } + } + + public func openContacts() { + guard let rootTabController = self.rootTabController else { + return + } + + self.popToRoot(animated: false) + + if let index = rootTabController.controllers.firstIndex(where: { $0 is ContactsController }) { + rootTabController.selectedIndex = index + } + } + + public func openSettings(edit: Bool) { guard let rootTabController = self.rootTabController else { return } @@ -769,6 +812,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon if let index = rootTabController.controllers.firstIndex(where: { $0 is PeerInfoScreenImpl }) { rootTabController.selectedIndex = index } + + if edit { + self.accountSettingsController?.activateEdit() + } } public func openBirthdaySetup() { @@ -785,6 +832,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon accountSettingsController.openAvatars() } } + + public func startNewCall() { + self.callListController?.tabBarActivateSearch() + } } #if SWIFT_PACKAGE diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index 51296e37f1..755c04caef 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -104,12 +104,12 @@ public struct ApplicationSpecificItemCacheCollectionId { private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { case webSearchRecentQueries = 0 case wallpaperSearchRecentQueries = 1 - case settingsSearchRecentItems = 2 case localThemes = 3 case storyDrafts = 4 case storySources = 5 case hashtagSearchRecentQueries = 6 case browserRecentlyVisited = 7 + case settingsSearchRecentItems = 8 } public struct ApplicationSpecificOrderedItemListCollectionId { diff --git a/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift b/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift index e4dc9fe8f2..fa09dead82 100644 --- a/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift +++ b/submodules/TelegramUpdateUI/Sources/UpdateInfoController.swift @@ -74,7 +74,7 @@ private enum UpdateInfoControllerEntry: ItemListNodeEntry { arguments.linkAction(action, itemLink) }) case let .update(_, title): - return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: title, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.openAppStorePage() }) } diff --git a/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift b/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift index f079434561..4e82d82aa8 100644 --- a/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift +++ b/submodules/TelegramUpdateUI/Sources/UpdateInfoItem.swift @@ -200,19 +200,20 @@ class UpdateInfoItemNode: ListViewItemNode { let textColor: UIColor = item.theme.list.itemPrimaryTextColor let inset: CGFloat + let spacing: CGFloat = 16.0 let itemBackgroundColor: UIColor let itemSeparatorColor: UIColor - let verticalInset: CGFloat = 14.0 + let verticalInset: CGFloat = 16.0 switch item.style { case .plain: itemBackgroundColor = item.theme.list.plainBackgroundColor itemSeparatorColor = item.theme.list.itemPlainSeparatorColor - inset = 14.0 + params.leftInset + inset = spacing + params.leftInset case .blocks: itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor - inset = 14.0 + params.rightInset + inset = spacing + params.rightInset } let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 88.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) @@ -226,10 +227,10 @@ class UpdateInfoItemNode: ListViewItemNode { switch item.style { case .plain: - contentSize = CGSize(width: params.width, height: 88.0 + textLayout.size.height + verticalInset * 2.0) + contentSize = CGSize(width: params.width, height: 76.0 + textLayout.size.height + verticalInset * 2.0) insets = itemListNeighborsPlainInsets(neighbors) case .blocks: - contentSize = CGSize(width: params.width, height: 88.0 + textLayout.size.height + verticalInset * 2.0) + contentSize = CGSize(width: params.width, height: 76.0 + textLayout.size.height + verticalInset * 2.0) insets = itemListNeighborsGroupedInsets(neighbors, params) } @@ -322,7 +323,7 @@ class UpdateInfoItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) @@ -334,8 +335,8 @@ class UpdateInfoItemNode: ListViewItemNode { strongSelf.iconNode.frame = iconFrame strongSelf.overlayNode.frame = iconFrame - strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: iconFrame.maxX + inset, y: iconFrame.minY + ceil((iconFrame.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) - strongSelf.textNode.frame = CGRect(origin: CGPoint(x: inset, y: iconFrame.maxY + inset), size: textLayout.size) + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: iconFrame.maxX + spacing, y: iconFrame.minY + ceil((iconFrame.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: inset, y: iconFrame.maxY + spacing), size: textLayout.size) strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel)) } diff --git a/submodules/TranslateUI/BUILD b/submodules/TranslateUI/BUILD index 376c71301b..fb3aaf6cac 100644 --- a/submodules/TranslateUI/BUILD +++ b/submodules/TranslateUI/BUILD @@ -32,6 +32,8 @@ swift_library( "//submodules/UndoUI:UndoUI", "//submodules/ActivityIndicator:ActivityIndicator", "//submodules/ShimmerEffect:ShimmerEffect", + "//submodules/Components/ResizableSheetComponent", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TranslateUI/Sources/TranslateButtonComponent.swift b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift index bf431c92ac..d00837baae 100644 --- a/submodules/TranslateUI/Sources/TranslateButtonComponent.swift +++ b/submodules/TranslateUI/Sources/TranslateButtonComponent.swift @@ -60,13 +60,14 @@ private final class TranslateButtonContentComponent: CombinedComponent { ) let sideInset: CGFloat = 16.0 + let textSideInset: CGFloat = 60.0 context.add(title - .position(CGPoint(x: sideInset + title.size.width / 2.0, y: context.availableSize.height / 2.0)) + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: context.availableSize.height / 2.0)) ) context.add(icon - .position(CGPoint(x: context.availableSize.width - sideInset - icon.size.width / 2.0, y: context.availableSize.height / 2.0)) + .position(CGPoint(x: sideInset + icon.size.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize @@ -155,7 +156,7 @@ final class TranslateButtonComponent: Component { self.component = component self.backgroundView.backgroundColor = component.theme.list.itemBlocksBackgroundColor - self.backgroundView.layer.cornerRadius = 10.0 + self.backgroundView.layer.cornerRadius = 26.0 let _ = self.centralContentView.update( transition: transition, diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index d246a19ad3..acd4163bfc 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -15,6 +15,8 @@ import MultilineTextWithEntitiesComponent import BundleIconComponent import UndoUI import SwiftUI +import ResizableSheetComponent +import GlassBarButtonComponent private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage { return generateImage(size, rotatedContext: { size, context in @@ -32,7 +34,7 @@ private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage { })! } -private final class TranslateScreenComponent: CombinedComponent { +private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext @@ -55,7 +57,7 @@ private final class TranslateScreenComponent: CombinedComponent { self.expand = expand } - static func ==(lhs: TranslateScreenComponent, rhs: TranslateScreenComponent) -> Bool { + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { if lhs.context !== rhs.context { return false } @@ -252,13 +254,13 @@ private final class TranslateScreenComponent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let topInset: CGFloat = environment.navigationHeight + 22.0 - let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let topInset: CGFloat = environment.navigationHeight - 35.0 + let sideInset: CGFloat = 20.0 + environment.safeInsets.left let textTopInset: CGFloat = 16.0 - let textSideInset: CGFloat = 16.0 + let textSideInset: CGFloat = 20.0 let textSpacing: CGFloat = 5.0 - let itemSpacing: CGFloat = 16.0 - let itemHeight: CGFloat = 44.0 + let itemSpacing: CGFloat = 20.0 + let itemHeight: CGFloat = 52.0 var languageCode = environment.strings.baseLanguageCode let rawSuffix = "-raw" @@ -284,7 +286,7 @@ private final class TranslateScreenComponent: CombinedComponent { let originalText = originalText.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: state.text, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)), + text: .plain(NSAttributedString(string: state.text, font: Font.medium(20.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: state.textExpanded ? 0 : 1, lineSpacing: 0.1 @@ -311,7 +313,7 @@ private final class TranslateScreenComponent: CombinedComponent { if let translatedText = state.translatedText { maybeTranslationText = translationText.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: translatedText, font: Font.medium(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)), + text: .plain(NSAttributedString(string: translatedText, font: Font.medium(20.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .natural)), horizontalAlignment: .natural, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -340,7 +342,7 @@ private final class TranslateScreenComponent: CombinedComponent { let textBackgroundSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight + itemSpacing) let textBackground = textBackground.update( - component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0), + component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0), availableSize: textBackgroundSize, transition: context.transition ) @@ -372,12 +374,12 @@ private final class TranslateScreenComponent: CombinedComponent { content: AnyComponent(ZStack([ AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( fillColor: theme.list.itemPrimaryTextColor, - size: CGSize(width: 22.0, height: 22.0) + size: CGSize(width: 26.0, height: 26.0) ))), AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent( state: state.isSpeakingOriginalText ? .pause : .play, tintColor: checkColor, - size: CGSize(width: 18.0, height: 18.0) + size: CGSize(width: 20.0, height: 20.0) ))), ])), action: { [weak state] in @@ -387,12 +389,12 @@ private final class TranslateScreenComponent: CombinedComponent { state.speakOriginalText() } ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 22.0, height: 22.0), + availableSize: CGSize(width: 26.0, height: 26.0), transition: .immediate ) context.add(originalSpeakButton - .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalSpeakButton.size.width / 2.0 - 3.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height - originalSpeakButton.size.height / 2.0 - 2.0)) + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - originalSpeakButton.size.width / 2.0 + 9.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height - originalSpeakButton.size.height / 2.0 - 2.0 + 12.0)) ) } } else { @@ -442,44 +444,46 @@ private final class TranslateScreenComponent: CombinedComponent { context.add(translationText .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationText.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationText.size.height / 2.0)) ) + + if state.availableSpeakLanguages.contains(state.toLanguage) { + let translationSpeakButton = translationSpeakButton.update( + component: Button( + content: AnyComponent(ZStack([ + AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( + fillColor: theme.list.itemAccentColor, + size: CGSize(width: 26.0, height: 26.0) + ))), + AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent( + state: state.isSpeakingTranslatedText ? .pause : .play, + tintColor: theme.list.itemCheckColors.foregroundColor, + size: CGSize(width: 20.0, height: 20.0) + ))), + ])), + action: { [weak state] in + guard let state = state else { + return + } + state.speakTranslatedText() + } + ).minSize(CGSize(width: 44.0, height: 44.0)), + availableSize: CGSize(width: 26.0, height: 26.0), + transition: .immediate + ) + + context.add(translationSpeakButton + .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - translationSpeakButton.size.width / 2.0 + 9.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight - translationSpeakButton.size.height / 2.0 - 2.0 + 12.0)) + .appear(.default()) + .disappear(.default()) + ) + } } else if let translationPlaceholder = maybeTranslationPlaceholder { context.add(translationPlaceholder .position(CGPoint(x: textBackgroundOrigin.x + textSideInset + translationPlaceholder.size.width / 2.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationPlaceholder.size.height / 2.0 + 4.0)) ) } - if state.availableSpeakLanguages.contains(state.toLanguage) { - let translationSpeakButton = translationSpeakButton.update( - component: Button( - content: AnyComponent(ZStack([ - AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( - fillColor: theme.list.itemAccentColor, - size: CGSize(width: 22.0, height: 22.0) - ))), - AnyComponentWithIdentity(id: "a", component: AnyComponent(PlayPauseIconComponent( - state: state.isSpeakingTranslatedText ? .pause : .play, - tintColor: theme.list.itemCheckColors.foregroundColor, - size: CGSize(width: 18.0, height: 18.0) - ))), - ])), - action: { [weak state] in - guard let state = state else { - return - } - state.speakTranslatedText() - } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 22.0, height: 22.0), - transition: .immediate - ) - - context.add(translationSpeakButton - .position(CGPoint(x: context.availableSize.width - sideInset - textSideInset - translationSpeakButton.size.width / 2.0 - 3.0, y: textBackgroundOrigin.y + textTopInset + originalTitle.size.height + textSpacing + originalText.size.height + itemSpacing + textTopInset + translationTitle.size.height + textSpacing + translationTextHeight - translationSpeakButton.size.height / 2.0 - 2.0)) - ) - } - - let buttonsSpacing: CGFloat = 24.0 - let smallSectionSpacing: CGFloat = 8.0 + let buttonsSpacing: CGFloat = 20.0 + let smallSectionSpacing: CGFloat = 20.0 var buttonsHeight: CGFloat = 0.0 @@ -532,500 +536,157 @@ private final class TranslateScreenComponent: CombinedComponent { } } -public class TranslateScreen: ViewController { - final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { - private var presentationData: PresentationData - private weak var controller: TranslateScreen? - - private let component: AnyComponent - private let theme: PresentationTheme? - - let dim: ASDisplayNode - let wrappingView: UIView - let containerView: UIView - let scrollView: UIScrollView - let hostView: ComponentHostView - - private(set) var isExpanded = false - private var panGestureRecognizer: UIPanGestureRecognizer? - private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? - - private var currentIsVisible: Bool = false - private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - - fileprivate var temporaryDismiss = false - - init(context: AccountContext, controller: TranslateScreen, component: AnyComponent, theme: PresentationTheme?) { - self.presentationData = context.sharedContext.currentPresentationData.with { $0 } - - self.controller = controller - - self.component = component - self.theme = theme - - let effectiveTheme = theme ?? self.presentationData.theme - - self.dim = ASDisplayNode() - self.dim.alpha = 0.0 - self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) - - self.wrappingView = UIView() - self.containerView = UIView() - self.scrollView = UIScrollView() - self.hostView = ComponentHostView() - - super.init() - - self.scrollView.delegate = self.wrappedScrollViewDelegate - self.scrollView.showsVerticalScrollIndicator = false - - self.containerView.clipsToBounds = true - self.containerView.backgroundColor = effectiveTheme.list.blocksBackgroundColor - - self.addSubnode(self.dim) - - self.view.addSubview(self.wrappingView) - self.wrappingView.addSubview(self.containerView) - self.containerView.addSubview(self.scrollView) - self.scrollView.addSubview(self.hostView) - } - - override func didLoad() { - super.didLoad() - - let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) - panRecognizer.delegate = self.wrappedGestureRecognizerDelegate - panRecognizer.delaysTouchesBegan = false - panRecognizer.cancelsTouchesInView = true - self.panGestureRecognizer = panRecognizer - self.wrappingView.addGestureRecognizer(panRecognizer) - - self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - - self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.controller?.dismiss(animated: true) - } - } - - override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if let (layout, _) = self.currentLayout { - if case .regular = layout.metrics.widthClass { - return false - } - } - return true - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let contentOffset = self.scrollView.contentOffset.y - self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { - return true - } - return false - } - - private var isDismissing = false - func animateIn() { - ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) - - let targetPosition = self.containerView.center - let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) - - self.containerView.center = startPosition - let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) - transition.animateView(allowUserInteraction: true, { - self.containerView.center = targetPosition - }, completion: { _ in - }) - } - - func animateOut(completion: @escaping () -> Void = {}) { - self.isDismissing = true - - let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in - self?.controller?.dismiss(animated: false, completion: completion) - }) - let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) - alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) - - if !self.temporaryDismiss { - self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) - } - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { - self.currentLayout = (layout, navigationHeight) - - if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { - self.containerView.addSubview(navigationBar.view) - } - - self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) - - var effectiveExpanded = self.isExpanded - if case .regular = layout.metrics.widthClass { - effectiveExpanded = true - } - - let isLandscape = layout.orientation == .landscape - let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset - let topInset: CGFloat - if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { - if effectiveExpanded { - topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) - } else { - topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) - } - } else { - topInset = effectiveExpanded ? 0.0 : edgeTopInset - } - transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) - - let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) - self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) - - let clipFrame: CGRect - if layout.metrics.widthClass == .compact { - self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) - if isLandscape { - self.containerView.layer.cornerRadius = 0.0 - } else { - self.containerView.layer.cornerRadius = 10.0 - } - - if #available(iOS 11.0, *) { - if layout.safeInsets.bottom.isZero { - self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - } else { - self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] - } - } - - if isLandscape { - clipFrame = CGRect(origin: CGPoint(), size: layout.size) - } else { - let coveredByModalTransition: CGFloat = 0.0 - var containerTopInset: CGFloat = 10.0 - if let statusBarHeight = layout.statusBarHeight { - containerTopInset += statusBarHeight - } - - let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) - let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width - let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition - let maxScaledTopInset: CGFloat = containerTopInset - 10.0 - let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition - let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) - - clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) - } - } else { - self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) - self.containerView.layer.cornerRadius = 10.0 - - let verticalInset: CGFloat = 44.0 - - let maxSide = max(layout.size.width, layout.size.height) - let minSide = min(layout.size.width, layout.size.height) - let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) - clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) - } - - transition.setFrame(view: self.containerView, frame: clipFrame) - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) - - let environment = ViewControllerComponentContainer.Environment( - statusBarHeight: 0.0, - navigationHeight: navigationHeight, - safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), - additionalInsets: layout.additionalInsets, - inputHeight: layout.inputHeight ?? 0.0, - metrics: layout.metrics, - deviceMetrics: layout.deviceMetrics, - orientation: layout.metrics.orientation, - isVisible: self.currentIsVisible, - theme: self.theme ?? self.presentationData.theme, - strings: self.presentationData.strings, - dateTimeFormat: self.presentationData.dateTimeFormat, - controller: { [weak self] in - return self?.controller - } - ) - var contentSize = self.hostView.update( - transition: transition, - component: self.component, - environment: { - environment - }, - forceUpdate: true, - containerSize: CGSize(width: clipFrame.size.width, height: 10000.0) - ) - contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) - transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) - - self.scrollView.contentSize = contentSize - } - - private var didPlayAppearAnimation = false - func updateIsVisible(isVisible: Bool) { - if self.currentIsVisible == isVisible { - return - } - self.currentIsVisible = isVisible - - guard let currentLayout = self.currentLayout else { - return - } - self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) - - if !self.didPlayAppearAnimation { - self.didPlayAppearAnimation = true - self.animateIn() - } - } - - private var defaultTopInset: CGFloat { - guard let (layout, _) = self.currentLayout else{ - return 210.0 - } - if case .compact = layout.metrics.widthClass { - var factor: CGFloat = 0.2488 - if layout.size.width <= 320.0 { - factor = 0.15 - } - return floor(max(layout.size.width, layout.size.height) * factor) - } else { - return 210.0 - } - } - - private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { - if let view = view { - if let view = view as? UIScrollView { - return (view, nil) - } - if let node = view.asyncdisplaykit_node as? ListView { - return (node.scroller, node) - } - return findScrollView(view: view.superview) - } else { - return nil - } - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard let (layout, navigationHeight) = self.currentLayout else { - return - } - - let isLandscape = layout.orientation == .landscape - let edgeTopInset = isLandscape ? 0.0 : defaultTopInset - - switch recognizer.state { - case .began: - let point = recognizer.location(in: self.view) - let currentHitView = self.hitTest(point, with: nil) - - var scrollViewAndListNode = self.findScrollView(view: currentHitView) - if scrollViewAndListNode?.0.frame.height == self.frame.width { - scrollViewAndListNode = nil - } - let scrollView = scrollViewAndListNode?.0 - let listNode = scrollViewAndListNode?.1 - - let topInset: CGFloat - if self.isExpanded { - topInset = 0.0 - } else { - topInset = edgeTopInset - } - - self.panGestureArguments = (topInset, 0.0, scrollView, listNode) - case .changed: - guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { - return - } - let visibleContentOffset = listNode?.visibleContentOffset() - let contentOffset = scrollView?.contentOffset.y ?? 0.0 - - var translation = recognizer.translation(in: self.view).y - - var currentOffset = topInset + translation - - let epsilon = 1.0 - if case let .known(value) = visibleContentOffset, value <= epsilon { - if let scrollView = scrollView { - scrollView.bounces = false - scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) - } - } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { - scrollView.bounces = false - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } else if let scrollView = scrollView { - translation = panOffset - currentOffset = topInset + translation - if self.isExpanded { - recognizer.setTranslation(CGPoint(), in: self.view) - } else if currentOffset > 0.0 { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - } - - self.panGestureArguments = (topInset, translation, scrollView, listNode) - - if !self.isExpanded { - if currentOffset > 0.0, let scrollView = scrollView { - scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) - } - } - - var bounds = self.bounds - if self.isExpanded { - bounds.origin.y = -max(0.0, translation - edgeTopInset) - } else { - bounds.origin.y = -translation - } - bounds.origin.y = min(0.0, bounds.origin.y) - self.bounds = bounds - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) - case .ended: - guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { - return - } - self.panGestureArguments = nil - - let visibleContentOffset = listNode?.visibleContentOffset() - let contentOffset = scrollView?.contentOffset.y ?? 0.0 - - let translation = recognizer.translation(in: self.view).y - var velocity = recognizer.velocity(in: self.view) - - if self.isExpanded { - if case let .known(value) = visibleContentOffset, value > 0.1 { - velocity = CGPoint() - } else if case .unknown = visibleContentOffset { - velocity = CGPoint() - } else if contentOffset > 0.1 { - velocity = CGPoint() - } - } - - var bounds = self.bounds - if self.isExpanded { - bounds.origin.y = -max(0.0, translation - edgeTopInset) - } else { - bounds.origin.y = -translation - } - bounds.origin.y = min(0.0, bounds.origin.y) - - scrollView?.bounces = true - - let offset = currentTopInset + panOffset - let topInset: CGFloat = edgeTopInset - - var dismissing = false - if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { - self.controller?.dismiss(animated: true, completion: nil) - dismissing = true - } else if self.isExpanded { - if velocity.y > 300.0 || offset > topInset / 2.0 { - self.isExpanded = false - if let listNode = listNode { - listNode.scroller.setContentOffset(CGPoint(), animated: false) - } else if let scrollView = scrollView { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - - let distance = topInset - offset - let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) - } else { - self.isExpanded = true - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - } - } else if (velocity.y < -300.0 || offset < topInset / 2.0) { - if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { - DispatchQueue.main.async { - listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - } - - let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) - let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) - self.isExpanded = true - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) - } else { - if let listNode = listNode { - listNode.scroller.setContentOffset(CGPoint(), animated: false) - } else if let scrollView = scrollView { - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) - } - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - } - - if !dismissing { - var bounds = self.bounds - let previousBounds = bounds - bounds.origin.y = 0.0 - self.bounds = bounds - self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) - } - case .cancelled: - self.panGestureArguments = nil - - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) - default: - break - } - } - - func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { - guard isExpanded != self.isExpanded else { - return - } - self.isExpanded = isExpanded - - guard let (layout, navigationHeight) = self.currentLayout else { - return - } - self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) - } - } - - var node: Node { - return self.displayNode as! Node - } +private final class TranslateSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext - private let theme: PresentationTheme? - private let component: AnyComponent - private var isInitiallyExpanded = false + private let text: String + private let entities: [MessageTextEntity] + private let fromLanguage: String? + private let toLanguage: String + private let copyTranslation: ((String) -> Void)? + private let changeLanguage: (String, String, @escaping (String, String) -> Void) -> Void - private var currentLayout: ContainerViewLayout? + init( + context: AccountContext, + text: String, + entities: [MessageTextEntity], + fromLanguage: String?, + toLanguage: String, + copyTranslation: ((String) -> Void)?, + changeLanguage: @escaping (String, String, @escaping (String, String) -> Void) -> Void + ) { + self.context = context + self.text = text + self.entities = entities + self.fromLanguage = fromLanguage + self.toLanguage = toLanguage + self.copyTranslation = copyTranslation + self.changeLanguage = changeLanguage + } + + static func ==(lhs: TranslateSheetComponent, rhs: TranslateSheetComponent) -> Bool { + return true + } + + static var body: Body { + let sheet = Child(ResizableSheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let dismiss: (Bool) -> Void = { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + + let theme = environment.theme.withModalBlocksBackground() + + let sheet = sheet.update( + component: ResizableSheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + text: context.component.text, + entities: context.component.entities, + fromLanguage: context.component.fromLanguage, + toLanguage: context.component.toLanguage, + copyTranslation: context.component.copyTranslation, + changeLanguage: context.component.changeLanguage, + expand: {} + )), + titleItem: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Translate_Title, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor))) + ), + leftItem: AnyComponent( + GlassBarButtonComponent( + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, + isDark: theme.overallDarkAppearance, + state: .glass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.chat.inputPanel.panelControlColor + ) + )), + action: { _ in + dismiss(true) + } + ) + ), + hasTopEdgeEffect: false, + bottomItem: nil, + backgroundColor: .color(theme.actionSheet.opaqueItemBackgroundColor), + animateOut: animateOut + ), + environment: { + environment + ResizableSheetComponentEnvironment( + theme: theme, + statusBarHeight: environment.statusBarHeight, + safeInsets: environment.safeInsets, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + screenSize: context.availableSize, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + dismiss(animated) + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class TranslateScreen: ViewControllerComponentContainer { + private let context: AccountContext public var pushController: (ViewController) -> Void = { _ in } public var presentController: (ViewController) -> Void = { _ in } - - public var wasDismissed: (() -> Void)? - - public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, entities: [MessageTextEntity] = [], canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + public init( + context: AccountContext, + forceTheme: PresentationTheme? = nil, + text: String, + entities: [MessageTextEntity] = [], + canCopy: Bool, + fromLanguage: String?, + toLanguage: String? = nil, + ignoredLanguages: [String]? = nil + ) { + self.context = context + + let theme: ViewControllerComponentContainer.Theme + if let forceTheme { + theme = .custom(forceTheme) + } else { + theme = .default + } + + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } var baseLanguageCode = presentationData.strings.baseLanguageCode let rawSuffix = "-raw" if baseLanguageCode.hasSuffix(rawSuffix) { @@ -1047,21 +708,30 @@ public class TranslateScreen: ViewController { var copyTranslationImpl: ((String) -> Void)? var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)? - var expandImpl: (() -> Void)? - self.init(context: context, component: TranslateScreenComponent(context: context, text: text, entities: entities, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in - copyTranslationImpl?(text) - }, changeLanguage: { fromLang, toLang, completion in - changeLanguageImpl?(fromLang, toLang, completion) - }, expand: { - expandImpl?() - }), theme: forceTheme) - self.isInitiallyExpanded = isExpanded - - self.title = presentationData.strings.Translate_Title - - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + super.init( + context: context, + component: TranslateSheetComponent( + context: context, + text: text, + entities: entities, + fromLanguage: fromLanguage, + toLanguage: toLanguage, + copyTranslation: !canCopy ? nil : { text in + copyTranslationImpl?(text) + }, + changeLanguage: { fromLang, toLang, completion in + changeLanguageImpl?(fromLang, toLang, completion) + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: theme + ) + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) copyTranslationImpl = { [weak self] text in @@ -1075,116 +745,682 @@ public class TranslateScreen: ViewController { let pushController = self?.pushController let presentController = self?.presentController let controller = languageSelectionController(context: context, forceTheme: forceTheme, fromLanguage: fromLang, toLanguage: toLang, completion: { fromLang, toLang in - let controller = TranslateScreen(context: context, forceTheme: forceTheme, text: text, canCopy: canCopy, fromLanguage: fromLang, toLanguage: toLang, isExpanded: true, ignoredLanguages: ignoredLanguages) + let controller = TranslateScreen(context: context, forceTheme: forceTheme, text: text, canCopy: canCopy, fromLanguage: fromLang, toLanguage: toLang, ignoredLanguages: ignoredLanguages) controller.pushController = pushController ?? { _ in } controller.presentController = presentController ?? { _ in } presentController?(controller) }) - self?.node.temporaryDismiss = true - self?.dismiss(animated: true, completion: nil) + self?.dismissAnimated() pushController?(controller) } - - expandImpl = { [weak self] in - self?.node.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) - if let currentLayout = self?.currentLayout { - self?.containerLayoutUpdated(currentLayout, transition: .animated(duration: 0.4, curve: .spring)) - } - } } - - private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { - self.context = context - self.component = AnyComponent(component) - self.theme = theme - var presentationData = context.sharedContext.currentPresentationData.with { $0 } - if let theme { - presentationData = presentationData.withUpdated(theme: theme) - } - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) - } - required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - @objc private func cancelPressed() { - self.dismiss(animated: true, completion: nil) - } - - override open func loadDisplayNode() { - self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) - if self.isInitiallyExpanded { - (self.displayNode as! Node).update(isExpanded: true, transition: .immediate) + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent.View.Tag()) as? ResizableSheetComponent.View { + view.dismissAnimated() } - self.displayNodeDidLoad() - } - - public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - self.view.endEditing(true) - let wasDismissed = self.wasDismissed - if flag { - self.node.animateOut(completion: { - super.dismiss(animated: false, completion: {}) - wasDismissed?() - completion?() - }) - } else { - super.dismiss(animated: false, completion: {}) - wasDismissed?() - completion?() - } - } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.node.updateIsVisible(isVisible: true) - } - - override open func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - self.node.updateIsVisible(isVisible: false) - } - - override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - var navigationLayout = self.navigationLayout(layout: layout) - var navigationFrame = navigationLayout.navigationFrame - - var layout = layout - if case .regular = layout.metrics.widthClass { - let verticalInset: CGFloat = 44.0 - let maxSide = max(layout.size.width, layout.size.height) - let minSide = min(layout.size.width, layout.size.height) - let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) - let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) - navigationFrame.size.width = clipFrame.width - layout.size = clipFrame.size - } - - navigationFrame.size.height = 56.0 - navigationLayout.navigationFrame = navigationFrame - navigationLayout.defaultContentHeight = 56.0 - - layout.statusBarHeight = nil - - self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition) - } - - override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.currentLayout = layout - super.containerLayoutUpdated(layout, transition: transition) - - let navigationHeight: CGFloat = 56.0 - - self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) } } +//public class TranslateScreen: ViewController { +// final class Node: ViewControllerTracingNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { +// private var presentationData: PresentationData +// private weak var controller: TranslateScreen? +// +// private let component: AnyComponent +// private let theme: PresentationTheme? +// +// let dim: ASDisplayNode +// let wrappingView: UIView +// let containerView: UIView +// let scrollView: UIScrollView +// let hostView: ComponentHostView +// +// private(set) var isExpanded = false +// private var panGestureRecognizer: UIPanGestureRecognizer? +// private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? +// +// private var currentIsVisible: Bool = false +// private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? +// +// fileprivate var temporaryDismiss = false +// +// init(context: AccountContext, controller: TranslateScreen, component: AnyComponent, theme: PresentationTheme?) { +// self.presentationData = context.sharedContext.currentPresentationData.with { $0 } +// +// self.controller = controller +// +// self.component = component +// self.theme = theme +// +// let effectiveTheme = theme ?? self.presentationData.theme +// +// self.dim = ASDisplayNode() +// self.dim.alpha = 0.0 +// self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) +// +// self.wrappingView = UIView() +// self.containerView = UIView() +// self.scrollView = UIScrollView() +// self.hostView = ComponentHostView() +// +// super.init() +// +// self.scrollView.delegate = self.wrappedScrollViewDelegate +// self.scrollView.showsVerticalScrollIndicator = false +// +// self.containerView.clipsToBounds = true +// self.containerView.backgroundColor = effectiveTheme.list.blocksBackgroundColor +// +// self.addSubnode(self.dim) +// +// self.view.addSubview(self.wrappingView) +// self.wrappingView.addSubview(self.containerView) +// self.containerView.addSubview(self.scrollView) +// self.scrollView.addSubview(self.hostView) +// } +// +// override func didLoad() { +// super.didLoad() +// +// let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) +// panRecognizer.delegate = self.wrappedGestureRecognizerDelegate +// panRecognizer.delaysTouchesBegan = false +// panRecognizer.cancelsTouchesInView = true +// self.panGestureRecognizer = panRecognizer +// self.wrappingView.addGestureRecognizer(panRecognizer) +// +// self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) +// +// self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) +// } +// +// @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { +// if case .ended = recognizer.state { +// self.controller?.dismiss(animated: true) +// } +// } +// +// override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { +// if let (layout, _) = self.currentLayout { +// if case .regular = layout.metrics.widthClass { +// return false +// } +// } +// return true +// } +// +// func scrollViewDidScroll(_ scrollView: UIScrollView) { +// let contentOffset = self.scrollView.contentOffset.y +// self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) +// } +// +// func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { +// if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { +// return true +// } +// return false +// } +// +// private var isDismissing = false +// func animateIn() { +// ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) +// +// let targetPosition = self.containerView.center +// let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) +// +// self.containerView.center = startPosition +// let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) +// transition.animateView(allowUserInteraction: true, { +// self.containerView.center = targetPosition +// }, completion: { _ in +// }) +// } +// +// func animateOut(completion: @escaping () -> Void = {}) { +// self.isDismissing = true +// +// let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) +// positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in +// self?.controller?.dismiss(animated: false, completion: completion) +// }) +// let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) +// alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) +// +// if !self.temporaryDismiss { +// self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) +// } +// } +// +// func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ComponentTransition) { +// self.currentLayout = (layout, navigationHeight) +// +// if let controller = self.controller, let navigationBar = controller.navigationBar, navigationBar.view.superview !== self.wrappingView { +// self.containerView.addSubview(navigationBar.view) +// } +// +// self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) +// +// var effectiveExpanded = self.isExpanded +// if case .regular = layout.metrics.widthClass { +// effectiveExpanded = true +// } +// +// let isLandscape = layout.orientation == .landscape +// let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset +// let topInset: CGFloat +// if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { +// if effectiveExpanded { +// topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) +// } else { +// topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) +// } +// } else { +// topInset = effectiveExpanded ? 0.0 : edgeTopInset +// } +// transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) +// +// let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) +// self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) +// +// let clipFrame: CGRect +// if layout.metrics.widthClass == .compact { +// self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) +// if isLandscape { +// self.containerView.layer.cornerRadius = 0.0 +// } else { +// self.containerView.layer.cornerRadius = 10.0 +// } +// +// if #available(iOS 11.0, *) { +// if layout.safeInsets.bottom.isZero { +// self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] +// } else { +// self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] +// } +// } +// +// if isLandscape { +// clipFrame = CGRect(origin: CGPoint(), size: layout.size) +// } else { +// let coveredByModalTransition: CGFloat = 0.0 +// var containerTopInset: CGFloat = 10.0 +// if let statusBarHeight = layout.statusBarHeight { +// containerTopInset += statusBarHeight +// } +// +// let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) +// let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width +// let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition +// let maxScaledTopInset: CGFloat = containerTopInset - 10.0 +// let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition +// let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) +// +// clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) +// } +// } else { +// self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) +// self.containerView.layer.cornerRadius = 10.0 +// +// let verticalInset: CGFloat = 44.0 +// +// let maxSide = max(layout.size.width, layout.size.height) +// let minSide = min(layout.size.width, layout.size.height) +// let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) +// clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) +// } +// +// transition.setFrame(view: self.containerView, frame: clipFrame) +// transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) +// +// let environment = ViewControllerComponentContainer.Environment( +// statusBarHeight: 0.0, +// navigationHeight: navigationHeight, +// safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right), +// additionalInsets: layout.additionalInsets, +// inputHeight: layout.inputHeight ?? 0.0, +// metrics: layout.metrics, +// deviceMetrics: layout.deviceMetrics, +// orientation: layout.metrics.orientation, +// isVisible: self.currentIsVisible, +// theme: self.theme ?? self.presentationData.theme, +// strings: self.presentationData.strings, +// dateTimeFormat: self.presentationData.dateTimeFormat, +// controller: { [weak self] in +// return self?.controller +// } +// ) +// var contentSize = self.hostView.update( +// transition: transition, +// component: self.component, +// environment: { +// environment +// }, +// forceUpdate: true, +// containerSize: CGSize(width: clipFrame.size.width, height: 10000.0) +// ) +// contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) +// transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) +// +// self.scrollView.contentSize = contentSize +// } +// +// private var didPlayAppearAnimation = false +// func updateIsVisible(isVisible: Bool) { +// if self.currentIsVisible == isVisible { +// return +// } +// self.currentIsVisible = isVisible +// +// guard let currentLayout = self.currentLayout else { +// return +// } +// self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) +// +// if !self.didPlayAppearAnimation { +// self.didPlayAppearAnimation = true +// self.animateIn() +// } +// } +// +// private var defaultTopInset: CGFloat { +// guard let (layout, _) = self.currentLayout else{ +// return 210.0 +// } +// if case .compact = layout.metrics.widthClass { +// var factor: CGFloat = 0.2488 +// if layout.size.width <= 320.0 { +// factor = 0.15 +// } +// return floor(max(layout.size.width, layout.size.height) * factor) +// } else { +// return 210.0 +// } +// } +// +// private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { +// if let view = view { +// if let view = view as? UIScrollView { +// return (view, nil) +// } +// if let node = view.asyncdisplaykit_node as? ListView { +// return (node.scroller, node) +// } +// return findScrollView(view: view.superview) +// } else { +// return nil +// } +// } +// +// @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { +// guard let (layout, navigationHeight) = self.currentLayout else { +// return +// } +// +// let isLandscape = layout.orientation == .landscape +// let edgeTopInset = isLandscape ? 0.0 : defaultTopInset +// +// switch recognizer.state { +// case .began: +// let point = recognizer.location(in: self.view) +// let currentHitView = self.hitTest(point, with: nil) +// +// var scrollViewAndListNode = self.findScrollView(view: currentHitView) +// if scrollViewAndListNode?.0.frame.height == self.frame.width { +// scrollViewAndListNode = nil +// } +// let scrollView = scrollViewAndListNode?.0 +// let listNode = scrollViewAndListNode?.1 +// +// let topInset: CGFloat +// if self.isExpanded { +// topInset = 0.0 +// } else { +// topInset = edgeTopInset +// } +// +// self.panGestureArguments = (topInset, 0.0, scrollView, listNode) +// case .changed: +// guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { +// return +// } +// let visibleContentOffset = listNode?.visibleContentOffset() +// let contentOffset = scrollView?.contentOffset.y ?? 0.0 +// +// var translation = recognizer.translation(in: self.view).y +// +// var currentOffset = topInset + translation +// +// let epsilon = 1.0 +// if case let .known(value) = visibleContentOffset, value <= epsilon { +// if let scrollView = scrollView { +// scrollView.bounces = false +// scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) +// } +// } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { +// scrollView.bounces = false +// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) +// } else if let scrollView = scrollView { +// translation = panOffset +// currentOffset = topInset + translation +// if self.isExpanded { +// recognizer.setTranslation(CGPoint(), in: self.view) +// } else if currentOffset > 0.0 { +// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) +// } +// } +// +// self.panGestureArguments = (topInset, translation, scrollView, listNode) +// +// if !self.isExpanded { +// if currentOffset > 0.0, let scrollView = scrollView { +// scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) +// } +// } +// +// var bounds = self.bounds +// if self.isExpanded { +// bounds.origin.y = -max(0.0, translation - edgeTopInset) +// } else { +// bounds.origin.y = -translation +// } +// bounds.origin.y = min(0.0, bounds.origin.y) +// self.bounds = bounds +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) +// case .ended: +// guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { +// return +// } +// self.panGestureArguments = nil +// +// let visibleContentOffset = listNode?.visibleContentOffset() +// let contentOffset = scrollView?.contentOffset.y ?? 0.0 +// +// let translation = recognizer.translation(in: self.view).y +// var velocity = recognizer.velocity(in: self.view) +// +// if self.isExpanded { +// if case let .known(value) = visibleContentOffset, value > 0.1 { +// velocity = CGPoint() +// } else if case .unknown = visibleContentOffset { +// velocity = CGPoint() +// } else if contentOffset > 0.1 { +// velocity = CGPoint() +// } +// } +// +// var bounds = self.bounds +// if self.isExpanded { +// bounds.origin.y = -max(0.0, translation - edgeTopInset) +// } else { +// bounds.origin.y = -translation +// } +// bounds.origin.y = min(0.0, bounds.origin.y) +// +// scrollView?.bounces = true +// +// let offset = currentTopInset + panOffset +// let topInset: CGFloat = edgeTopInset +// +// var dismissing = false +// if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { +// self.controller?.dismiss(animated: true, completion: nil) +// dismissing = true +// } else if self.isExpanded { +// if velocity.y > 300.0 || offset > topInset / 2.0 { +// self.isExpanded = false +// if let listNode = listNode { +// listNode.scroller.setContentOffset(CGPoint(), animated: false) +// } else if let scrollView = scrollView { +// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) +// } +// +// let distance = topInset - offset +// let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) +// let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) +// } else { +// self.isExpanded = true +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) +// } +// } else if (velocity.y < -300.0 || offset < topInset / 2.0) { +// if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { +// DispatchQueue.main.async { +// listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) +// } +// } +// +// let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) +// let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) +// self.isExpanded = true +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) +// } else { +// if let listNode = listNode { +// listNode.scroller.setContentOffset(CGPoint(), animated: false) +// } else if let scrollView = scrollView { +// scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) +// } +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) +// } +// +// if !dismissing { +// var bounds = self.bounds +// let previousBounds = bounds +// bounds.origin.y = 0.0 +// self.bounds = bounds +// self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) +// } +// case .cancelled: +// self.panGestureArguments = nil +// +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(.animated(duration: 0.3, curve: .easeInOut))) +// default: +// break +// } +// } +// +// func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { +// guard isExpanded != self.isExpanded else { +// return +// } +// self.isExpanded = isExpanded +// +// guard let (layout, navigationHeight) = self.currentLayout else { +// return +// } +// self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) +// } +// } +// +// var node: Node { +// return self.displayNode as! Node +// } +// +// private let context: AccountContext +// private let theme: PresentationTheme? +// private let component: AnyComponent +// private var isInitiallyExpanded = false +// +// private var currentLayout: ContainerViewLayout? +// +// public var pushController: (ViewController) -> Void = { _ in } +// public var presentController: (ViewController) -> Void = { _ in } +// +// public var wasDismissed: (() -> Void)? +// +// public convenience init(context: AccountContext, forceTheme: PresentationTheme? = nil, text: String, entities: [MessageTextEntity] = [], canCopy: Bool, fromLanguage: String?, toLanguage: String? = nil, isExpanded: Bool = false, ignoredLanguages: [String]? = nil) { +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// +// var baseLanguageCode = presentationData.strings.baseLanguageCode +// let rawSuffix = "-raw" +// if baseLanguageCode.hasSuffix(rawSuffix) { +// baseLanguageCode = String(baseLanguageCode.dropLast(rawSuffix.count)) +// } +// +// let dontTranslateLanguages = effectiveIgnoredTranslationLanguages(context: context, ignoredLanguages: ignoredLanguages) +// +// var toLanguage = toLanguage ?? baseLanguageCode +// if toLanguage == fromLanguage { +// if fromLanguage == "en" { +// toLanguage = dontTranslateLanguages.first(where: { $0 != "en" }) ?? "en" +// } else { +// toLanguage = "en" +// } +// } +// +// toLanguage = normalizeTranslationLanguage(toLanguage) +// +// var copyTranslationImpl: ((String) -> Void)? +// var changeLanguageImpl: ((String, String, @escaping (String, String) -> Void) -> Void)? +// var expandImpl: (() -> Void)? +// self.init(context: context, component: TranslateScreenComponent(context: context, text: text, entities: entities, fromLanguage: fromLanguage, toLanguage: toLanguage, copyTranslation: !canCopy ? nil : { text in +// copyTranslationImpl?(text) +// }, changeLanguage: { fromLang, toLang, completion in +// changeLanguageImpl?(fromLang, toLang, completion) +// }, expand: { +// expandImpl?() +// }), theme: forceTheme) +// +// self.isInitiallyExpanded = isExpanded +// +// self.title = presentationData.strings.Translate_Title +// +// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) +// +// self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) +// +// copyTranslationImpl = { [weak self] text in +// UIPasteboard.general.string = text +// let content = UndoOverlayContent.copy(text: presentationData.strings.Conversation_TextCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) +// self?.dismiss(animated: true, completion: nil) +// } +// +// changeLanguageImpl = { [weak self] fromLang, toLang, completion in +// let pushController = self?.pushController +// let presentController = self?.presentController +// let controller = languageSelectionController(context: context, forceTheme: forceTheme, fromLanguage: fromLang, toLanguage: toLang, completion: { fromLang, toLang in +// let controller = TranslateScreen(context: context, forceTheme: forceTheme, text: text, canCopy: canCopy, fromLanguage: fromLang, toLanguage: toLang, isExpanded: true, ignoredLanguages: ignoredLanguages) +// controller.pushController = pushController ?? { _ in } +// controller.presentController = presentController ?? { _ in } +// presentController?(controller) +// }) +// +// self?.node.temporaryDismiss = true +// self?.dismiss(animated: true, completion: nil) +// +// pushController?(controller) +// } +// +// expandImpl = { [weak self] in +// self?.node.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) +// if let currentLayout = self?.currentLayout { +// self?.containerLayoutUpdated(currentLayout, transition: .animated(duration: 0.4, curve: .spring)) +// } +// } +// } +// +// private init(context: AccountContext, component: C, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment { +// self.context = context +// self.component = AnyComponent(component) +// self.theme = theme +// +// var presentationData = context.sharedContext.currentPresentationData.with { $0 } +// if let theme { +// presentationData = presentationData.withUpdated(theme: theme) +// } +// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) +// } +// +// required public init(coder aDecoder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// @objc private func cancelPressed() { +// self.dismiss(animated: true, completion: nil) +// } +// +// override open func loadDisplayNode() { +// self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme) +// if self.isInitiallyExpanded { +// (self.displayNode as! Node).update(isExpanded: true, transition: .immediate) +// } +// self.displayNodeDidLoad() +// } +// +// public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { +// self.view.endEditing(true) +// let wasDismissed = self.wasDismissed +// if flag { +// self.node.animateOut(completion: { +// super.dismiss(animated: false, completion: {}) +// wasDismissed?() +// completion?() +// }) +// } else { +// super.dismiss(animated: false, completion: {}) +// wasDismissed?() +// completion?() +// } +// } +// +// override open func viewDidAppear(_ animated: Bool) { +// super.viewDidAppear(animated) +// +// self.node.updateIsVisible(isVisible: true) +// } +// +// override open func viewDidDisappear(_ animated: Bool) { +// super.viewDidDisappear(animated) +// +// self.node.updateIsVisible(isVisible: false) +// } +// +// override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { +// var navigationLayout = self.navigationLayout(layout: layout) +// var navigationFrame = navigationLayout.navigationFrame +// +// var layout = layout +// if case .regular = layout.metrics.widthClass { +// let verticalInset: CGFloat = 44.0 +// let maxSide = max(layout.size.width, layout.size.height) +// let minSide = min(layout.size.width, layout.size.height) +// let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) +// let clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) +// navigationFrame.size.width = clipFrame.width +// layout.size = clipFrame.size +// } +// +// navigationFrame.size.height = 56.0 +// navigationLayout.navigationFrame = navigationFrame +// navigationLayout.defaultContentHeight = 56.0 +// +// layout.statusBarHeight = nil +// +// self.applyNavigationBarLayout(layout, navigationLayout: navigationLayout, additionalBackgroundHeight: 0.0, additionalCutout: nil, transition: transition) +// } +// +// override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { +// self.currentLayout = layout +// super.containerLayoutUpdated(layout, transition: transition) +// +// let navigationHeight: CGFloat = 56.0 +// +// self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: ComponentTransition(transition)) +// } +//} + public func presentTranslateScreen( context: AccountContext, text: String, @@ -1213,7 +1449,7 @@ public func presentTranslateScreen( if useSystemTranslation { presentSystemTranslateScreen(context: context, text: text) } else { - let controller = TranslateScreen(context: context, text: text, canCopy: canCopy, fromLanguage: fromLanguage, toLanguage: toLanguage, isExpanded: isExpanded, ignoredLanguages: ignoredLanguages) + let controller = TranslateScreen(context: context, text: text, canCopy: canCopy, fromLanguage: fromLanguage, toLanguage: toLanguage, ignoredLanguages: ignoredLanguages) controller.pushController = pushController controller.presentController = presentController controller.wasDismissed = wasDismissed diff --git a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift index 3d65b1f3f2..7e2a1c9095 100644 --- a/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift +++ b/submodules/WebUI/Sources/WebAppMessagePreviewScreen.swift @@ -72,16 +72,16 @@ private final class SheetContent: CombinedComponent { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let sideInset: CGFloat = 16.0 - var contentSize = CGSize(width: context.availableSize.width, height: 36.0) + var contentSize = CGSize(width: context.availableSize.width, height: 38.0) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -92,7 +92,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 55cd331d46..b8399de7b0 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -109,10 +109,10 @@ private final class SheetContent: CombinedComponent { let closeButton = closeButton.update( component: GlassBarButtonComponent( - size: CGSize(width: 40.0, height: 40.0), - backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + size: CGSize(width: 44.0, height: 44.0), + backgroundColor: nil, isDark: theme.overallDarkAppearance, - state: .generic, + state: .glass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", @@ -123,7 +123,7 @@ private final class SheetContent: CombinedComponent { component.dismiss() } ), - availableSize: CGSize(width: 40.0, height: 40.0), + availableSize: CGSize(width: 44.0, height: 44.0), transition: .immediate ) context.add(closeButton