diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index ae1b080ee0..792f85ead7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14094,3 +14094,5 @@ Sorry for the inconvenience."; "Gift.Unpin.Title" = "Too Manu Pinned Gifts"; "Gift.Unpin.Subtitle" = "Select a gift to unpin below:"; "Gift.Unpin.Unpin" = "Unpin"; + +"ChatList.Search.Ad" = "Ad"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 563920563b..861079ac79 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -6219,7 +6219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController title: title, options: options, completed: { - //removeAd?(adAttribute.opaqueId) } ) ) @@ -6236,9 +6235,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController guard let navigationController = self?.navigationController as? NavigationController else { return } - c?.dismiss(completion: { - if context.isPremium && !"".isEmpty { - //removeAd?(adAttribute.opaqueId) + c?.dismiss(completion: { [weak self] in + guard let self else { + return + } + if context.isPremium { + self.present(UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: nil, text: self.presentationData.strings.ReportAd_Hidden, cancel: nil, destructive: false), elevatedLayout: false, action: { _ in + return true + }), in: .current) + + let _ = self.context.engine.accountData.updateAdMessagesEnabled(enabled: false).start() + + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + searchContentNode.removeAds() + } } else { var replaceImpl: ((ViewController) -> Void)? let demoController = context.sharedContext.makePremiumDemoController(context: context, subject: .noAds, forceDark: false, action: { diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 8802ea428c..f4e1600400 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -570,6 +570,12 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo self.selectionPanelNode?.selectedMessages = self.stateValue.selectedMessageIds ?? [] } + public func removeAds() { + for pane in self.paneContainerNode.currentPanes.values { + pane.node.removeAds() + } + } + private var currentSearchOptions: ChatListSearchOptions { return self.searchOptionsValue ?? ChatListSearchOptions(peer: nil, date: nil) } diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 8ce513b4a2..8707a296cc 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -858,19 +858,12 @@ public enum ChatListSearchEntry: Comparable, Identifiable { context.engine.messages.markAdAction(opaqueId: peer.opaqueId, media: false, fullscreen: false) }, disabledAction: { _ in interaction.disabledPeerSelected(peer.peer, nil, .generic) - }, contextAction: peerContextAction.flatMap { peerContextAction in - return { node, gesture, location in - peerContextAction(peer.peer, .search(nil), node, gesture, location) - } - }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: nil, openStories: { itemPeer, sourceNode in - guard case let .peer(_, chatPeer) = itemPeer, let peer = chatPeer else { - return - } - if let sourceNode = sourceNode as? ContactsPeerItemNode { - openStories(peer.id, sourceNode.avatarNode) - } - }, adButtonAction: { node in + }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: nil, adButtonAction: { node in interaction.openAdInfo(node, peer) + }, visibilityUpdated: { isVisible in + if isVisible { + context.engine.messages.markAdAsSeen(opaqueId: peer.opaqueId) + } }) case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, isSelf): let primaryPeer: EnginePeer @@ -1613,6 +1606,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { private var deletedMessagesDisposable: Disposable? + private var adsHiddenPromise = ValuePromise(false) + private var adsHidden = false { + didSet { + self.adsHiddenPromise.set(self.adsHidden) + } + } + private var searchQueryValue: String? private var searchOptionsValue: ChatListSearchOptions? @@ -1954,6 +1954,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let previousRecentlySearchedPeersState = Atomic(value: nil) let hadAnySearchMessages = Atomic(value: false) + let adsHiddenPromise = self.adsHiddenPromise + let foundItems: Signal<([ChatListSearchEntry], Bool)?, NoError> = combineLatest(queue: .mainQueue(), searchQuery, searchOptions, self.searchScopePromise.get(), downloadItems) |> mapToSignal { [weak self] query, options, searchScope, downloadItems -> Signal<([ChatListSearchEntry], Bool)?, NoError> in if query == nil && options == nil && [.chats, .topics, .channels, .apps].contains(key) { @@ -2726,9 +2728,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { selectionPromise.get(), resolvedMessage, fixedRecentlySearchedPeers, - foundThreads + foundThreads, + adsHiddenPromise.get() ) - |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads -> ([ChatListSearchEntry], Bool)? in + |> map { accountPeer, foundLocalPeers, foundRemotePeers, foundRemoteMessages, foundPublicMessages, presentationData, searchState, selectionState, resolvedMessage, recentPeers, allAndFoundThreads, adsHidden -> ([ChatListSearchEntry], Bool)? in let isSearching = foundRemotePeers.3 || foundRemoteMessages.1 || foundPublicMessages.1 var entries: [ChatListSearchEntry] = [] var index = 0 @@ -3046,11 +3049,13 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var numberOfGlobalPeers = 0 index = 0 - for peer in foundRemotePeers.2 { - if !existingPeerIds.contains(peer.peer.id) { - existingPeerIds.insert(peer.peer.id) - entries.append(.adPeer(peer, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, finalQuery)) - index += 1 + if !adsHidden { + for peer in foundRemotePeers.2 { + if !existingPeerIds.contains(peer.peer.id) { + existingPeerIds.insert(peer.peer.id) + entries.append(.adPeer(peer, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, finalQuery)) + index += 1 + } } } @@ -3448,6 +3453,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) let previousSelectedMessages = Atomic?>(value: nil) let previousExpandGlobalSearch = Atomic(value: false) + let previousAdsHidden = Atomic(value: false) self.searchQueryDisposable = (searchQuery |> deliverOnMainQueue).startStrict(next: { [weak self, weak listInteraction, weak chatListInteraction] query in @@ -3536,6 +3542,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let strongSelf = self { let previousSelectedMessageIds = previousSelectedMessages.swap(strongSelf.selectedMessages) let previousExpandGlobalSearch = previousExpandGlobalSearch.swap(strongSelf.searchStateValue.expandGlobalSearch) + let previousAdsHidden = previousAdsHidden.swap(strongSelf.adsHidden) var entriesAndFlags = foundItems?.0 @@ -3572,8 +3579,9 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { let selectionChanged = (previousSelectedMessageIds == nil) != (strongSelf.selectedMessages == nil) let expandGlobalSearchChanged = previousExpandGlobalSearch != strongSelf.searchStateValue.expandGlobalSearch + let adsHiddenChanged = previousAdsHidden != strongSelf.adsHidden - let animated = selectionChanged || expandGlobalSearchChanged + let animated = selectionChanged || expandGlobalSearchChanged || adsHiddenChanged let firstTime = previousEntries == nil var transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: newEntries, displayingResults: entriesAndFlags != nil, isEmpty: !isSearching && (entriesAndFlags?.isEmpty ?? false), isLoading: isSearching, animated: animated, context: context, presentationData: strongSelf.presentationData, enableHeaders: true, filter: peersFilter, requestPeerType: requestPeerType, location: location, key: strongSelf.key, tagMask: tagMask, interaction: chatListInteraction, listInteraction: listInteraction, peerContextAction: { message, node, rect, gesture, location in interaction.peerContextAction?(message, node, rect, gesture, location) @@ -4909,6 +4917,10 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { self.mediaNode.updateSelectedMessages(animated: animated) } + func removeAds() { + self.adsHidden = true + } + private func enqueueRecentTransition(_ transition: ChatListSearchContainerRecentTransition, firstTime: Bool) { self.enqueuedRecentTransitions.append((transition, firstTime)) diff --git a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift index 37f101cf1a..ed8c8ae367 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchPaneContainerNode.swift @@ -23,6 +23,7 @@ protocol ChatListSearchPaneNode: ASDisplayNode { func updateSelectedMessages(animated: Bool) func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? func didBecomeFocused() + func removeAds() var searchCurrentMessages: [EngineMessage]? { get } } diff --git a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift index 553024fd8e..a1cd2004a2 100644 --- a/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift +++ b/submodules/ContactsPeerItem/Sources/ContactsPeerItem.swift @@ -210,6 +210,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { let storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? let openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? let adButtonAction: ((ASDisplayNode) -> Void)? + let visibilityUpdated: ((Bool) -> Void)? public let selectable: Bool @@ -254,7 +255,8 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { animationRenderer: MultiAnimationRenderer? = nil, storyStats: (total: Int, unseen: Int, hasUnseenCloseFriends: Bool)? = nil, openStories: ((ContactsPeerItemPeer, ASDisplayNode) -> Void)? = nil, - adButtonAction: ((ASDisplayNode) -> Void)? = nil + adButtonAction: ((ASDisplayNode) -> Void)? = nil, + visibilityUpdated: ((Bool) -> Void)? = nil ) { self.presentationData = presentationData self.style = style @@ -294,6 +296,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader { self.storyStats = storyStats self.openStories = openStories self.adButtonAction = adButtonAction + self.visibilityUpdated = visibilityUpdated if let index = index { var letter: String = "#" @@ -538,6 +541,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { ) } self.statusNode.visibilityRect = self.visibilityStatus == false ? CGRect.zero : CGRect.infinite + + self.item?.visibilityUpdated?(self.visibilityStatus) } } } @@ -1799,14 +1804,17 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode { adButton = current } else { adButton = HighlightableButtonNode() - adButton.setImage(UIImage(bundleImageName: "Components/AdMock"), for: .normal) strongSelf.addSubnode(adButton) strongSelf.adButton = adButton adButton.addTarget(strongSelf, action: #selector(strongSelf.adButtonPressed), forControlEvents: .touchUpInside) } - - adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - 31.0 - 13.0, y: 11.0), size: CGSize(width: 31.0, height: 15.0)) + if updatedTheme != nil || adButton.image(for: .normal) == nil { + adButton.setImage(PresentationResourcesChatList.searchAdIcon(item.presentationData.theme, strings: item.presentationData.strings), for: .normal) + } + if let icon = adButton.image(for: .normal) { + adButton.frame = CGRect(origin: CGPoint(x: params.width - 20.0 - icon.size.width - 13.0, y: 11.0), size: icon.size).insetBy(dx: -11.0, dy: -11.0) + } } else if let adButton = strongSelf.adButton { strongSelf.adButton = nil adButton.removeFromSupernode() diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index c04ad0365b..3058038c34 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -308,6 +308,7 @@ private enum PreferencesKeyValues: Int32 { case botBiometricsState = 39 case businessLinks = 40 case starGifts = 41 + case botStorageState = 42 } public func applicationSpecificPreferencesKey(_ value: Int32) -> ValueBoxKey { @@ -538,6 +539,13 @@ public struct PreferencesKeys { key.setInt32(0, value: PreferencesKeyValues.starGifts.rawValue) return key } + + public static func botStorageState(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 4 + 8) + key.setInt32(0, value: PreferencesKeyValues.botStorageState.rawValue) + key.setInt64(4, value: peerId.toInt64()) + return key + } } private enum SharedDataKeyValues: Int32 { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index 3b69134a7b..f5738a162c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -2093,6 +2093,37 @@ public extension TelegramEngine.EngineData.Item { } } + public struct BotStorageValue: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = String? + + fileprivate var id: EnginePeer.Id + fileprivate var storageKey: String + + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id, key: String) { + self.id = id + self.storageKey = key + } + + var key: PostboxViewKey { + return .preferences(keys: Set([PreferencesKeys.botStorageState(peerId: self.id)])) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PreferencesView else { + preconditionFailure() + } + if let state = view.values[PreferencesKeys.botStorageState(peerId: self.id)]?.get(TelegramBotStorageState.self) { + return state.data[self.storageKey] + } else { + return nil + } + } + } + public struct BusinessChatLinks: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = TelegramBusinessChatLinks? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index 44fabb5b5a..7c2f4db14b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -617,10 +617,11 @@ func _internal_markAdAction(account: Account, opaqueId: Data, media: Bool, fulls let _ = signal.start() } -func _internal_markAsSeen(account: Account, opaqueId: Data) -> Signal { - return account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId))) +func _internal_markAdAsSeen(account: Account, opaqueId: Data) { + let signal = account.network.request(Api.functions.messages.viewSponsoredMessage(randomId: Buffer(data: opaqueId))) |> `catch` { _ -> Signal in return .single(.boolFalse) } |> ignoreValues + let _ = signal.start() } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 57deb4e298..17e1143ddb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -416,6 +416,86 @@ func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: |> switchToLatest } +private let maxBotStorageSize = 5 * 1024 * 1024 +public struct TelegramBotStorageState: Codable, Equatable { + public struct KeyValue: Codable, Equatable { + var key: String + var value: String + } + + public var data: [String: String] + + public init( + data: [String: String] + ) { + self.data = data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + let values = try container.decode([KeyValue].self, forKey: "data") + var data: [String: String] = [:] + for pair in values { + data[pair.key] = pair.value + } + self.data = data + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + var values: [KeyValue] = [] + for (key, value) in self.data { + values.append(KeyValue(key: key, value: value)) + } + try container.encode(values, forKey: "data") + } +} + +private func _internal_updateBotStorageState(account: Account, peerId: EnginePeer.Id, update: @escaping (TelegramBotStorageState?) -> TelegramBotStorageState) -> Signal { + return account.postbox.transaction { transaction -> Signal in + let previousState = transaction.getPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId))?.get(TelegramBotStorageState.self) + let updatedState = update(previousState) + + var totalSize = 0 + for (_, value) in updatedState.data { + totalSize += value.utf8.count + } + guard totalSize <= maxBotStorageSize else { + return .fail(.quotaExceeded) + } + + transaction.setPreferencesEntry(key: PreferencesKeys.botStorageState(peerId: peerId), value: PreferencesEntry(updatedState)) + return .never() + } + |> castError(BotStorageError.self) + |> switchToLatest + |> ignoreValues +} + +public enum BotStorageError { + case quotaExceeded +} + +func _internal_setBotStorageValue(account: Account, peerId: EnginePeer.Id, key: String, value: String?) -> Signal { + return _internal_updateBotStorageState(account: account, peerId: peerId, update: { current in + var data = current?.data ?? [:] + if let value { + data[key] = value + } else { + data.removeValue(forKey: key) + } + return TelegramBotStorageState(data: data) + }) +} + +func _internal_clearBotStorage(account: Account, peerId: EnginePeer.Id) -> Signal { + return _internal_updateBotStorageState(account: account, peerId: peerId, update: { _ in + return TelegramBotStorageState(data: [:]) + }) +} + public struct TelegramBotBiometricsState: Codable, Equatable { public struct OpaqueToken: Codable, Equatable { public let publicKey: Data diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 07e4ed2243..0acd5526e2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1520,6 +1520,10 @@ public extension TelegramEngine { _internal_markAdAction(account: self.account, opaqueId: opaqueId, media: media, fullscreen: fullscreen) } + public func markAdAsSeen(opaqueId: Data) { + _internal_markAdAsSeen(account: self.account, opaqueId: opaqueId) + } + public func getAllLocalChannels(count: Int) -> Signal<[EnginePeer.Id], NoError> { return self.account.postbox.transaction { transaction -> [EnginePeer.Id] in var result: [EnginePeer.Id] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 8c9d9e6693..024fe828ea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1529,11 +1529,7 @@ public extension TelegramEngine { public func searchAdPeers(query: String) -> Signal<[AdPeer], NoError> { return _internal_searchAdPeers(account: self.account, query: query) } - - public func markAsSeen(ad opaqueId: Data) -> Signal { - return _internal_markAsSeen(account: self.account, opaqueId: opaqueId) - } - + public func isPremiumRequiredToContact(_ peerIds: [EnginePeer.Id]) -> Signal<[EnginePeer.Id: RequirementToContact], NoError> { return _internal_updateIsPremiumRequiredToContact(account: self.account, peerIds: peerIds) } @@ -1673,6 +1669,14 @@ public extension TelegramEngine { return _internal_botsWithBiometricState(account: self.account) } + public func setBotStorageValue(peerId: EnginePeer.Id, key: String, value: String?) -> Signal { + return _internal_setBotStorageValue(account: self.account, peerId: peerId, key: key, value: value) + } + + public func clearBotStorage(peerId: EnginePeer.Id) -> Signal { + return _internal_clearBotStorage(account: self.account, peerId: peerId) + } + public func toggleChatManagingBotIsPaused(chatId: EnginePeer.Id) { let _ = _internal_toggleChatManagingBotIsPaused(account: self.account, chatId: chatId).startStandalone() } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 1691444d0b..6afe54e1d7 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -128,6 +128,8 @@ public enum PresentationResourceKey: Int32 { case chatListGeneralTopicIcon case chatListGeneralTopicSmallIcon + + case searchAdIcon case chatTitleLockIcon case chatTitleMuteIcon diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 4700e4fb7d..f281093841 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -535,4 +535,38 @@ public struct PresentationResourcesChatList { }) }) } + + public static func searchAdIcon(_ theme: PresentationTheme, strings: PresentationStrings) -> UIImage? { + return theme.image(PresentationResourceKey.searchAdIcon.rawValue, { theme in + let titleString = NSAttributedString(string: strings.ChatList_Search_Ad, font: Font.regular(11.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center) + let stringRect = titleString.boundingRect(with: CGSize(width: 200.0, height: 20.0), options: .usesLineFragmentOrigin, context: nil) + + return generateImage(CGSize(width: floor(stringRect.width) + 18.0, height: 15.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(theme.list.itemAccentColor.withMultipliedAlpha(0.1).cgColor) + context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: size.height / 2.0).cgPath) + context.fillPath() + + context.setFillColor(theme.list.itemAccentColor.cgColor) + + let circleSize = CGSize(width: 2.0 - UIScreenPixel, height: 2.0 - UIScreenPixel) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 3.0 + UIScreenPixel), size: circleSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 7.0 - UIScreenPixel), size: circleSize)) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 8.0, y: 10.0), size: circleSize)) + + let textRect = CGRect( + x: 5.0, + y: (size.height - stringRect.height) / 2.0 - UIScreenPixel, + width: stringRect.width, + height: stringRect.height + ) + + UIGraphicsPushContext(context) + titleString.draw(in: textRect) + UIGraphicsPopContext() + }) + }) + } } diff --git a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json deleted file mode 100644 index cae3ed8f8e..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "admock.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png b/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png deleted file mode 100644 index 39dae925bd..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Components/AdMock.imageset/admock.png and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json deleted file mode 100644 index 0e82572e81..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "MockSMS.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png b/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png deleted file mode 100644 index cfb9545fe2..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Components/PayMock.imageset/MockSMS.png and /dev/null differ diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 21a1fe7c2b..894d749af3 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -964,601 +964,651 @@ public final class WebAppController: ViewController, AttachmentContainable { let json = try? JSONSerialization.jsonObject(with: eventData ?? Data(), options: []) as? [String: Any] switch eventName { - case "web_app_ready": - self.animateTransitionIn() - case "web_app_switch_inline_query": - if let json, let query = json["query"] as? String { - if let chatTypes = json["chat_types"] as? [String], !chatTypes.isEmpty { - var requestPeerTypes: [ReplyMarkupButtonRequestPeerType] = [] - for type in chatTypes { - switch type { - case "users": - requestPeerTypes.append(.user(ReplyMarkupButtonRequestPeerType.User(isBot: false, isPremium: nil))) - case "bots": - requestPeerTypes.append(.user(ReplyMarkupButtonRequestPeerType.User(isBot: true, isPremium: nil))) - case "groups": - requestPeerTypes.append(.group(ReplyMarkupButtonRequestPeerType.Group(isCreator: false, hasUsername: nil, isForum: nil, botParticipant: false, userAdminRights: nil, botAdminRights: nil))) - case "channels": - requestPeerTypes.append(.channel(ReplyMarkupButtonRequestPeerType.Channel(isCreator: false, hasUsername: nil, userAdminRights: nil, botAdminRights: nil))) - default: - break - } - } - controller.requestSwitchInline(query, requestPeerTypes, { [weak controller] in - controller?.dismiss() - }) - } else { - controller.dismiss() - controller.requestSwitchInline(query, nil, {}) - } - } - case "web_app_data_send": - if controller.source.isSimple, let eventData = body["eventData"] as? String { - self.handleSendData(data: eventData) - } - case "web_app_setup_main_button": - if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu { - self.delayedScriptMessages.append(message) - } else if let json = json { - if var isVisible = json["is_visible"] as? Bool { - let text = json["text"] as? String - if (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - isVisible = false - } - - let backgroundColorString = json["color"] as? String - let backgroundColor = backgroundColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.fillColor - let textColorString = json["text_color"] as? String - let textColor = textColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.foregroundColor - - let isLoading = json["is_progress_visible"] as? Bool - let isEnabled = json["is_active"] as? Bool - let hasShimmer = json["has_shine_effect"] as? Bool - let state = AttachmentMainButtonState(text: text, font: .bold, background: .color(backgroundColor), textColor: textColor, isVisible: isVisible, progress: (isLoading ?? false) ? .center : .none, isEnabled: isEnabled ?? true, hasShimmer: hasShimmer ?? false) - self.mainButtonState = state - } - } - case "web_app_setup_secondary_button": - if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu { - self.delayedScriptMessages.append(message) - } else if let json = json { - if var isVisible = json["is_visible"] as? Bool { - let text = json["text"] as? String - if (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - isVisible = false - } - - let backgroundColorString = json["color"] as? String - let backgroundColor = backgroundColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.fillColor - let textColorString = json["text_color"] as? String - let textColor = textColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.foregroundColor - - let isLoading = json["is_progress_visible"] as? Bool - let isEnabled = json["is_active"] as? Bool - let hasShimmer = json["has_shine_effect"] as? Bool - let position = json["position"] as? String - - let state = AttachmentMainButtonState(text: text, font: .bold, background: .color(backgroundColor), textColor: textColor, isVisible: isVisible, progress: (isLoading ?? false) ? .center : .none, isEnabled: isEnabled ?? true, hasShimmer: hasShimmer ?? false, position: position.flatMap { AttachmentMainButtonState.Position(rawValue: $0) }) - self.secondaryButtonState = state - } - } - case "web_app_request_viewport": - self.requestLayout(transition: .immediate) - case "web_app_request_safe_area": - self.requestLayout(transition: .immediate) - case "web_app_request_content_safe_area": - self.requestLayout(transition: .immediate) - case "web_app_request_theme": - self.sendThemeChangedEvent() - case "web_app_expand": - if let lastExpansionTimestamp = self.lastExpansionTimestamp, currentTimestamp < lastExpansionTimestamp + 1.0 { - - } else { - self.lastExpansionTimestamp = currentTimestamp - controller.requestAttachmentMenuExpansion() - } - case "web_app_close": - controller.dismiss() - case "web_app_open_tg_link": - if let json = json, let path = json["path_full"] as? String { - let forceRequest = json["force_request"] as? Bool ?? false - controller.openUrl("https://t.me\(path)", false, forceRequest, { [weak controller] in - let _ = controller -// controller?.dismiss() - }) - } - case "web_app_open_invoice": - if let json = json, let slug = json["slug"] as? String { - self.paymentDisposable = (self.context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - } - |> deliverOnMainQueue).start(next: { [weak self] invoice in - if let strongSelf = self, let invoice, let navigationController = strongSelf.controller?.getNavigationController() { - let inputData = Promise() - inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) - }) - if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext { - let starsInputData = combineLatest( - inputData.get(), - starsContext.state - ) - |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in - if let data, let state { - return (state, data.form, data.botPeer, nil) - } else { - return nil - } - } - let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen( - context: strongSelf.context, - starsContext: starsContext, - invoice: invoice, - source: .slug(slug), - extendedMedia: [], - inputData: starsInputData, - completion: { [weak self] paid in - guard let self else { - return - } - self.sendInvoiceClosedEvent(slug: slug, result: paid ? .paid : .cancelled) - } - ) - navigationController.pushViewController(controller) - }) - } else { - let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in - self?.sendInvoiceClosedEvent(slug: slug, result: .paid) - }, cancelled: { [weak self] in - self?.sendInvoiceClosedEvent(slug: slug, result: .cancelled) - }, failed: { [weak self] in - self?.sendInvoiceClosedEvent(slug: slug, result: .failed) - }) - checkoutController.navigationPresentation = .modal - navigationController.pushViewController(checkoutController) - } - } - }) - } - case "web_app_open_link": - if let json = json, let url = json["url"] as? String { - let webAppConfiguration = WebAppConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) - if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), !["http", "https"].contains(scheme) && !webAppConfiguration.allowedProtocols.contains(scheme) { - return - } - - let tryInstantView = json["try_instant_view"] as? Bool ?? false - let tryBrowser = json["try_browser"] as? String - - if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { - self.webView?.lastTouchTimestamp = nil - if tryInstantView { - let _ = (resolveInstantViewUrl(account: self.context.account, url: url) - |> mapToSignal { result -> Signal in - guard case let .result(result) = result else { - return .complete() - } - return .single(result) - } - |> deliverOnMainQueue).start(next: { [weak self] result in - guard let strongSelf = self else { - return - } - switch result { - case let .instantView(webPage, anchor): - let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate)) - strongSelf.controller?.getNavigationController()?.pushViewController(controller) - default: - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) - } - }) - } else { - var url = url - if let tryBrowser { - let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url)) - var matchingOption: OpenInOption? - for option in openInOptions { - if case .other = option.application { - switch tryBrowser { - case "safari": - break - case "chrome": - if option.identifier == "chrome" { - matchingOption = option - break - } - case "firefox": - if ["firefox", "firefoxFocus"].contains(option.identifier) { - matchingOption = option - break - } - case "opera": - if ["operaMini", "operaTouch"].contains(option.identifier) { - matchingOption = option - break - } - default: - break - } - } - } - if let matchingOption, case let .openUrl(newUrl) = matchingOption.action() { - url = newUrl - } - } - - self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) - } - } - } - case "web_app_setup_back_button": - if let json = json, let isVisible = json["is_visible"] as? Bool { - self.hasBackButton = isVisible - self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true) - if controller.isFullscreen { - self.requestLayout(transition: .immediate) - } - } - case "web_app_trigger_haptic_feedback": - if let json = json, let type = json["type"] as? String { - switch type { - case "impact": - if let impactType = json["impact_style"] as? String { - switch impactType { - case "light": - self.hapticFeedback.impact(.light) - case "medium": - self.hapticFeedback.impact(.medium) - case "heavy": - self.hapticFeedback.impact(.heavy) - case "rigid": - self.hapticFeedback.impact(.rigid) - case "soft": - self.hapticFeedback.impact(.soft) - default: - break - } - } - case "notification": - if let notificationType = json["notification_type"] as? String { - switch notificationType { - case "success": - self.hapticFeedback.success() - case "error": - self.hapticFeedback.error() - case "warning": - self.hapticFeedback.warning() - default: - break - } - } - case "selection_change": - self.hapticFeedback.tap() + case "web_app_ready": + self.animateTransitionIn() + case "web_app_switch_inline_query": + if let json, let query = json["query"] as? String { + if let chatTypes = json["chat_types"] as? [String], !chatTypes.isEmpty { + var requestPeerTypes: [ReplyMarkupButtonRequestPeerType] = [] + for type in chatTypes { + switch type { + case "users": + requestPeerTypes.append(.user(ReplyMarkupButtonRequestPeerType.User(isBot: false, isPremium: nil))) + case "bots": + requestPeerTypes.append(.user(ReplyMarkupButtonRequestPeerType.User(isBot: true, isPremium: nil))) + case "groups": + requestPeerTypes.append(.group(ReplyMarkupButtonRequestPeerType.Group(isCreator: false, hasUsername: nil, isForum: nil, botParticipant: false, userAdminRights: nil, botAdminRights: nil))) + case "channels": + requestPeerTypes.append(.channel(ReplyMarkupButtonRequestPeerType.Channel(isCreator: false, hasUsername: nil, userAdminRights: nil, botAdminRights: nil))) default: break + } } + controller.requestSwitchInline(query, requestPeerTypes, { [weak controller] in + controller?.dismiss() + }) + } else { + controller.dismiss() + controller.requestSwitchInline(query, nil, {}) } - case "web_app_set_background_color": - if let json = json, let colorValue = json["color"] as? String, let color = UIColor(hexString: colorValue) { - self.appBackgroundColor = color - self.updateBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) - } - case "web_app_set_header_color": - if let json = json { - if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { - self.headerColor = nil - self.headerColorKey = colorKey - } else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { - self.headerColor = color - self.headerColorKey = nil + } + case "web_app_data_send": + if controller.source.isSimple, let eventData = body["eventData"] as? String { + self.handleSendData(data: eventData) + } + case "web_app_setup_main_button": + if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu { + self.delayedScriptMessages.append(message) + } else if let json = json { + if var isVisible = json["is_visible"] as? Bool { + let text = json["text"] as? String + if (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + isVisible = false } - self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) - } - case "web_app_set_bottom_bar_color": - if let json = json { - if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { - self.bottomPanelColor = color - } - } - case "web_app_open_popup": - if let json, let message = json["message"] as? String, let buttons = json["buttons"] as? [Any] { - let presentationData = self.presentationData - let title = json["title"] as? String - var alertButtons: [TextAlertAction] = [] + let backgroundColorString = json["color"] as? String + let backgroundColor = backgroundColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.fillColor + let textColorString = json["text_color"] as? String + let textColor = textColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.foregroundColor - for buttonJson in buttons.reversed() { - if let button = buttonJson as? [String: Any], let id = button["id"] as? String, let type = button["type"] as? String { - let buttonAction = { - self.sendAlertButtonEvent(id: id) + let isLoading = json["is_progress_visible"] as? Bool + let isEnabled = json["is_active"] as? Bool + let hasShimmer = json["has_shine_effect"] as? Bool + let state = AttachmentMainButtonState(text: text, font: .bold, background: .color(backgroundColor), textColor: textColor, isVisible: isVisible, progress: (isLoading ?? false) ? .center : .none, isEnabled: isEnabled ?? true, hasShimmer: hasShimmer ?? false) + self.mainButtonState = state + } + } + case "web_app_setup_secondary_button": + if let webView = self.webView, !webView.didTouchOnce && controller.url == nil && controller.source == .attachMenu { + self.delayedScriptMessages.append(message) + } else if let json = json { + if var isVisible = json["is_visible"] as? Bool { + let text = json["text"] as? String + if (text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + isVisible = false + } + + let backgroundColorString = json["color"] as? String + let backgroundColor = backgroundColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.fillColor + let textColorString = json["text_color"] as? String + let textColor = textColorString.flatMap({ UIColor(hexString: $0) }) ?? self.presentationData.theme.list.itemCheckColors.foregroundColor + + let isLoading = json["is_progress_visible"] as? Bool + let isEnabled = json["is_active"] as? Bool + let hasShimmer = json["has_shine_effect"] as? Bool + let position = json["position"] as? String + + let state = AttachmentMainButtonState(text: text, font: .bold, background: .color(backgroundColor), textColor: textColor, isVisible: isVisible, progress: (isLoading ?? false) ? .center : .none, isEnabled: isEnabled ?? true, hasShimmer: hasShimmer ?? false, position: position.flatMap { AttachmentMainButtonState.Position(rawValue: $0) }) + self.secondaryButtonState = state + } + } + case "web_app_request_viewport": + self.requestLayout(transition: .immediate) + case "web_app_request_safe_area": + self.requestLayout(transition: .immediate) + case "web_app_request_content_safe_area": + self.requestLayout(transition: .immediate) + case "web_app_request_theme": + self.sendThemeChangedEvent() + case "web_app_expand": + if let lastExpansionTimestamp = self.lastExpansionTimestamp, currentTimestamp < lastExpansionTimestamp + 1.0 { + + } else { + self.lastExpansionTimestamp = currentTimestamp + controller.requestAttachmentMenuExpansion() + } + case "web_app_close": + controller.dismiss() + case "web_app_open_tg_link": + if let json = json, let path = json["path_full"] as? String { + let forceRequest = json["force_request"] as? Bool ?? false + controller.openUrl("https://t.me\(path)", false, forceRequest, { [weak controller] in + let _ = controller +// controller?.dismiss() + }) + } + case "web_app_open_invoice": + if let json = json, let slug = json["slug"] as? String { + self.paymentDisposable = (self.context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> deliverOnMainQueue).start(next: { [weak self] invoice in + if let strongSelf = self, let invoice, let navigationController = strongSelf.controller?.getNavigationController() { + let inputData = Promise() + inputData.set(BotCheckoutController.InputData.fetch(context: strongSelf.context, source: .slug(slug)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + }) + if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext { + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer, nil) + } else { + return nil + } } - let text = button["text"] as? String - switch type { - case "default": - if let text = text { - alertButtons.append(TextAlertAction(type: .genericAction, title: text, action: { - buttonAction() - })) + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen( + context: strongSelf.context, + starsContext: starsContext, + invoice: invoice, + source: .slug(slug), + extendedMedia: [], + inputData: starsInputData, + completion: { [weak self] paid in + guard let self else { + return + } + self.sendInvoiceClosedEvent(slug: slug, result: paid ? .paid : .cancelled) } - case "destructive": - if let text = text { - alertButtons.append(TextAlertAction(type: .destructiveAction, title: text, action: { - buttonAction() - })) + ) + navigationController.pushViewController(controller) + }) + } else { + let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in + self?.sendInvoiceClosedEvent(slug: slug, result: .paid) + }, cancelled: { [weak self] in + self?.sendInvoiceClosedEvent(slug: slug, result: .cancelled) + }, failed: { [weak self] in + self?.sendInvoiceClosedEvent(slug: slug, result: .failed) + }) + checkoutController.navigationPresentation = .modal + navigationController.pushViewController(checkoutController) + } + } + }) + } + case "web_app_open_link": + if let json = json, let url = json["url"] as? String { + let webAppConfiguration = WebAppConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) + if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: escapedUrl), let scheme = url.scheme?.lowercased(), !["http", "https"].contains(scheme) && !webAppConfiguration.allowedProtocols.contains(scheme) { + return + } + + let tryInstantView = json["try_instant_view"] as? Bool ?? false + let tryBrowser = json["try_browser"] as? String + + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { + self.webView?.lastTouchTimestamp = nil + if tryInstantView { + let _ = (resolveInstantViewUrl(account: self.context.account, url: url) + |> mapToSignal { result -> Signal in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + switch result { + case let .instantView(webPage, anchor): + let controller = strongSelf.context.sharedContext.makeInstantPageController(context: strongSelf.context, webPage: webPage, anchor: anchor, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .otherPrivate)) + strongSelf.controller?.getNavigationController()?.pushViewController(controller) + default: + strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + } + }) + } else { + var url = url + if let tryBrowser { + let openInOptions = availableOpenInOptions(context: self.context, item: .url(url: url)) + var matchingOption: OpenInOption? + for option in openInOptions { + if case .other = option.application { + switch tryBrowser { + case "safari": + break + case "chrome": + if option.identifier == "chrome" { + matchingOption = option + break + } + case "firefox": + if ["firefox", "firefoxFocus"].contains(option.identifier) { + matchingOption = option + break + } + case "opera": + if ["operaMini", "operaTouch"].contains(option.identifier) { + matchingOption = option + break + } + default: + break } - case "ok": - alertButtons.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { - buttonAction() - })) - case "cancel": - alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { - buttonAction() - })) - case "close": - alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: { - buttonAction() - })) + } + } + if let matchingOption, case let .openUrl(newUrl) = matchingOption.action() { + url = newUrl + } + } + + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, navigationController: nil, dismissInput: {}) + } + } + } + case "web_app_setup_back_button": + if let json = json, let isVisible = json["is_visible"] as? Bool { + self.hasBackButton = isVisible + self.controller?.cancelButtonNode.setState(isVisible ? .back : .cancel, animated: true) + if controller.isFullscreen { + self.requestLayout(transition: .immediate) + } + } + case "web_app_trigger_haptic_feedback": + if let json = json, let type = json["type"] as? String { + switch type { + case "impact": + if let impactType = json["impact_style"] as? String { + switch impactType { + case "light": + self.hapticFeedback.impact(.light) + case "medium": + self.hapticFeedback.impact(.medium) + case "heavy": + self.hapticFeedback.impact(.heavy) + case "rigid": + self.hapticFeedback.impact(.rigid) + case "soft": + self.hapticFeedback.impact(.soft) default: break } } - } - - var actionLayout: TextAlertContentActionLayout = .horizontal - if alertButtons.count > 2 { - actionLayout = .vertical - alertButtons = Array(alertButtons.reversed()) - } - let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: title, text: message, actions: alertButtons, actionLayout: actionLayout) - alertController.dismissed = { byOutsideTap in - if byOutsideTap { - self.sendAlertButtonEvent(id: nil) - } - } - self.controller?.present(alertController, in: .window(.root)) - } - case "web_app_setup_closing_behavior": - if let json, let needConfirmation = json["need_confirmation"] as? Bool { - self.needDismissConfirmation = needConfirmation - } - case "web_app_open_scan_qr_popup": - var info: String = "" - if let json, let text = json["text"] as? String { - info = text - } - let controller = QrCodeScanScreen(context: self.context, subject: .custom(info: info)) - controller.completion = { [weak self] result in - if let strongSelf = self { - if let result = result { - strongSelf.sendQrCodeScannedEvent(data: result) - } else { - strongSelf.sendQrCodeScannerClosedEvent() - } - } - } - self.currentQrCodeScannerScreen = controller - self.controller?.present(controller, in: .window(.root)) - case "web_app_close_scan_qr_popup": - if let controller = self.currentQrCodeScannerScreen { - self.currentQrCodeScannerScreen = nil - controller.dismissAnimated() - } - case "web_app_read_text_from_clipboard": - if let json, let requestId = json["req_id"] as? String { - let botId = controller.botId - let isAttachMenu = controller.url == nil - - let _ = (self.context.engine.messages.attachMenuBots() - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] attachMenuBots in - guard let self else { - return - } - let currentTimestamp = CACurrentMediaTime() - var fillData = false - - let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) - if isAttachMenu || attachMenuBot != nil { - if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { - self.webView?.lastTouchTimestamp = nil - fillData = true + case "notification": + if let notificationType = json["notification_type"] as? String { + switch notificationType { + case "success": + self.hapticFeedback.success() + case "error": + self.hapticFeedback.error() + case "warning": + self.hapticFeedback.warning() + default: + break } } - - self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) - }) + case "selection_change": + self.hapticFeedback.tap() + default: + break } - case "web_app_request_write_access": - self.requestWriteAccess() - case "web_app_request_phone": - self.shareAccountContact() - case "web_app_invoke_custom_method": - if let json, let requestId = json["req_id"] as? String, let method = json["method"] as? String, let params = json["params"] { - var paramsString: String? - if let string = params as? String { - paramsString = string - } else if let data1 = try? JSONSerialization.data(withJSONObject: params, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) { - paramsString = convertedString + } + case "web_app_set_background_color": + if let json = json, let colorValue = json["color"] as? String, let color = UIColor(hexString: colorValue) { + self.appBackgroundColor = color + self.updateBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) + } + case "web_app_set_header_color": + if let json = json { + if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) { + self.headerColor = nil + self.headerColorKey = colorKey + } else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { + self.headerColor = color + self.headerColorKey = nil + } + self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear)) + } + case "web_app_set_bottom_bar_color": + if let json = json { + if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) { + self.bottomPanelColor = color + } + } + case "web_app_open_popup": + if let json, let message = json["message"] as? String, let buttons = json["buttons"] as? [Any] { + let presentationData = self.presentationData + + let title = json["title"] as? String + var alertButtons: [TextAlertAction] = [] + + for buttonJson in buttons.reversed() { + if let button = buttonJson as? [String: Any], let id = button["id"] as? String, let type = button["type"] as? String { + let buttonAction = { + self.sendAlertButtonEvent(id: id) + } + let text = button["text"] as? String + switch type { + case "default": + if let text = text { + alertButtons.append(TextAlertAction(type: .genericAction, title: text, action: { + buttonAction() + })) + } + case "destructive": + if let text = text { + alertButtons.append(TextAlertAction(type: .destructiveAction, title: text, action: { + buttonAction() + })) + } + case "ok": + alertButtons.append(TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + buttonAction() + })) + case "cancel": + alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + buttonAction() + })) + case "close": + alertButtons.append(TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Close, action: { + buttonAction() + })) + default: + break + } } - self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}") } - case "web_app_setup_settings_button": - if let json, let isVisible = json["is_visible"] as? Bool { - self.controller?.hasSettings = isVisible + + var actionLayout: TextAlertContentActionLayout = .horizontal + if alertButtons.count > 2 { + actionLayout = .vertical + alertButtons = Array(alertButtons.reversed()) } - case "web_app_biometry_get_info": - self.sendBiometryInfoReceivedEvent() - case "web_app_biometry_request_access": - var reason: String? - if let json, let reasonValue = json["reason"] as? String, !reasonValue.isEmpty { - reason = reasonValue + let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: title, text: message, actions: alertButtons, actionLayout: actionLayout) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + self.sendAlertButtonEvent(id: nil) + } } - self.requestBiometryAccess(reason: reason) - case "web_app_biometry_request_auth": - self.requestBiometryAuth() - case "web_app_biometry_update_token": - var tokenData: Data? - if let json, let tokenDataValue = json["token"] as? String, !tokenDataValue.isEmpty { - tokenData = tokenDataValue.data(using: .utf8) + self.controller?.present(alertController, in: .window(.root)) + } + case "web_app_setup_closing_behavior": + if let json, let needConfirmation = json["need_confirmation"] as? Bool { + self.needDismissConfirmation = needConfirmation + } + case "web_app_open_scan_qr_popup": + var info: String = "" + if let json, let text = json["text"] as? String { + info = text + } + let controller = QrCodeScanScreen(context: self.context, subject: .custom(info: info)) + controller.completion = { [weak self] result in + if let strongSelf = self { + if let result = result { + strongSelf.sendQrCodeScannedEvent(data: result) + } else { + strongSelf.sendQrCodeScannerClosedEvent() + } } - self.requestBiometryUpdateToken(tokenData: tokenData) - case "web_app_biometry_open_settings": - if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { - self.webView?.lastTouchTimestamp = nil + } + self.currentQrCodeScannerScreen = controller + self.controller?.present(controller, in: .window(.root)) + case "web_app_close_scan_qr_popup": + if let controller = self.currentQrCodeScannerScreen { + self.currentQrCodeScannerScreen = nil + controller.dismissAnimated() + } + case "web_app_read_text_from_clipboard": + if let json, let requestId = json["req_id"] as? String { + let botId = controller.botId + let isAttachMenu = controller.url == nil + + let _ = (self.context.engine.messages.attachMenuBots() + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] attachMenuBots in + guard let self else { + return + } + let currentTimestamp = CACurrentMediaTime() + var fillData = false + + let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) + if isAttachMenu || attachMenuBot != nil { + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { + self.webView?.lastTouchTimestamp = nil + fillData = true + } + } + + self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) + }) + } + case "web_app_request_write_access": + self.requestWriteAccess() + case "web_app_request_phone": + self.shareAccountContact() + case "web_app_invoke_custom_method": + if let json, let requestId = json["req_id"] as? String, let method = json["method"] as? String, let params = json["params"] { + var paramsString: String? + if let string = params as? String { + paramsString = string + } else if let data1 = try? JSONSerialization.data(withJSONObject: params, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) { + paramsString = convertedString + } + self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}") + } + case "web_app_setup_settings_button": + if let json, let isVisible = json["is_visible"] as? Bool { + self.controller?.hasSettings = isVisible + } + case "web_app_biometry_get_info": + self.sendBiometryInfoReceivedEvent() + case "web_app_biometry_request_access": + var reason: String? + if let json, let reasonValue = json["reason"] as? String, !reasonValue.isEmpty { + reason = reasonValue + } + self.requestBiometryAccess(reason: reason) + case "web_app_biometry_request_auth": + self.requestBiometryAuth() + case "web_app_biometry_update_token": + var tokenData: Data? + if let json, let tokenDataValue = json["token"] as? String, !tokenDataValue.isEmpty { + tokenData = tokenDataValue.data(using: .utf8) + } + self.requestBiometryUpdateToken(tokenData: tokenData) + case "web_app_biometry_open_settings": + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { + self.webView?.lastTouchTimestamp = nil - self.openBotSettings() - } - case "web_app_setup_swipe_behavior": - if let json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { - self.controller?._isPanGestureEnabled = isPanGestureEnabled - } - case "web_app_share_to_story": - if let json, let mediaUrl = json["media_url"] as? String { - let text = json["text"] as? String - let link = json["widget_link"] as? [String: Any] - - var linkUrl: String? - var linkName: String? - if let link { - if let url = link["url"] as? String { - linkUrl = url - if let name = link["name"] as? String { - linkName = name - } + self.openBotSettings() + } + case "web_app_setup_swipe_behavior": + if let json, let isPanGestureEnabled = json["allow_vertical_swipe"] as? Bool { + self.controller?._isPanGestureEnabled = isPanGestureEnabled + } + case "web_app_share_to_story": + if let json, let mediaUrl = json["media_url"] as? String { + let text = json["text"] as? String + let link = json["widget_link"] as? [String: Any] + + var linkUrl: String? + var linkName: String? + if let link { + if let url = link["url"] as? String { + linkUrl = url + if let name = link["name"] as? String { + linkName = name } } - - enum FetchResult { - case result(Data) - case progress(Float) + } + + enum FetchResult { + case result(Data) + case progress(Float) + } + + let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { + })) + self.controller?.present(controller, in: .window(.root)) + + let _ = (fetchHttpResource(url: mediaUrl) + |> map(Optional.init) + |> `catch` { error in + return .single(nil) + } + |> mapToSignal { value -> Signal in + if case let .dataPart(_, data, _, complete) = value, complete { + return .single(.result(data)) + } else if case let .progressUpdated(progress) = value { + return .single(.progress(progress)) + } else { + return .complete() } - - let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: { - })) - self.controller?.present(controller, in: .window(.root)) - - let _ = (fetchHttpResource(url: mediaUrl) - |> map(Optional.init) - |> `catch` { error in - return .single(nil) + } + |> deliverOnMainQueue).start(next: { [weak self, weak controller] next in + guard let self else { + return } - |> mapToSignal { value -> Signal in - if case let .dataPart(_, data, _, complete) = value, complete { - return .single(.result(data)) - } else if case let .progressUpdated(progress) = value { - return .single(.progress(progress)) + controller?.dismiss() + + switch next { + case let .result(data): + var source: Any? + if let image = UIImage(data: data) { + source = image } else { - return .complete() + let tempFile = TempBox.shared.tempFile(fileName: "image.mp4") + if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) { + source = tempFile.path + } } + if let source { + let externalState = MediaEditorTransitionOutExternalState( + storyTarget: nil, + isForcedTarget: false, + isPeerArchived: false, + transitionOut: nil + ) + let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in + let target: Stories.PendingTarget = result.target + externalState.storyTarget = target + + if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { + rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) + } + }) + if let navigationController = self.controller?.getNavigationController() { + navigationController.pushViewController(controller) + } + } + default: + break } - |> deliverOnMainQueue).start(next: { [weak self, weak controller] next in - guard let self else { - return - } - controller?.dismiss() - - switch next { - case let .result(data): - var source: Any? - if let image = UIImage(data: data) { - source = image - } else { - let tempFile = TempBox.shared.tempFile(fileName: "image.mp4") - if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) { - source = tempFile.path - } - } - if let source { - let externalState = MediaEditorTransitionOutExternalState( - storyTarget: nil, - isForcedTarget: false, - isPeerArchived: false, - transitionOut: nil - ) - let controller = self.context.sharedContext.makeStoryMediaEditorScreen(context: self.context, source: source, text: text, link: linkUrl.flatMap { ($0, linkName) }, completion: { result, commit in - let target: Stories.PendingTarget = result.target - externalState.storyTarget = target - - if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit) - } - }) - if let navigationController = self.controller?.getNavigationController() { - navigationController.pushViewController(controller) - } - } - default: - break - } + }) + } + case "web_app_request_fullscreen": + self.setIsFullscreen(true) + case "web_app_exit_fullscreen": + self.setIsFullscreen(false) + case "web_app_start_accelerometer": + if let json { + let refreshRate = json["refresh_rate"] as? Double + self.setIsAccelerometerActive(true, refreshRate: refreshRate) + } + case "web_app_stop_accelerometer": + self.setIsAccelerometerActive(false) + case "web_app_start_device_orientation": + if let json { + let refreshRate = json["refresh_rate"] as? Double + let absolute = (json["need_absolute"] as? Bool) == true + self.setIsDeviceOrientationActive(true, refreshRate: refreshRate, absolute: absolute) + } + case "web_app_stop_device_orientation": + self.setIsDeviceOrientationActive(false) + case "web_app_start_gyroscope": + if let json { + let refreshRate = json["refresh_rate"] as? Double + self.setIsGyroscopeActive(true, refreshRate: refreshRate) + } + case "web_app_stop_gyroscope": + self.setIsGyroscopeActive(false) + case "web_app_set_emoji_status": + if let json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { + let duration = json["duration"] as? Double + self.setEmojiStatus(emojiId, duration: duration.flatMap { Int32($0) }) + } + case "web_app_add_to_home_screen": + self.addToHomeScreen() + case "web_app_check_home_screen": + let data: JSON = ["status": "unknown"] + self.webView?.sendEvent(name: "home_screen_checked", data: data.string) + case "web_app_request_location": + self.requestLocation() + case "web_app_check_location": + self.checkLocation() + case "web_app_open_location_settings": + if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { + self.webView?.lastTouchTimestamp = nil + + self.openLocationSettings() + } + case "web_app_send_prepared_message": + if let json, let id = json["id"] as? String { + self.sendPreparedMessage(id: id) + } + case "web_app_request_emoji_status_access": + self.requestEmojiStatusAccess() + case "web_app_request_file_download": + if let json, let url = json["url"] as? String, let fileName = json["file_name"] as? String { + self.downloadFile(url: url, fileName: fileName) + } + case "web_app_toggle_orientation_lock": + if let json, let lock = json["locked"] as? Bool { + controller.parentController()?.lockOrientation = lock + } + case "web_app_device_storage_save_key": + if let json, let requestId = json["req_id"] as? String, let key = json["key"] as? String, let value = json["value"] { + var effectiveValue: String? + if let stringValue = value as? String { + effectiveValue = stringValue + } else { + effectiveValue = nil + } + let _ = self.context.engine.peers.setBotStorageValue(peerId: controller.botId, key: key, value: effectiveValue).start(error: { [weak self] _ in + let data: JSON = [ + "req_id": requestId, + "error": "UNKNOWN_ERROR" + ] + self?.webView?.sendEvent(name: "device_storage_failed", data: data.string) + }, completed: { [weak self] in + let data: JSON = [ + "req_id": requestId + ] + self?.webView?.sendEvent(name: "device_storage_key_saved", data: data.string) + }) + } + case "web_app_device_storage_get_key": + if let json, let requestId = json["req_id"] as? String { + if let key = json["key"] as? String { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.BotStorageValue(id: controller.botId, key: key)) + |> deliverOnMainQueue).start(next: { [weak self] value in + let data: JSON = [ + "req_id": requestId, + "value": value ?? NSNull() + ] + self?.webView?.sendEvent(name: "device_storage_key_received", data: data.string) }) + } else { + let data: JSON = [ + "req_id": requestId, + "error": "KEY_INVALID" + ] + self.webView?.sendEvent(name: "device_storage_failed", data: data.string) } - case "web_app_request_fullscreen": - self.setIsFullscreen(true) - case "web_app_exit_fullscreen": - self.setIsFullscreen(false) - case "web_app_start_accelerometer": - if let json { - let refreshRate = json["refresh_rate"] as? Double - self.setIsAccelerometerActive(true, refreshRate: refreshRate) - } - case "web_app_stop_accelerometer": - self.setIsAccelerometerActive(false) - case "web_app_start_device_orientation": - if let json { - let refreshRate = json["refresh_rate"] as? Double - let absolute = (json["need_absolute"] as? Bool) == true - self.setIsDeviceOrientationActive(true, refreshRate: refreshRate, absolute: absolute) - } - case "web_app_stop_device_orientation": - self.setIsDeviceOrientationActive(false) - case "web_app_start_gyroscope": - if let json { - let refreshRate = json["refresh_rate"] as? Double - self.setIsGyroscopeActive(true, refreshRate: refreshRate) - } - case "web_app_stop_gyroscope": - self.setIsGyroscopeActive(false) - case "web_app_set_emoji_status": - if let json, let emojiIdString = json["custom_emoji_id"] as? String, let emojiId = Int64(emojiIdString) { - let duration = json["duration"] as? Double - self.setEmojiStatus(emojiId, duration: duration.flatMap { Int32($0) }) - } - case "web_app_add_to_home_screen": - self.addToHomeScreen() - case "web_app_check_home_screen": - let data: JSON = ["status": "unknown"] - self.webView?.sendEvent(name: "home_screen_checked", data: data.string) - case "web_app_request_location": - self.requestLocation() - case "web_app_check_location": - self.checkLocation() - case "web_app_open_location_settings": - if let lastTouchTimestamp = self.webView?.lastTouchTimestamp, currentTimestamp < lastTouchTimestamp + 10.0 { - self.webView?.lastTouchTimestamp = nil - - self.openLocationSettings() - } - case "web_app_send_prepared_message": - if let json, let id = json["id"] as? String { - self.sendPreparedMessage(id: id) - } - case "web_app_request_emoji_status_access": - self.requestEmojiStatusAccess() - case "web_app_request_file_download": - if let json, let url = json["url"] as? String, let fileName = json["file_name"] as? String { - self.downloadFile(url: url, fileName: fileName) - } - case "web_app_toggle_orientation_lock": - if let json, let lock = json["locked"] as? Bool { - controller.parentController()?.lockOrientation = lock - } - default: - break + } + case "web_app_device_storage_clear": + if let json, let requestId = json["req_id"] as? String { + let _ = (self.context.engine.peers.clearBotStorage(peerId: controller.botId) + |> deliverOnMainQueue).start(completed: { [weak self] in + let data: JSON = [ + "req_id": requestId + ] + self?.webView?.sendEvent(name: "device_storage_cleared", data: data.string) + }) + } + default: + break } }