diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 20d80e1fa4..1adeb98195 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12203,6 +12203,7 @@ Sorry for the inconvenience."; "HashtagSearch.NoResults" = "No Results"; "HashtagSearch.NoResultsQueryDescription" = "There were no results for %@.\nTry another hashtag."; +"HashtagSearch.NoResultsQueryCashtagDescription" = "There were no results for %@.\nTry another cashtag."; "Chat.Context.Phone.AddToContacts" = "Add to Contacts"; "Chat.Context.Phone.CreateNewContact" = "Create New Contact"; @@ -12314,3 +12315,13 @@ Sorry for the inconvenience."; "BusinessLink.AlertTextLimitText" = "The message text limit is 4096 characters"; "Chat.SendMessageMenu.EditMessage" = "Edit Message"; + +"Story.ViewLink" = "Open Link"; + +"PeerInfo.Bot.Balance" = "Balance"; +"PeerInfo.Bot.Balance.Stars_1" = "%@ Star"; +"PeerInfo.Bot.Balance.Stars_any" = "%@ Stars"; + +"HashtagSearch.StoriesFound_1" = "%@ Story Found"; +"HashtagSearch.StoriesFound_any" = "%@ Stories Found"; +"HashtagSearch.StoriesFoundInfo" = "View stories with %@"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c272a9d0ed..a00192a1fb 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -970,7 +970,7 @@ public protocol SharedAccountContext: AnyObject { func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, isScheduledMessages: Bool, isFile: Bool, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String, all: Bool) -> ViewController - func makeStorySearchController(context: AccountContext, query: String) -> ViewController + func makeStorySearchController(context: AccountContext, query: String, listContext: SearchStoryListContext?) -> ViewController func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController func makeArchiveSettingsController(context: AccountContext) -> ViewController func makeFilterSettingsController(context: AccountContext, modal: Bool, scrollToTags: Bool, dismissed: (() -> Void)?) -> ViewController @@ -1046,6 +1046,7 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController + func makeStarsStatisticsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 9c0fe7ce9a..8ccd7133f9 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -227,6 +227,7 @@ public extension ChatMessageItemAssociatedData { public enum ChatControllerInteractionLongTapAction { case url(String) + case phone(String) case mention(String) case peerMention(EnginePeer.Id, String) case command(String) diff --git a/submodules/Camera/Sources/CameraMetrics.swift b/submodules/Camera/Sources/CameraMetrics.swift index 8bef2e9d07..c1c8a3e429 100644 --- a/submodules/Camera/Sources/CameraMetrics.swift +++ b/submodules/Camera/Sources/CameraMetrics.swift @@ -46,8 +46,10 @@ public extension Camera { return [1.0] case .iPhone14, .iPhone14Plus, .iPhone15, .iPhone15Plus: return [0.5, 1.0, 2.0] - case .iPhone14Pro, .iPhone14ProMax, .iPhone15Pro, .iPhone15ProMax: + case .iPhone14Pro, .iPhone14ProMax, .iPhone15Pro: return [0.5, 1.0, 2.0, 3.0] + case .iPhone15ProMax: + return [0.5, 1.0, 2.0, 5.0] case .unknown: return [1.0, 2.0] } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index da1a4b3fc3..472b4961a4 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2928,6 +2928,20 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) }))) } else if case let .channel(channel) = peer { + if channel.hasPermission(.postStories) { + items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryFeed_ContextAddStory, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: { + guard let self else { + return + } + + self.openStoryCamera(fromList: true) + }) + }))) + } + let openTitle: String let openIcon: String switch channel.info { diff --git a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift index 6336580241..7f6756ad14 100644 --- a/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift +++ b/submodules/DeviceLocationManager/Sources/DeviceLocationManager.swift @@ -54,7 +54,7 @@ public final class DeviceLocationManager: NSObject { self.manager.delegate = self self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - self.manager.distanceFilter = 5.0 +// self.manager.distanceFilter = 5.0 self.manager.activityType = .other self.manager.pausesLocationUpdatesAutomatically = false self.manager.headingFilter = 2.0 diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 982235d599..075cd90b65 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3088,12 +3088,15 @@ public final class DrawingToolsInteraction { var isVideo = false var isAdditional = false var isMessage = false + var isLink = false if let entity = entityView.entity as? DrawingStickerEntity { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true isAdditional = isAdditionalValue } else if case .message = entity.content { isMessage = true + } else if case .link = entity.content { + isLink = true } } @@ -3112,7 +3115,7 @@ public final class DrawingToolsInteraction { } })) } - if let entityView = entityView as? DrawingLocationEntityView { + if entityView is DrawingLocationEntityView || isLink { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in if let self, let entityView { self.editEntity(entityView.entity) @@ -3126,7 +3129,7 @@ public final class DrawingToolsInteraction { self.entitiesView.selectEntity(entityView.entity) } })) - } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage { + } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage && !isLink { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Flip, accessibilityLabel: presentationData.strings.Paint_Flip), action: { [weak self] in if let self { self.flipSelectedEntity() diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index 0299a883cf..1b7bb28e75 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -142,6 +142,8 @@ public class DrawingStickerEntityView: DrawingEntityView { return image } else if case .message = self.stickerEntity.content { return self.animatedImageView?.image + } else if case .link = self.stickerEntity.content { + return self.animatedImageView?.image } else { return nil } @@ -169,6 +171,13 @@ public class DrawingStickerEntityView: DrawingEntityView { return CGSize(width: 512.0, height: 512.0) case let .message(_, size, _, _, _): return size + case let .link(_, _, _, _, size, compactSize, style): + switch style { + case .white, .black: + return size ?? compactSize + case .whiteCompact, .blackCompact: + return compactSize + } } } @@ -296,6 +305,10 @@ public class DrawingStickerEntityView: DrawingEntityView { if let file, let _ = mediaRect { self.setupWithVideo(file) } + } else if case .link = self.stickerEntity.content { + if let image = self.stickerEntity.renderImage { + self.setupWithImage(image, overlayImage: nil) + } } } @@ -682,6 +695,32 @@ public class DrawingStickerEntityView: DrawingEntityView { } + override func selectedTapAction() -> Bool { + if case let .link(url, name, positionBelowText, largeMedia, size, compactSize, style) = self.stickerEntity.content { + let updatedStyle: DrawingStickerEntity.Content.LinkStyle + switch style { + case .white: + updatedStyle = .black + case .black: + updatedStyle = .whiteCompact + case .whiteCompact: + updatedStyle = .blackCompact + case .blackCompact: + if let _ = size { + updatedStyle = .white + } else { + updatedStyle = .whiteCompact + } + } + self.stickerEntity.content = .link(url, name, positionBelowText, largeMedia, size, compactSize, updatedStyle) + self.animatedImageView?.image = nil + self.update(animated: false) + return true + } else { + return super.selectedTapAction() + } + } + func innerLayoutSubview(boundingSize: CGSize) -> CGSize { return boundingSize } @@ -701,6 +740,21 @@ public class DrawingStickerEntityView: DrawingEntityView { if let image { self.setupWithImage(image) } + } else if case let .link(_, _, _, _, _, _, style) = self.stickerEntity.content, self.animatedImageView?.image == nil { + let image: UIImage? + switch style { + case .white: + image = self.stickerEntity.renderImage + case .black: + image = self.stickerEntity.secondaryRenderImage + case .whiteCompact: + image = self.stickerEntity.tertiaryRenderImage + case .blackCompact: + image = self.stickerEntity.quaternaryRenderImage + } + if let image { + self.setupWithImage(image) + } } self.updateMirroring(animated: animated) @@ -1085,7 +1139,7 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { let aspectRatio = entity.baseSize.width / entity.baseSize.height let width: CGFloat - let height: CGFloat + var height: CGFloat if entity.baseSize.width > entity.baseSize.height { width = self.bounds.width - inset * 2.0 @@ -1102,6 +1156,14 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { if case .message = entity.content { cornerRadius *= 2.1 count = 24 + } else if case .link = entity.content { + count = 24 + if height > 0.0 && width / height > 5.0 { + height *= 1.6 + } else { + cornerRadius *= 2.1 + height *= 1.2 + } } else if case .image = entity.content { count = 24 } diff --git a/submodules/Geocoding/Sources/Geocoding.swift b/submodules/Geocoding/Sources/Geocoding.swift index c0e43e749d..ce8bb7f02f 100644 --- a/submodules/Geocoding/Sources/Geocoding.swift +++ b/submodules/Geocoding/Sources/Geocoding.swift @@ -38,6 +38,7 @@ public struct ReverseGeocodedPlacemark { public let name: String? public let street: String? public let city: String? + public let state: String? public let country: String? public let countryCode: String? @@ -79,12 +80,12 @@ public func reverseGeocodeLocation(latitude: Double, longitude: Double, locale: let countryCode = placemark.isoCountryCode let result: ReverseGeocodedPlacemark if placemark.thoroughfare == nil && placemark.locality == nil && placemark.country == nil { - result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, country: nil, countryCode: nil) + result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, state: nil, country: nil, countryCode: nil) } else { if placemark.thoroughfare == nil && placemark.locality == nil, let ocean = placemark.ocean { - result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, country: countryName, countryCode: countryCode) + result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, state: nil, country: countryName, countryCode: countryCode) } else { - result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, country: countryName, countryCode: countryCode) + result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, state: placemark.administrativeArea, country: countryName, countryCode: countryCode) } } subscriber.putNext(result) diff --git a/submodules/HashtagSearchUI/BUILD b/submodules/HashtagSearchUI/BUILD index 91c74fef79..79973de2e0 100644 --- a/submodules/HashtagSearchUI/BUILD +++ b/submodules/HashtagSearchUI/BUILD @@ -24,6 +24,10 @@ swift_library( "//submodules/Postbox:Postbox", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/ComponentFlow", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 9e2496cca9..dfdd9fbaef 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -23,7 +23,7 @@ public final class HashtagSearchController: TelegramBaseController { private var transitionDisposable: Disposable? private let openMessageFromSearchDisposable = MetaDisposable() - private var presentationData: PresentationData + private(set) var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let animationCache: AnimationCache @@ -54,14 +54,17 @@ public final class HashtagSearchController: TelegramBaseController { self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings + if let self { + let previousTheme = self.presentationData.theme + let previousStrings = self.presentationData.strings - strongSelf.presentationData = presentationData + self.presentationData = presentationData if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings() + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style + + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.controllerNode.updatePresentationData(self.presentationData) } } }) @@ -88,15 +91,7 @@ public final class HashtagSearchController: TelegramBaseController { self.displayNodeDidLoad() } - - private func updateThemeAndStrings() { - self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style - self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) - - self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) - } - public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 6e7883caa7..55b39a4f9a 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -1,6 +1,7 @@ import Display import UIKit import AsyncDisplayKit +import ComponentFlow import SwiftSignalKit import TelegramCore import TelegramPresentationData @@ -13,6 +14,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg private let context: AccountContext private weak var controller: HashtagSearchController? private var query: String + private var isCashtag = false + private var presentationData: PresentationData private let searchQueryPromise = ValuePromise() private var searchQueryDisposable: Disposable? @@ -35,6 +38,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg let globalController: ChatController? let globalChatContents: HashtagSearchGlobalChatContents? + private var globalStorySearchContext: SearchStoryListContext? + private var globalStorySearchDisposable = MetaDisposable() + private var globalStorySearchState: StoryListContext.State? + private var globalStorySearchComponentView: ComponentView? + private var panRecognizer: InteractiveTransitionGestureRecognizer? private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -45,6 +53,8 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.controller = controller self.query = query self.navigationBar = navigationBar + self.isCashtag = query.hasPrefix("$") + self.presentationData = controller.presentationData let presentationData = context.sharedContext.currentPresentationData.with { $0 } @@ -53,8 +63,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.containerNode = ASDisplayNode() - let cleanHashtag = cleanHashtag(query) - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: cleanHashtag, hasCurrentChat: peer != nil, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: query, hasCurrentChat: peer != nil, cancel: { [weak controller] in controller?.dismiss() }) @@ -75,20 +84,20 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController = nil } - let myChatContents = HashtagSearchGlobalChatContents(context: context, query: cleanHashtag, publicPosts: false) + let myChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: false) self.myChatContents = myChatContents self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default)) self.myController?.alwaysShowSearchResultsAsList = true self.myController?.showListEmptyResults = true self.myController?.customNavigationController = navigationController - let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: cleanHashtag, publicPosts: true) + let globalChatContents = HashtagSearchGlobalChatContents(context: context, query: query, publicPosts: true) self.globalChatContents = globalChatContents self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default)) self.globalController?.alwaysShowSearchResultsAsList = true self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController - + if controller.publicPosts { self.searchContentNode.selectedIndex = 2 } else if peer == nil { @@ -140,9 +149,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } else if index == 2 { self.isSearching.set(self.globalChatContents?.searching ?? .single(false)) } - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) } self.recentListNode.setSearchQuery = { [weak self] query in @@ -172,9 +179,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self?.searchQueryPromise.set(query) } - let _ = addRecentHashtagSearchQuery(engine: context.engine, string: cleanHashtag).startStandalone() - self.searchContentNode.onReturn = { query in + if !self.isCashtag { let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + self.searchContentNode.onReturn = { query in + let _ = addRecentHashtagSearchQuery(engine: context.engine, string: "#" + query).startStandalone() + } } let throttledSearchQuery = self.searchQueryPromise.get() @@ -190,7 +199,13 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.searchQueryDisposable = (throttledSearchQuery |> deliverOnMainQueue).start(next: { [weak self] query in if let self { - self.updateSearchQuery(query) + let prefix: String + if self.isCashtag { + prefix = "$" + } else { + prefix = "#" + } + self.updateSearchQuery(prefix + query) } }) @@ -202,11 +217,14 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg transition.updateAlpha(node: self.shimmerNode, alpha: isSearching ? 1.0 : 0.0) } }) + + self.updateStorySearch() } deinit { self.searchQueryDisposable?.dispose() self.isSearchingDisposable?.dispose() + self.globalStorySearchDisposable.dispose() } private var panAllowedDirections: InteractiveTransitionGestureRecognizerDirections { @@ -278,9 +296,7 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.searchContentNode.transitionFraction = self.panTransitionFraction - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) - } + self.requestUpdate(transition: .immediate) case .ended, .cancelled: var directionIsToRight: Bool? if abs(velocity) > 10.0 { @@ -320,30 +336,53 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.panTransitionFraction = 0.0 self.searchContentNode.transitionFraction = nil - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring)) - } + self.requestUpdate(transition: .animated(duration: 0.4, curve: .spring)) default: break } } - func updateSearchQuery(_ query: String) { + private func updateSearchQuery(_ query: String) { + let queryUpdated = self.query != query self.query = query - let cleanQuery = cleanHashtag(query) - if !cleanQuery.isEmpty { - self.currentController?.beginMessageSearch("#" + cleanQuery) + if !query.isEmpty { + self.currentController?.beginMessageSearch(query) - self.myChatContents?.hashtagSearchUpdate(query: cleanQuery) - self.myController?.beginMessageSearch("#" + cleanQuery) + self.myChatContents?.hashtagSearchUpdate(query: query) + self.myController?.beginMessageSearch(query) - self.globalChatContents?.hashtagSearchUpdate(query: cleanQuery) - self.globalController?.beginMessageSearch("#" + cleanQuery) + self.globalChatContents?.hashtagSearchUpdate(query: query) + self.globalController?.beginMessageSearch(query) } - if let (layout, navigationHeight) = self.containerLayout { - let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) + if queryUpdated { + self.updateStorySearch() + } + + self.requestUpdate(transition: .immediate) + } + + private func updateStorySearch() { + self.globalStorySearchState = nil + self.globalStorySearchDisposable.set(nil) + self.globalStorySearchContext = nil + + if !self.query.isEmpty { + let globalStorySearchContext = SearchStoryListContext(account: self.context.account, source: .hashtag(self.query)) + self.globalStorySearchDisposable.set((globalStorySearchContext.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + if state.totalCount > 0 { + self.globalStorySearchState = state + } else { + self.globalStorySearchState = nil + } + self.requestUpdate(transition: .animated(duration: 0.25, curve: .easeInOut)) + })) + self.globalStorySearchContext = globalStorySearchContext } } @@ -351,9 +390,11 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg self.currentController?.cancelSelectingMessages() } - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.backgroundColor = theme.chatList.backgroundColor - self.searchContentNode.updateTheme(theme) + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = presentationData.theme.chatList.backgroundColor + self.searchContentNode.updateTheme(presentationData.theme) } func scrollToTop() { @@ -366,6 +407,12 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } + func requestUpdate(transition: ContainedViewLayoutTransition) { + if let (layout, navigationHeight) = self.containerLayout { + let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition) + } + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { let isFirstTime = self.containerLayout == nil self.containerLayout = (layout, navigationBarHeight) @@ -407,8 +454,53 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } if let controller = self.globalController { + var topInset: CGFloat = insets.top - 89.0 + if let state = self.globalStorySearchState { + let componentView: ComponentView + var panelTransition = Transition(transition) + if let current = self.globalStorySearchComponentView { + componentView = current + } else { + panelTransition = .immediate + componentView = ComponentView() + self.globalStorySearchComponentView = componentView + } + let panelSize = componentView.update( + transition: .immediate, + component: AnyComponent(StoryResultsPanelComponent( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + query: self.query, + state: state, + sideInset: layout.safeInsets.left, + action: { [weak self] in + guard let self else { + return + } + let searchController = self.context.sharedContext.makeStorySearchController(context: self.context, query: self.query, listContext: self.globalStorySearchContext) + self.controller?.push(searchController) + } + )), + environment: {}, + containerSize: layout.size + ) + let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top - 36.0), size: panelSize) + if let view = componentView.view { + if view.superview == nil { + controller.view.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: -panelSize.height), to: .zero, duration: 0.25, additive: true) + } + panelTransition.setFrame(view: view, frame: panelFrame) + } + topInset += panelSize.height + } else if let globalStorySearchComponentView = self.globalStorySearchComponentView { + globalStorySearchComponentView.view?.removeFromSuperview() + self.globalStorySearchComponentView = nil + } + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: layout.size.width * 2.0, y: 0.0), size: layout.size)) - controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) if controller.displayNode.supernode == nil { controller.viewWillAppear(false) @@ -458,14 +550,3 @@ final class HashtagSearchControllerNode: ASDisplayNode, ASGestureRecognizerDeleg } } } - -private func cleanHashtag(_ string: String) -> String { - var string = string - if string.hasPrefix("#") { - string.removeFirst() - } - if string.hasPrefix("$") { - string.removeFirst() - } - return string -} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift index 4db226fcd8..3eabc0345c 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift @@ -54,7 +54,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: nil) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: nil) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: nil) } self.isSearchingPromise.set(true) @@ -102,7 +102,7 @@ final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { if self.publicPosts { search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: self.currentSearchState) } else { - search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: currentSearchState) + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: self.query, state: currentSearchState) } self.historyViewDisposable?.dispose() diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index ebd74ef10c..2bbfab6224 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -66,8 +66,18 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { self.hasCurrentChat = hasCurrentChat self.cancel = cancel + + let icon: SearchBarNode.Icon + if initialQuery.hasPrefix("$") { + icon = .cashtag + } else { + icon = .hashtag + } - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, icon: .hashtag, displayBackground: false) + var initialQuery = initialQuery + initialQuery.removeFirst() + + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, icon: icon, displayBackground: false) self.searchBar.text = initialQuery self.searchBar.placeholderString = NSAttributedString(string: strings.HashtagSearch_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) diff --git a/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift new file mode 100644 index 0000000000..32a693623c --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/StoryResultsPanelComponent.swift @@ -0,0 +1,193 @@ +import Foundation +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import MultilineTextComponent +import BundleIconComponent +import StorySetIndicatorComponent +import AccountContext + +final class StoryResultsPanelComponent: CombinedComponent { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let query: String + let state: StoryListContext.State + let sideInset: CGFloat + let action: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + query: String, + state: StoryListContext.State, + sideInset: CGFloat, + action: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.query = query + self.state = state + self.sideInset = sideInset + self.action = action + } + + static func ==(lhs: StoryResultsPanelComponent, rhs: StoryResultsPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.query != rhs.query { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.sideInset != rhs.sideInset { + return false + } + return true + } + + static var body: Body { + let background = Child(Rectangle.self) + let avatars = Child(StorySetIndicatorComponent.self) + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let arrow = Child(BundleIconComponent.self) + let separator = Child(Rectangle.self) + let button = Child(Button.self) + + return { context in + let component = context.component + + let spacing: CGFloat = 3.0 + + let textLeftInset: CGFloat = 81.0 + component.sideInset + let textTopInset: CGFloat = 9.0 + + var existingPeerIds = Set() + var items: [StorySetIndicatorComponent.Item] = [] + for item in component.state.items { + guard let peer = item.peer, !existingPeerIds.contains(peer.id) else { + continue + } + existingPeerIds.insert(peer.id) + items.append(StorySetIndicatorComponent.Item(storyItem: item.storyItem, peer: peer)) + } + + let avatars = avatars.update( + component: StorySetIndicatorComponent( + context: component.context, + strings: component.strings, + items: Array(items.prefix(3)), + displayAvatars: true, + hasUnseen: true, + hasUnseenPrivate: false, + totalCount: 0, + theme: component.theme, + action: {} + ), + availableSize: context.availableSize, + transition: .immediate + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.HashtagSearch_StoriesFound(Int32(component.state.totalCount)), + font: Font.semibold(15.0), + textColor: component.theme.rootController.navigationBar.primaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let text = text.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.strings.HashtagSearch_StoriesFoundInfo(component.query).string, + font: Font.regular(14.0), + textColor: component.theme.rootController.navigationBar.secondaryTextColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .natural, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - textLeftInset, height: context.availableSize.height), + transition: .immediate + ) + + let arrow = arrow.update( + component: BundleIconComponent( + name: "Item List/DisclosureArrow", + tintColor: component.theme.list.disclosureArrowColor + ), + availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), + transition: .immediate + ) + + let size = CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + spacing + text.size.height + textTopInset + 2.0) + + let background = background.update( + component: Rectangle(color: component.theme.rootController.navigationBar.opaqueBackgroundColor), + availableSize: size, + transition: .immediate + ) + + let separator = separator.update( + component: Rectangle(color: component.theme.rootController.navigationBar.separatorColor), + availableSize: CGSize(width: size.width, height: UIScreenPixel), + transition: .immediate + ) + + let button = button.update( + component: Button( + content: AnyComponent(Rectangle(color: .clear)), + action: component.action + ), + availableSize: size, + transition: .immediate + ) + + context.add(background + .position(CGPoint(x: background.size.width / 2.0, y: background.size.height / 2.0)) + ) + + context.add(separator + .position(CGPoint(x: background.size.width / 2.0, y: background.size.height - separator.size.height / 2.0)) + ) + + context.add(avatars + .position(CGPoint(x: component.sideInset + 10.0 + 30.0, y: background.size.height / 2.0)) + ) + + context.add(title + .position(CGPoint(x: textLeftInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textLeftInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(arrow + .position(CGPoint(x: context.availableSize.width - arrow.size.width - component.sideInset, y: size.height / 2.0)) + ) + + context.add(button + .position(CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + ) + + return size + } + } +} diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index a47eb382ab..be4312b6e6 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -822,7 +822,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } - let map = TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let attributes: [InstantPageImageAttribute] = [InstantPageMapAttribute(zoom: zoom, dimensions: dimensions.cgSize)] var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h index 9fa2410cae..b20f65fd31 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCamera.h @@ -85,6 +85,8 @@ typedef enum @property (nonatomic, assign) CGFloat zoomLevel; @property (nonatomic, readonly) CGFloat minZoomLevel; @property (nonatomic, readonly) CGFloat maxZoomLevel; +@property (nonatomic, readonly) int32_t maxMarkZoomValue; +@property (nonatomic, readonly) int32_t secondMarkZoomValue; - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h index 2a816abd14..04ef8a5a7a 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/PGCameraCaptureSession.h @@ -30,6 +30,9 @@ @property (nonatomic, readonly) CGFloat minZoomLevel; @property (nonatomic, readonly) CGFloat maxZoomLevel; +@property (nonatomic, readonly) int32_t maxMarkZoomValue; +@property (nonatomic, readonly) int32_t secondMarkZoomValue; + - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated; @property (nonatomic, readonly) bool hasUltrawideCamera; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h index 540134b897..3854bd1dfa 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraMainView.h @@ -17,6 +17,7 @@ @class TGMediaPickerPhotoStripView; @class TGMediaPickerGallerySelectedItemsModel; @class TGMediaEditingContext; +@class PGCamera; @interface TGCameraCornersView : UIImageView @@ -67,7 +68,7 @@ @property (nonatomic, assign) CGRect previewViewFrame; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera; +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera; - (void)setDocumentFrameHidden:(bool)hidden; - (void)setCameraMode:(PGCameraMode)mode; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraZoomView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraZoomView.h index 69a21795fb..98e29136e0 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraZoomView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraZoomView.h @@ -29,7 +29,7 @@ - (void)panGesture:(UIPanGestureRecognizer *)gestureRecognizer; -- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel; +- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera hasCenterRightZoom:(bool)hasCenterRightZoom minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel secondMarkZoomValue:(CGFloat)secondMarkZoomValue; @end diff --git a/submodules/LegacyComponents/Sources/PGCamera.m b/submodules/LegacyComponents/Sources/PGCamera.m index 2e4348a9eb..c06c9db451 100644 --- a/submodules/LegacyComponents/Sources/PGCamera.m +++ b/submodules/LegacyComponents/Sources/PGCamera.m @@ -765,6 +765,14 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus"; }]; } +- (int32_t)maxMarkZoomValue { + return self.captureSession.maxMarkZoomValue; +} + +- (int32_t)secondMarkZoomValue { + return self.captureSession.secondMarkZoomValue; +} + #pragma mark - Device Angle - (void)startDeviceAngleMeasuring diff --git a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m index 28b7f4a23c..32a96a696a 100644 --- a/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m +++ b/submodules/LegacyComponents/Sources/PGCameraCaptureSession.m @@ -540,7 +540,7 @@ const NSInteger PGCameraFrameRate = 30; } - (CGFloat)maxZoomLevel { - return MIN(16.0f, self.videoDevice.activeFormat.videoMaxZoomFactor); + return MIN(64.0f, self.videoDevice.activeFormat.videoMaxZoomFactor); } - (void)resetZoom { @@ -551,6 +551,14 @@ const NSInteger PGCameraFrameRate = 30; [self setZoomLevel:zoomLevel animated:false]; } +- (int32_t)maxMarkZoomValue { + return 25.0; +} + +- (int32_t)secondMarkZoomValue { + return 5.0; +} + - (void)setZoomLevel:(CGFloat)zoomLevel animated:(bool)animated { if (![self.videoDevice respondsToSelector:@selector(setVideoZoomFactor:)]) @@ -574,10 +582,10 @@ const NSInteger PGCameraFrameRate = 30; if (level < 1.0) { level = MAX(0.5, level); backingLevel = 1.0 + ((level - 0.5) / 0.5) * (firstMark - 1.0); - } else if (zoomLevel < 2.0) { - backingLevel = firstMark + ((level - 1.0) / 1.0) * (secondMark - firstMark); + } else if (zoomLevel < self.secondMarkZoomValue) { + backingLevel = firstMark + ((level - 1.0) / (self.secondMarkZoomValue - 1.0)) * (secondMark - firstMark); } else { - backingLevel = secondMark + ((level - 2.0) / 6.0) * (self.maxZoomLevel - secondMark); + backingLevel = secondMark + ((level - self.secondMarkZoomValue) / (self.maxMarkZoomValue - self.secondMarkZoomValue)) * (self.maxZoomLevel - secondMark); } } else if (marks.count == 1) { CGFloat mark = [marks.firstObject floatValue]; @@ -598,7 +606,7 @@ const NSInteger PGCameraFrameRate = 30; } } } - CGFloat finalLevel = MAX(1.0, MIN([strongSelf maxZoomLevel], backingLevel)); + CGFloat finalLevel = MAX(1.0, MIN([strongSelf maxZoomLevel], backingLevel)); if (animated) { bool zoomingIn = finalLevel > self.videoDevice.videoZoomFactor; bool needsCrossfade = level >= 1.0; diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index fb5e591389..f9c31a60da 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -307,12 +307,12 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { - _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainPhoneView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; } else { - _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera]; + _interfaceView = [[TGCameraMainTabletView alloc] initWithFrame:screenBounds avatar:_intent == TGCameraControllerAvatarIntent videoModeByDefault:_intent == TGCameraControllerGenericVideoOnlyIntent hasUltrawideCamera:_camera.hasUltrawideCamera hasTelephotoCamera:_camera.hasTelephotoCamera camera:_camera]; [_interfaceView setInterfaceOrientation:interfaceOrientation animated:false]; CGSize referenceSize = [self referenceViewSizeForOrientation:interfaceOrientation]; @@ -806,29 +806,29 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus } }; - _camera.captureSession.crossfadeNeeded = ^{ - __strong TGCameraController *strongSelf = weakSelf; - if (strongSelf != nil) - { - if (strongSelf->_crossfadingForZoom) { - return; - } - strongSelf->_crossfadingForZoom = true; - - [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) - { - TGDispatchOnMainThread(^ - { - [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:false]; - - TGDispatchAfter(0.15, dispatch_get_main_queue(), ^{ - [strongSelf->_previewView endTransitionAnimated:true]; - strongSelf->_crossfadingForZoom = false; - }); - }); - }]; - }; - }; +// _camera.captureSession.crossfadeNeeded = ^{ +// __strong TGCameraController *strongSelf = weakSelf; +// if (strongSelf != nil) +// { +// if (strongSelf->_crossfadingForZoom) { +// return; +// } +// strongSelf->_crossfadingForZoom = true; +// +// [strongSelf->_camera captureNextFrameCompletion:^(UIImage *image) +// { +// TGDispatchOnMainThread(^ +// { +// [strongSelf->_previewView beginTransitionWithSnapshotImage:image animated:false]; +// +// TGDispatchAfter(0.15, dispatch_get_main_queue(), ^{ +// [strongSelf->_previewView endTransitionAnimated:true]; +// strongSelf->_crossfadingForZoom = false; +// }); +// }); +// }]; +// }; +// }; } #pragma mark - View Life Cycle @@ -2666,7 +2666,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus case UIGestureRecognizerStateChanged: { CGFloat delta = (gestureRecognizer.scale - 1.0f) * 1.25; - if (_camera.zoomLevel > 2.0) { + if (_camera.zoomLevel > _camera.secondMarkZoomValue) { delta *= 2.0; } CGFloat value = MAX(_camera.minZoomLevel, MIN(_camera.maxZoomLevel, _camera.zoomLevel + delta)); diff --git a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m index a8741b4b1d..d040959263 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainPhoneView.m @@ -101,7 +101,7 @@ @synthesize cancelPressed; @synthesize actionHandle = _actionHandle; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera { self = [super initWithFrame:frame]; if (self != nil) @@ -223,7 +223,7 @@ _topPanelBackgroundView.backgroundColor = [TGCameraInterfaceAssets transparentPanelBackgroundColor]; [_topPanelView addSubview:_topPanelBackgroundView]; - _zoomModeView = [[TGCameraZoomModeView alloc] initWithFrame:CGRectMake(floor((frame.size.width - 129.0) / 2.0), frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18 - 43, 129, 43) hasUltrawideCamera:hasUltrawideCamera hasTelephotoCamera:hasTelephotoCamera minZoomLevel:hasUltrawideCamera ? 0.5 : 1.0 maxZoomLevel:8.0]; + _zoomModeView = [[TGCameraZoomModeView alloc] initWithFrame:CGRectMake(floor((frame.size.width - 172.0) / 2.0), frame.size.height - _bottomPanelHeight - _bottomPanelOffset - 18 - 43, 172, 43) hasUltrawideCamera:hasUltrawideCamera hasTelephotoCamera:hasTelephotoCamera hasCenterRightZoom:true minZoomLevel:hasUltrawideCamera ? 0.5 : 1.0 maxZoomLevel:camera.maxMarkZoomValue secondMarkZoomValue:camera.secondMarkZoomValue]; _zoomModeView.zoomChanged = ^(CGFloat zoomLevel, bool done, bool animated) { __strong TGCameraMainPhoneView *strongSelf = weakSelf; if (strongSelf == nil) @@ -642,7 +642,7 @@ [UIView animateWithDuration:0.2 delay:0.0 options:7 << 16 animations:^{ CGFloat offset = hidden ? 19 : 18 + 43; - _zoomModeView.frame = CGRectMake(floor((self.bounds.size.width - 129.0) / 2.0), self.bounds.size.height - _bottomPanelHeight - _bottomPanelOffset - offset, 129, 43); + _zoomModeView.frame = CGRectMake(floor((self.bounds.size.width - 172.0) / 2.0), self.bounds.size.height - _bottomPanelHeight - _bottomPanelOffset - offset, 172, 43); } completion:nil]; [UIView animateWithDuration:0.25 animations:^ @@ -688,7 +688,7 @@ _topFlipButton.alpha = alpha; CGFloat offset = hidden ? 19 : 18 + 43; - _zoomModeView.frame = CGRectMake(floor((self.bounds.size.width - 129.0) / 2.0), self.bounds.size.height - _bottomPanelHeight - _bottomPanelOffset - offset, 129, 43); + _zoomModeView.frame = CGRectMake(floor((self.bounds.size.width - 172.0) / 2.0), self.bounds.size.height - _bottomPanelHeight - _bottomPanelOffset - offset, 172, 43); if (hasDoneButton) { diff --git a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m index 632527b333..c4ce724045 100644 --- a/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m +++ b/submodules/LegacyComponents/Sources/TGCameraMainTabletView.m @@ -42,7 +42,7 @@ const CGFloat TGCameraTabletPanelViewWidth = 102.0f; @synthesize shutterReleased; @synthesize cancelPressed; -- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera +- (instancetype)initWithFrame:(CGRect)frame avatar:(bool)avatar videoModeByDefault:(bool)videoModeByDefault hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera camera:(PGCamera *)camera { self = [super initWithFrame:frame]; if (self != nil) diff --git a/submodules/LegacyComponents/Sources/TGCameraZoomView.m b/submodules/LegacyComponents/Sources/TGCameraZoomView.m index f673cff9bb..ae319856ec 100644 --- a/submodules/LegacyComponents/Sources/TGCameraZoomView.m +++ b/submodules/LegacyComponents/Sources/TGCameraZoomView.m @@ -233,16 +233,19 @@ { CGFloat _minZoomLevel; CGFloat _maxZoomLevel; + CGFloat _secondMarkZoomValue; UIView *_backgroundView; bool _hasUltrawideCamera; bool _hasTelephotoCamera; + bool _hasCenterRightZoom; bool _beganFromPress; TGCameraZoomModeItemView *_leftItem; TGCameraZoomModeItemView *_centerItem; + TGCameraZoomModeItemView *_centerRightItem; TGCameraZoomModeItemView *_rightItem; bool _lockedOn; @@ -251,15 +254,17 @@ @implementation TGCameraZoomModeView -- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel +- (instancetype)initWithFrame:(CGRect)frame hasUltrawideCamera:(bool)hasUltrawideCamera hasTelephotoCamera:(bool)hasTelephotoCamera hasCenterRightZoom:(bool)hasCenterRightZoom minZoomLevel:(CGFloat)minZoomLevel maxZoomLevel:(CGFloat)maxZoomLevel secondMarkZoomValue:(CGFloat)secondMarkZoomValue { self = [super initWithFrame:frame]; if (self != nil) { _hasUltrawideCamera = hasUltrawideCamera; _hasTelephotoCamera = hasTelephotoCamera; + _hasCenterRightZoom = hasCenterRightZoom; _minZoomLevel = minZoomLevel; _maxZoomLevel = maxZoomLevel; + _secondMarkZoomValue = secondMarkZoomValue; _backgroundView = [[UIView alloc] initWithFrame:self.bounds]; _backgroundView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.15]; @@ -271,7 +276,10 @@ _centerItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(43, 0, 43, 43)]; [_centerItem addTarget:self action:@selector(centerPressed) forControlEvents:UIControlEventTouchUpInside]; - _rightItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(86, 0, 43, 43)]; + _centerRightItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(86, 0, 43, 43)]; + [_centerRightItem addTarget:self action:@selector(centerRightPressed) forControlEvents:UIControlEventTouchUpInside]; + + _rightItem = [[TGCameraZoomModeItemView alloc] initWithFrame:CGRectMake(129, 0, 43, 43)]; [_rightItem addTarget:self action:@selector(rightPressed) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:_backgroundView]; @@ -279,6 +287,10 @@ if (hasTelephotoCamera && hasUltrawideCamera) { [self addSubview:_leftItem]; [self addSubview:_rightItem]; + + if (hasCenterRightZoom) { + [self addSubview:_centerRightItem]; + } } UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)]; @@ -302,18 +314,18 @@ - (void)pressGesture:(UILongPressGestureRecognizer *)gestureRecognizer { switch (gestureRecognizer.state) { - case UIGestureRecognizerStateBegan: - _beganFromPress = true; - self.zoomChanged(_zoomLevel, false, false); - break; - case UIGestureRecognizerStateEnded: - self.zoomChanged(_zoomLevel, true, false); - break; - case UIGestureRecognizerStateCancelled: - self.zoomChanged(_zoomLevel, true, false); - break; - default: - break; + case UIGestureRecognizerStateBegan: + _beganFromPress = true; + self.zoomChanged(_zoomLevel, false, false); + break; + case UIGestureRecognizerStateEnded: + self.zoomChanged(_zoomLevel, true, false); + break; + case UIGestureRecognizerStateCancelled: + self.zoomChanged(_zoomLevel, true, false); + break; + default: + break; } } @@ -321,54 +333,54 @@ CGPoint translation = [gestureRecognizer translationInView:self]; switch (gestureRecognizer.state) { - case UIGestureRecognizerStateChanged: - { - if (_lockedOn) { - if (ABS(translation.x) > 8.0) { - _lockedOn = false; - [gestureRecognizer setTranslation:CGPointZero inView:self]; - - CGFloat delta = translation.x > 0 ? -0.06 : 0.06; - CGFloat newLevel = MAX(_minZoomLevel, MIN(_maxZoomLevel, _zoomLevel + delta)); - _zoomLevel = newLevel; - self.zoomChanged(newLevel, false, false); - return; - } else { - return; + case UIGestureRecognizerStateChanged: + { + if (_lockedOn) { + if (ABS(translation.x) > 8.0) { + _lockedOn = false; + [gestureRecognizer setTranslation:CGPointZero inView:self]; + + CGFloat delta = translation.x > 0 ? -0.06 : 0.06; + CGFloat newLevel = MAX(_minZoomLevel, MIN(_maxZoomLevel, _zoomLevel + delta)); + _zoomLevel = newLevel; + self.zoomChanged(newLevel, false, false); + return; + } else { + return; + } } - } - - CGFloat previousLevel = _zoomLevel; - - CGFloat delta = -translation.x / 60.0; - if (_zoomLevel > 2.0) { - delta *= 3.5; - } - CGFloat newLevel = MAX(_minZoomLevel, MIN(_maxZoomLevel, _zoomLevel + delta)); - - CGFloat near = floor(newLevel); - if (near <= 2.0 && ABS(newLevel - near) < 0.05 && previousLevel != near && translation.x < 15.0) { - newLevel = near; - _lockedOn = true; - [gestureRecognizer setTranslation:CGPointZero inView:self]; + CGFloat previousLevel = _zoomLevel; + + CGFloat delta = -translation.x / 60.0; + if (_zoomLevel > _secondMarkZoomValue) { + delta *= 3.5; + } + CGFloat newLevel = MAX(_minZoomLevel, MIN(_maxZoomLevel, _zoomLevel + delta)); + + CGFloat near = floor(newLevel); + if (near <= _secondMarkZoomValue && ABS(newLevel - near) < 0.05 && previousLevel != near && translation.x < 15.0) { + newLevel = near; + _lockedOn = true; + + [gestureRecognizer setTranslation:CGPointZero inView:self]; + } + + _zoomLevel = newLevel; + self.zoomChanged(newLevel, false, false); } - - _zoomLevel = newLevel; - self.zoomChanged(newLevel, false, false); - } - break; - case UIGestureRecognizerStateEnded: - case UIGestureRecognizerStateCancelled: - { - if (gestureRecognizer.view != self || !_beganFromPress) { - self.zoomChanged(_zoomLevel, true, false); + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + { + if (gestureRecognizer.view != self || !_beganFromPress) { + self.zoomChanged(_zoomLevel, true, false); + } + _beganFromPress = false; } - _beganFromPress = false; - } - break; - default: - break; + break; + default: + break; } if (!_lockedOn) { @@ -405,13 +417,20 @@ } } -- (void)rightPressed { +- (void)centerRightPressed { if (_zoomLevel != 2.0) { [self setZoomLevel:2.0 animated:true]; self.zoomChanged(2.0, true, true); } } +- (void)rightPressed { + if (_zoomLevel != _secondMarkZoomValue) { + [self setZoomLevel:_secondMarkZoomValue animated:true]; + self.zoomChanged(_secondMarkZoomValue, true, true); + } +} + - (void)setZoomLevel:(CGFloat)zoomLevel { [self setZoomLevel:zoomLevel animated:false]; } @@ -432,21 +451,44 @@ [_centerItem setValue:value selected:false animated:animated]; } [_rightItem setValue:@"2" selected:false animated:animated]; - } else if (zoomLevel < 2.0) { + } else if (zoomLevel < _secondMarkZoomValue) { [_leftItem setValue:@"0,5" selected:false animated:animated]; bool selected = _hasTelephotoCamera && _hasUltrawideCamera; if ((zoomLevel - 1.0) < 0.025) { [_centerItem setValue:@"1×" selected:true animated:animated]; + [_centerRightItem setValue:@"2" selected:false animated:animated]; } else { - NSString *value = [NSString stringWithFormat:@"%.1f×", zoomLevel]; - value = [value stringByReplacingOccurrencesOfString:@"." withString:@","]; - value = [value stringByReplacingOccurrencesOfString:@",0×" withString:@"×"]; - if ([value isEqual:@"2×"]) { - value = @"1,9×"; + if (_centerRightItem != nil) { + if (zoomLevel >= 2.0) { + [_centerItem setValue:@"1" selected:false animated:animated]; + + NSString *value = [NSString stringWithFormat:@"%.1f×", zoomLevel]; + value = [value stringByReplacingOccurrencesOfString:@"." withString:@","]; + value = [value stringByReplacingOccurrencesOfString:@",0×" withString:@"×"]; + + NSString *markValue = [NSString stringWithFormat:@"%d×", (int)_secondMarkZoomValue]; + NSString *lowerMarkValue = [NSString stringWithFormat:@"%d.9×", (int)_secondMarkZoomValue - 1]; + if ([value isEqual:markValue]) { + value = lowerMarkValue; + } + [_centerRightItem setValue:value selected:selected animated:animated]; + } else { + NSString *value = [NSString stringWithFormat:@"%.1f×", zoomLevel]; + value = [value stringByReplacingOccurrencesOfString:@"." withString:@","]; + value = [value stringByReplacingOccurrencesOfString:@",0×" withString:@"×"]; + + NSString *markValue = [NSString stringWithFormat:@"%d×", 2]; + NSString *lowerMarkValue = [NSString stringWithFormat:@"%d.9×", 2 - 1]; + if ([value isEqual:markValue]) { + value = lowerMarkValue; + } + [_centerItem setValue:value selected:selected animated:animated]; + } + } else { + } - [_centerItem setValue:value selected:selected animated:animated]; } - [_rightItem setValue:@"2" selected:false animated:animated]; + [_rightItem setValue:[NSString stringWithFormat:@"%d", (int)_secondMarkZoomValue] selected:false animated:animated]; } else { [_leftItem setValue:@"0,5" selected:false animated:animated]; @@ -455,6 +497,9 @@ if (_rightItem.superview != nil) { [_centerItem setValue:@"1" selected:false animated:animated]; + if (_centerRightItem != nil) { + [_centerRightItem setValue:@"2" selected:false animated:animated]; + } [_rightItem setValue:value selected:true animated:animated]; } else { [_centerItem setValue:value selected:true animated:animated]; @@ -498,17 +543,24 @@ if (_leftItem.superview == nil && _rightItem.superview == nil) { _backgroundView.frame = CGRectMake(43, 0, 43, 43); } else if (_leftItem.superview != nil && _rightItem.superview == nil) { - _backgroundView.frame = CGRectMake(21 + TGScreenPixel, 0, 86, 43); - _leftItem.frame = CGRectMake(21 + TGScreenPixel, 0, 43, 43); - _centerItem.frame = CGRectMake(21 + TGScreenPixel + 43, 0, 43, 43); + _backgroundView.frame = CGRectMake(42 + TGScreenPixel, 0, 86, 43); + _leftItem.frame = CGRectMake(42 + TGScreenPixel, 0, 43, 43); + _centerItem.frame = CGRectMake(42 + TGScreenPixel + 43, 0, 43, 43); } else if (_leftItem.superview == nil && _rightItem.superview != nil) { - _backgroundView.frame = CGRectMake(21 + TGScreenPixel, 0, 86, 43); - _centerItem.frame = CGRectMake(21 + TGScreenPixel, 0, 43, 43); - _rightItem.frame = CGRectMake(21 + TGScreenPixel + 43, 0, 43, 43); + _backgroundView.frame = CGRectMake(42 + TGScreenPixel, 0, 86, 43); + _centerItem.frame = CGRectMake(42 + TGScreenPixel, 0, 43, 43); + _rightItem.frame = CGRectMake(42 + TGScreenPixel + 43, 0, 43, 43); + } else if (_leftItem.superview != nil && _rightItem.superview != nil && _centerRightItem.superview == nil) { + _backgroundView.frame = CGRectMake(21 + TGScreenPixel, 0, 129, 43); + _leftItem.frame = CGRectMake(21 + TGScreenPixel, 0, 43, 43.0); + _centerItem.frame = CGRectMake(21 + TGScreenPixel + 43, 0, 43, 43.0); + _rightItem.frame = CGRectMake(21 + TGScreenPixel + 86, 0, 43, 43.0); } else { + _backgroundView.frame = CGRectMake(0, 0, 172, 43); _leftItem.frame = CGRectMake(0, 0, 43, 43.0); _centerItem.frame = CGRectMake(43, 0, 43, 43.0); - _rightItem.frame = CGRectMake(86, 0, 43, 43.0); + _centerRightItem.frame = CGRectMake(86, 0, 43, 43.0); + _rightItem.frame = CGRectMake(129, 0, 43, 43.0); } } @@ -517,6 +569,7 @@ _interfaceOrientation = interfaceOrientation; _leftItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation)); _centerItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation)); + _centerRightItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation)); _rightItem.transform = CGAffineTransformMakeRotation(TGRotationForInterfaceOrientation(interfaceOrientation)); } diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 145a408982..ddf9101ee6 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -147,13 +147,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image, _): self.file = nil self.imagePromise.set(.single(image)) - case .animatedImage: - self.file = nil - case .video: - self.file = nil - case .dualVideoReference: - self.file = nil - case .message: + case .animatedImage, .video, .dualVideoReference, .message, .link: self.file = nil } } diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 61f0ad4f4d..df4d05e948 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -18,7 +18,7 @@ public enum LocationPickerMode { } class LocationPickerInteraction { - let sendLocation: (CLLocationCoordinate2D, String?, String?) -> Void + let sendLocation: (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void let sendLiveLocation: (CLLocationCoordinate2D) -> Void let sendVenue: (TelegramMediaMap, Int64?, String?) -> Void let toggleMapModeSelection: () -> Void @@ -33,7 +33,7 @@ class LocationPickerInteraction { let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, String?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -122,16 +122,27 @@ public final class LocationPickerController: ViewController, AttachmentContainab strongSelf.controllerNode.updatePresentationData(presentationData) } }) - - let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in - return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout, liveProximityNotificationRadius: nil) - } - - self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate, name, countryCode in + + self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate, name, geoAddress in guard let strongSelf = self else { return } - strongSelf.completion(locationWithTimeout(coordinate, nil), nil, nil, name, countryCode) + strongSelf.completion( + TelegramMediaMap( + latitude: coordinate.latitude, + longitude: coordinate.longitude, + heading: nil, + accuracyRadius: nil, + venue: nil, + address: geoAddress, + liveBroadcastingTimeout: nil, + liveProximityNotificationRadius: nil + ), + nil, + nil, + name, + geoAddress?.country + ) strongSelf.dismiss() }, sendLiveLocation: { [weak self] coordinate in guard let strongSelf = self else { @@ -190,7 +201,7 @@ public final class LocationPickerController: ViewController, AttachmentContainab } let venueType = venue.venue?.type ?? "" if ["home", "work"].contains(venueType) { - completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) + completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) } else { completion(venue, queryId, resultId, venue.venue?.address, nil) } diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 069066261f..6a676d2e0a 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -35,9 +35,15 @@ private enum LocationPickerEntryId: Hashable { case attribution } +private extension MapGeoAddress { + func withUpdated(street: String?) -> MapGeoAddress { + return MapGeoAddress(country: self.country, state: self.state, city: self.city, street: street) + } +} + private enum LocationPickerEntry: Comparable, Identifiable { - case city(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?) - case location(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?, Bool) + case city(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, MapGeoAddress?) + case location(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, MapGeoAddress?, Bool) case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case header(PresentationTheme, String) case venue(PresentationTheme, TelegramMediaMap?, Int64?, String?, Int) @@ -62,14 +68,14 @@ private enum LocationPickerEntry: Comparable, Identifiable { static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { switch lhs { - case let .city(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode): - if case let .city(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode { + case let .city(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsAddress): + if case let .city(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsAddress == rhsAddress { return true } else { return false } - case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode, lhsIsTop): - if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode, rhsIsTop) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode, lhsIsTop == rhsIsTop { + case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsAddress, lhsIsTop): + if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsAddress, rhsIsTop) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsAddress == rhsAddress, lhsIsTop == rhsIsTop { return true } else { return false @@ -147,21 +153,21 @@ private enum LocationPickerEntry: Comparable, Identifiable { func item(engine: TelegramEngine, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { switch self { - case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode): + case let .city(_, title, subtitle, _, _, _, coordinate, name, address): let icon: LocationActionListItemIcon if let name { - icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: name, address: presentationData.strings.Location_TypeCity, provider: "city", id: countryCode, type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, venue: MapVenue(title: name, address: presentationData.strings.Location_TypeCity, provider: "city", id: address?.country, type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } else { icon = .location } return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: nil, action: { if let coordinate = coordinate { - interaction?.sendLocation(coordinate, name, countryCode) + interaction?.sendLocation(coordinate, name, address?.withUpdated(street: nil)) } }, highlighted: { highlighted in interaction?.updateSendActionHighlight(highlighted) }) - case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, countryCode, isTop): + case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, address, isTop): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) @@ -172,7 +178,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { if let venue = venue { interaction?.sendVenue(venue, queryId, resultId) } else if let coordinate = coordinate { - interaction?.sendLocation(coordinate, name, countryCode) + interaction?.sendLocation(coordinate, name, address) } }, highlighted: { highlighted in if isTop { @@ -260,9 +266,11 @@ struct LocationPickerState { var mapMode: LocationMapMode var displayingMapModeOptions: Bool var selectedLocation: LocationPickerLocation + var geoAddress: MapGeoAddress? var city: String? var street: String? var countryCode: String? + var state: String? var isStreet: Bool var forceSelection: Bool var searchingVenuesAround: Bool @@ -271,6 +279,7 @@ struct LocationPickerState { self.mapMode = .map self.displayingMapModeOptions = false self.selectedLocation = .none + self.geoAddress = nil self.city = nil self.street = nil self.isStreet = false @@ -474,10 +483,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM |> map { homeCoordinate, workCoordinate -> [TelegramMediaMap]? in var venues: [TelegramMediaMap] = [] if let (latitude, longitude) = homeCoordinate, let address = homeAddress { - venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } if let (latitude, longitude) = workCoordinate, let address = workAddress { - venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } return venues } @@ -592,9 +601,9 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } if source == .story { if state.street != "" { - entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, state.geoAddress, false)) } else if state.city != "" { - entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.geoAddress)) } } else { entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) @@ -641,10 +650,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } if source == .story { if state.city != "" { - entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.geoAddress)) } if state.street != "" { - entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, state.geoAddress, false)) } } else { entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).string } ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) @@ -787,10 +796,15 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } let locale = localeWithStrings(presentationData.strings) - if case let .location(coordinate, address) = state.selectedLocation, address == nil { - strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale) - |> deliverOnMainQueue).start(next: { [weak self] placemark in - if let strongSelf = self { + let enLocale = Locale(identifier: "en-US") + + let setupGeocoding: (CLLocationCoordinate2D, @escaping (MapGeoAddress?, String, String?, String?, String?, Bool) -> Void) -> Void = { coordinate, completion in + strongSelf.geocodingDisposable.set( + combineLatest( + queue: Queue.mainQueue(), + reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale), + reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: enLocale) + ).start(next: { placemark, enPlacemark in var address = placemark?.fullAddress ?? "" if address.isEmpty { address = presentationData.strings.Map_Unknown @@ -823,65 +837,43 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM if streetName == "" && cityName == "" { streetName = presentationData.strings.Location_TypeLocation } - strongSelf.updateState { state in - var state = state - state.selectedLocation = .location(coordinate, address) - state.city = cityName - state.street = streetName - state.countryCode = countryCode - state.isStreet = placemark?.street != nil - return state + + var mapGeoAddress: MapGeoAddress? + if let countryCode, let enPlacemark { + mapGeoAddress = MapGeoAddress(country: countryCode, state: enPlacemark.state, city: enPlacemark.city, street: enPlacemark.street) } + completion(mapGeoAddress, address, cityName, streetName, countryCode, placemark?.street != nil) } - })) + )) + } + + if case let .location(coordinate, address) = state.selectedLocation, address == nil { + setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in + self?.updateState { state in + var state = state + state.selectedLocation = .location(coordinate, address) + state.geoAddress = geoAddress + state.city = cityName + state.street = streetName + state.countryCode = countryCode + state.isStreet = isStreet + return state + } + }) } else { let coordinate = controller.initialLocation ?? userLocation?.coordinate if case .none = state.selectedLocation, let coordinate, state.city == nil { - strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale) - |> deliverOnMainQueue).start(next: { [weak self] placemark in - if let strongSelf = self { - var address = placemark?.fullAddress ?? "" - if address.isEmpty { - address = presentationData.strings.Map_Unknown - } - var cityName: String? - var streetName: String? - let countryCode = placemark?.countryCode - if let city = placemark?.city { - if let countryCode = placemark?.countryCode { - cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" - } else { - cityName = city - } - } else { - cityName = "" - } - if let street = placemark?.street { - if let city = placemark?.city { - streetName = "\(street), \(city)" - } else { - streetName = street - } - } else if let name = placemark?.name { - streetName = name - } else if let country = placemark?.country, cityName == "" { - streetName = country - } else { - streetName = "" - } - if streetName == "" && cityName == "" { - streetName = presentationData.strings.Location_TypeLocation - } - strongSelf.updateState { state in - var state = state - state.city = cityName - state.street = streetName - state.countryCode = countryCode - state.isStreet = placemark?.street != nil - return state - } + setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in + self?.updateState { state in + var state = state + state.geoAddress = geoAddress + state.city = cityName + state.street = streetName + state.countryCode = countryCode + state.isStreet = isStreet + return state } - })) + }) } else { strongSelf.geocodingDisposable.set(nil) } diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index 1ae77009a7..38f7848b76 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -194,7 +194,7 @@ final class LocationSearchContainerNode: ASDisplayNode { guard let placemarkLocation = placemark.location else { continue } - let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation))) diff --git a/submodules/LocationUI/Sources/LocationUtils.swift b/submodules/LocationUI/Sources/LocationUtils.swift index 3abf13348b..1875e7eb55 100644 --- a/submodules/LocationUI/Sources/LocationUtils.swift +++ b/submodules/LocationUI/Sources/LocationUtils.swift @@ -8,7 +8,7 @@ import AccountContext extension TelegramMediaMap { convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil, proximityNotificationRadius: Int32? = nil) { - self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: proximityNotificationRadius) + self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: proximityNotificationRadius) } var coordinate: CLLocationCoordinate2D { diff --git a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift index 6059880d4b..783c011b87 100644 --- a/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift +++ b/submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift @@ -1519,8 +1519,8 @@ public final class PeerInfoAvatarListContainerNode: ASDisplayNode { component: AnyComponent(StorySetIndicatorComponent( context: self.context, strings: self.context.sharedContext.currentPresentationData.with({ $0 }).strings, - peer: storyParams.peer, - items: storyParams.items, + items: storyParams.items.map { StorySetIndicatorComponent.Item(storyItem: $0, peer: storyParams.peer) }, + displayAvatars: true, hasUnseen: storyParams.hasUnseen, hasUnseenPrivate: storyParams.hasUnseenPrivate, totalCount: storyParams.count, diff --git a/submodules/ShareItems/Sources/ShareItems.swift b/submodules/ShareItems/Sources/ShareItems.swift index 4742e79fad..3ff8e54ca6 100644 --- a/submodules/ShareItems/Sources/ShareItems.swift +++ b/submodules/ShareItems/Sources/ShareItems.swift @@ -316,9 +316,9 @@ private func preparedShareItem(postbox: Postbox, network: Network, to peerId: Pe let disposable = TGShareLocationSignals.locationMessageContent(for: url).start(next: { value in if let value = value as? TGShareLocationResult { if let title = value.title { - subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) + subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) } else { - subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) + subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)))))) } subscriber.putCompletion() } else if let value = value as? String { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 625ad769da..7e46f31cc7 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -290,6 +290,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1107729093] = { return Api.Game.parse_game($0) } dict[-1297942941] = { return Api.GeoPoint.parse_geoPoint($0) } dict[286776671] = { return Api.GeoPoint.parse_geoPointEmpty($0) } + dict[-565420653] = { return Api.GeoPointAddress.parse_geoPointAddress($0) } dict[1934380235] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) } dict[-711498484] = { return Api.GroupCall.parse_groupCall($0) } dict[2004925620] = { return Api.GroupCall.parse_groupCallDiscarded($0) } @@ -513,7 +514,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[577893055] = { return Api.MediaArea.parse_inputMediaAreaChannelPost($0) } dict[-1300094593] = { return Api.MediaArea.parse_inputMediaAreaVenue($0) } dict[1996756655] = { return Api.MediaArea.parse_mediaAreaChannelPost($0) } - dict[-544523486] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } + dict[-891992787] = { return Api.MediaArea.parse_mediaAreaGeoPoint($0) } dict[340088945] = { return Api.MediaArea.parse_mediaAreaSuggestedReaction($0) } dict[926421125] = { return Api.MediaArea.parse_mediaAreaUrl($0) } dict[-1098720356] = { return Api.MediaArea.parse_mediaAreaVenue($0) } @@ -1383,7 +1384,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") return nil } } @@ -1627,6 +1628,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.GeoPoint: _1.serialize(buffer, boxed) + case let _1 as Api.GeoPointAddress: + _1.serialize(buffer, boxed) case let _1 as Api.GlobalPrivacySettings: _1.serialize(buffer, boxed) case let _1 as Api.GroupCall: diff --git a/submodules/TelegramApi/Sources/Api14.swift b/submodules/TelegramApi/Sources/Api14.swift index 857e267a49..98bc7837e6 100644 --- a/submodules/TelegramApi/Sources/Api14.swift +++ b/submodules/TelegramApi/Sources/Api14.swift @@ -51,7 +51,7 @@ public extension Api { case inputMediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channel: Api.InputChannel, msgId: Int32) case inputMediaAreaVenue(coordinates: Api.MediaAreaCoordinates, queryId: Int64, resultId: String) case mediaAreaChannelPost(coordinates: Api.MediaAreaCoordinates, channelId: Int64, msgId: Int32) - case mediaAreaGeoPoint(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint) + case mediaAreaGeoPoint(flags: Int32, coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, address: Api.GeoPointAddress?) case mediaAreaSuggestedReaction(flags: Int32, coordinates: Api.MediaAreaCoordinates, reaction: Api.Reaction) case mediaAreaUrl(coordinates: Api.MediaAreaCoordinates, url: String) case mediaAreaVenue(coordinates: Api.MediaAreaCoordinates, geo: Api.GeoPoint, title: String, address: String, provider: String, venueId: String, venueType: String) @@ -82,12 +82,14 @@ public extension Api { serializeInt64(channelId, buffer: buffer, boxed: false) serializeInt32(msgId, buffer: buffer, boxed: false) break - case .mediaAreaGeoPoint(let coordinates, let geo): + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): if boxed { - buffer.appendInt32(-544523486) + buffer.appendInt32(-891992787) } + serializeInt32(flags, buffer: buffer, boxed: false) coordinates.serialize(buffer, true) geo.serialize(buffer, true) + if Int(flags) & Int(1 << 0) != 0 {address!.serialize(buffer, true)} break case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): if boxed { @@ -127,8 +129,8 @@ public extension Api { return ("inputMediaAreaVenue", [("coordinates", coordinates as Any), ("queryId", queryId as Any), ("resultId", resultId as Any)]) case .mediaAreaChannelPost(let coordinates, let channelId, let msgId): return ("mediaAreaChannelPost", [("coordinates", coordinates as Any), ("channelId", channelId as Any), ("msgId", msgId as Any)]) - case .mediaAreaGeoPoint(let coordinates, let geo): - return ("mediaAreaGeoPoint", [("coordinates", coordinates as Any), ("geo", geo as Any)]) + case .mediaAreaGeoPoint(let flags, let coordinates, let geo, let address): + return ("mediaAreaGeoPoint", [("flags", flags as Any), ("coordinates", coordinates as Any), ("geo", geo as Any), ("address", address as Any)]) case .mediaAreaSuggestedReaction(let flags, let coordinates, let reaction): return ("mediaAreaSuggestedReaction", [("flags", flags as Any), ("coordinates", coordinates as Any), ("reaction", reaction as Any)]) case .mediaAreaUrl(let coordinates, let url): @@ -198,18 +200,26 @@ public extension Api { } } public static func parse_mediaAreaGeoPoint(_ reader: BufferReader) -> MediaArea? { - var _1: Api.MediaAreaCoordinates? + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.MediaAreaCoordinates? if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates + _2 = Api.parse(reader, signature: signature) as? Api.MediaAreaCoordinates } - var _2: Api.GeoPoint? + var _3: Api.GeoPoint? if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.GeoPoint + _3 = Api.parse(reader, signature: signature) as? Api.GeoPoint } + var _4: Api.GeoPointAddress? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.GeoPointAddress + } } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MediaArea.mediaAreaGeoPoint(coordinates: _1!, geo: _2!) + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MediaArea.mediaAreaGeoPoint(flags: _1!, coordinates: _2!, geo: _3!, address: _4) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 85128fcdfd..4b516b88f5 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -10394,16 +10394,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func searchPosts(flags: Int32, hashtag: String?, venueProvider: String?, venueId: String?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1391183841) + buffer.appendInt32(1827279210) serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeString(hashtag!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(venueProvider!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(venueId!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {area!.serialize(buffer, true)} serializeString(offset, buffer: buffer, boxed: false) serializeInt32(limit, buffer: buffer, boxed: false) - return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("venueProvider", String(describing: venueProvider)), ("venueId", String(describing: venueId)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in + return (FunctionDescription(name: "stories.searchPosts", parameters: [("flags", String(describing: flags)), ("hashtag", String(describing: hashtag)), ("area", String(describing: area)), ("offset", String(describing: offset)), ("limit", String(describing: limit))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.FoundStories? in let reader = BufferReader(buffer) var result: Api.stories.FoundStories? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api7.swift b/submodules/TelegramApi/Sources/Api7.swift index dacc9ebe84..ac4e62f39d 100644 --- a/submodules/TelegramApi/Sources/Api7.swift +++ b/submodules/TelegramApi/Sources/Api7.swift @@ -648,6 +648,58 @@ public extension Api { } } +public extension Api { + enum GeoPointAddress: TypeConstructorDescription { + case geoPointAddress(flags: Int32, countryIso2: String, state: String?, city: String?, street: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + if boxed { + buffer.appendInt32(-565420653) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(countryIso2, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(state!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(city!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeString(street!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .geoPointAddress(let flags, let countryIso2, let state, let city, let street): + return ("geoPointAddress", [("flags", flags as Any), ("countryIso2", countryIso2 as Any), ("state", state as Any), ("city", city as Any), ("street", street as Any)]) + } + } + + public static func parse_geoPointAddress(_ reader: BufferReader) -> GeoPointAddress? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: String? + if Int(_1!) & Int(1 << 0) != 0 {_3 = parseString(reader) } + var _4: String? + if Int(_1!) & Int(1 << 1) != 0 {_4 = parseString(reader) } + var _5: String? + if Int(_1!) & Int(1 << 2) != 0 {_5 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 1) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 2) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.GeoPointAddress.geoPointAddress(flags: _1!, countryIso2: _2!, state: _3, city: _4, street: _5) + } + else { + return nil + } + } + + } +} public extension Api { enum GlobalPrivacySettings: TypeConstructorDescription { case globalPrivacySettings(flags: Int32) @@ -1262,53 +1314,3 @@ public extension Api { } } -public extension Api { - enum InputAppEvent: TypeConstructorDescription { - case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - if boxed { - buffer.appendInt32(488313413) - } - serializeDouble(time, buffer: buffer, boxed: false) - serializeString(type, buffer: buffer, boxed: false) - serializeInt64(peer, buffer: buffer, boxed: false) - data.serialize(buffer, true) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputAppEvent(let time, let type, let peer, let data): - return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) - } - } - - public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { - var _1: Double? - _1 = reader.readDouble() - var _2: String? - _2 = parseString(reader) - var _3: Int64? - _3 = reader.readInt64() - var _4: Api.JSONValue? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.JSONValue - } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api8.swift b/submodules/TelegramApi/Sources/Api8.swift index e936ab4fbc..711c158ee0 100644 --- a/submodules/TelegramApi/Sources/Api8.swift +++ b/submodules/TelegramApi/Sources/Api8.swift @@ -1,3 +1,53 @@ +public extension Api { + enum InputAppEvent: TypeConstructorDescription { + case inputAppEvent(time: Double, type: String, peer: Int64, data: Api.JSONValue) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + if boxed { + buffer.appendInt32(488313413) + } + serializeDouble(time, buffer: buffer, boxed: false) + serializeString(type, buffer: buffer, boxed: false) + serializeInt64(peer, buffer: buffer, boxed: false) + data.serialize(buffer, true) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputAppEvent(let time, let type, let peer, let data): + return ("inputAppEvent", [("time", time as Any), ("type", type as Any), ("peer", peer as Any), ("data", data as Any)]) + } + } + + public static func parse_inputAppEvent(_ reader: BufferReader) -> InputAppEvent? { + var _1: Double? + _1 = reader.readDouble() + var _2: String? + _2 = parseString(reader) + var _3: Int64? + _3 = reader.readInt64() + var _4: Api.JSONValue? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.JSONValue + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.InputAppEvent.inputAppEvent(time: _1!, type: _2!, peer: _3!, data: _4!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum InputBotApp: TypeConstructorDescription { case inputBotAppID(id: Int64, accessHash: Int64) @@ -1262,43 +1312,3 @@ public extension Api { } } -public extension Api { - enum InputClientProxy: TypeConstructorDescription { - case inputClientProxy(address: String, port: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .inputClientProxy(let address, let port): - if boxed { - buffer.appendInt32(1968737087) - } - serializeString(address, buffer: buffer, boxed: false) - serializeInt32(port, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .inputClientProxy(let address, let port): - return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) - } - } - - public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { - var _1: String? - _1 = parseString(reader) - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api9.swift b/submodules/TelegramApi/Sources/Api9.swift index 4634979e31..32c3e40b12 100644 --- a/submodules/TelegramApi/Sources/Api9.swift +++ b/submodules/TelegramApi/Sources/Api9.swift @@ -1,3 +1,43 @@ +public extension Api { + enum InputClientProxy: TypeConstructorDescription { + case inputClientProxy(address: String, port: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .inputClientProxy(let address, let port): + if boxed { + buffer.appendInt32(1968737087) + } + serializeString(address, buffer: buffer, boxed: false) + serializeInt32(port, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .inputClientProxy(let address, let port): + return ("inputClientProxy", [("address", address as Any), ("port", port as Any)]) + } + } + + public static func parse_inputClientProxy(_ reader: BufferReader) -> InputClientProxy? { + var _1: String? + _1 = parseString(reader) + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.InputClientProxy.inputClientProxy(address: _1!, port: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum InputCollectible: TypeConstructorDescription { case inputCollectiblePhone(phone: String) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index dc3d54acc7..bad242f113 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -482,7 +482,8 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return nil case .inputMediaAreaVenue: return nil - case let .mediaAreaGeoPoint(coordinates, geo): + case let .mediaAreaGeoPoint(_, coordinates, geo, address): + let _ = address let latitude: Double let longitude: Double switch geo { @@ -493,7 +494,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: nil, queryId: nil, resultId: nil)) + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: nil, address: address.flatMap(mapGeoAddressFromApiGeoPointAddress), queryId: nil, resultId: nil)) case let .mediaAreaVenue(coordinates, geo, title, address, provider, venueId, venueType): let latitude: Double let longitude: Double @@ -505,7 +506,7 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { latitude = 0.0 longitude = 0.0 } - return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), queryId: nil, resultId: nil)) + return .venue(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), venue: MediaArea.Venue(latitude: latitude, longitude: longitude, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType), address: nil, queryId: nil, resultId: nil)) case let .mediaAreaSuggestedReaction(flags, coordinates, reaction): if let reaction = MessageReaction.Reaction(apiReaction: reaction) { var parsedFlags = MediaArea.ReactionFlags() @@ -520,13 +521,13 @@ func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? { return nil } case let .mediaAreaUrl(coordinates, url): - return .url(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) + return .link(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), url: url) case let .mediaAreaChannelPost(coordinates, channelId, messageId): return .channelMessage(coordinates: coodinatesFromApiMediaAreaCoordinates(coordinates), messageId: EngineMessage.Id(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId)) } } -func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction) -> [Api.MediaArea] { +func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transaction?) -> [Api.MediaArea] { var apiMediaAreas: [Api.MediaArea] = [] for area in mediaAreas { let coordinates = area.coordinates @@ -538,7 +539,23 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac } else if let venueInfo = venue.venue { apiMediaAreas.append(.mediaAreaVenue(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), title: venueInfo.title, address: venueInfo.address ?? "", provider: venueInfo.provider ?? "", venueId: venueInfo.id ?? "", venueType: venueInfo.type ?? "")) } else { - apiMediaAreas.append(.mediaAreaGeoPoint(coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil))) + var flags: Int32 = 0 + var inputAddress: Api.GeoPointAddress? + if let address = venue.address { + var addressFlags: Int32 = 0 + if let _ = address.state { + addressFlags |= (1 << 0) + } + if let _ = address.city { + addressFlags |= (1 << 1) + } + if let _ = address.street { + addressFlags |= (1 << 2) + } + inputAddress = .geoPointAddress(flags: addressFlags, countryIso2: address.country, state: address.state, city: address.city, street: address.street) + flags |= (1 << 0) + } + apiMediaAreas.append(.mediaAreaGeoPoint(flags: flags, coordinates: inputCoordinates, geo: .geoPoint(flags: 0, long: venue.longitude, lat: venue.latitude, accessHash: 0, accuracyRadius: nil), address: inputAddress)) } case let .reaction(_, reaction, flags): var apiFlags: Int32 = 0 @@ -550,10 +567,10 @@ func apiMediaAreasFromMediaAreas(_ mediaAreas: [MediaArea], transaction: Transac } apiMediaAreas.append(.mediaAreaSuggestedReaction(flags: apiFlags, coordinates: inputCoordinates, reaction: reaction.apiReaction)) case let .channelMessage(_, messageId): - if let peer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(peer) { + if let transaction, let peer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(peer) { apiMediaAreas.append(.inputMediaAreaChannelPost(coordinates: inputCoordinates, channel: inputChannel, msgId: messageId.id)) } - case let .url(_, url): + case let .link(_, url): apiMediaAreas.append(.mediaAreaUrl(coordinates: inputCoordinates, url: url)) } } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift index f0e9de92ef..84d935b8a5 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaMap.swift @@ -10,8 +10,16 @@ func telegramMediaMapFromApiGeoPoint(_ geo: Api.GeoPoint, title: String?, addres } switch geo { case let .geoPoint(_, long, lat, _, accuracyRadius): - return TelegramMediaMap(latitude: lat, longitude: long, heading: heading, accuracyRadius: accuracyRadius.flatMap { Double($0) }, geoPlace: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + return TelegramMediaMap(latitude: lat, longitude: long, heading: heading, accuracyRadius: accuracyRadius.flatMap { Double($0) }, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) case .geoPointEmpty: - return TelegramMediaMap(latitude: 0.0, longitude: 0.0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + return TelegramMediaMap(latitude: 0.0, longitude: 0.0, heading: nil, accuracyRadius: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius) + } +} + + +func mapGeoAddressFromApiGeoPointAddress(_ geo: Api.GeoPointAddress) -> MapGeoAddress { + switch geo { + case let .geoPointAddress(_, countryIso2, state, city, street): + return MapGeoAddress(country: countryIso2, state: state, city: city, street: street) } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 3e52081bc2..68fd63150b 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1554,9 +1554,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox: switch draft { case .draftMessageEmpty: inputState = nil - case let .draftMessage(_, replyToMsgHeader, message, entities, media, date, effect): + case let .draftMessage(_, replyToMsgHeader, message, entities, media, date, _): let _ = media - let _ = effect var replySubject: EngineMessageReplySubject? if let replyToMsgHeader = replyToMsgHeader { switch replyToMsgHeader { diff --git a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift index 02ee72c513..24085d5110 100644 --- a/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift +++ b/submodules/TelegramCore/Sources/State/ProcessSecretChatIncomingDecryptedOperations.swift @@ -866,11 +866,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } @@ -1085,11 +1085,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } @@ -1364,11 +1364,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } @@ -1565,11 +1565,11 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32 case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): - parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift index 2039d62d8b..8592f4c664 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaMap.swift @@ -2,33 +2,28 @@ import Postbox public let liveLocationIndefinitePeriod: Int32 = 0x7fffffff -public final class NamedGeoPlace: PostboxCoding, Equatable { - public let country: String? +public final class MapGeoAddress: PostboxCoding, Equatable { + public let country: String public let state: String? public let city: String? - public let district: String? public let street: String? - public init(country: String?, state: String?, city: String?, district: String?, street: String?) { + public init(country: String, state: String?, city: String?, street: String?) { self.country = country self.state = state self.city = city - self.district = district self.street = street } public init(decoder: PostboxDecoder) { - self.country = decoder.decodeOptionalStringForKey("gp_co") + self.country = decoder.decodeStringForKey("gp_co", orElse: "") self.state = decoder.decodeOptionalStringForKey("gp_sta") self.city = decoder.decodeOptionalStringForKey("gp_ci") - self.district = decoder.decodeOptionalStringForKey("gp_dis") self.street = decoder.decodeOptionalStringForKey("gp_str") } public func encode(_ encoder: PostboxEncoder) { - if let country = self.country { - encoder.encodeString(country, forKey: "gp_co") - } + encoder.encodeString(country, forKey: "gp_co") if let state = self.state { encoder.encodeString(state, forKey: "gp_sta") @@ -38,16 +33,12 @@ public final class NamedGeoPlace: PostboxCoding, Equatable { encoder.encodeString(city, forKey: "gp_ci") } - if let district = self.district { - encoder.encodeString(district, forKey: "gp_dis") - } - if let street = self.street { encoder.encodeString(street, forKey: "gp_str") } } - public static func ==(lhs: NamedGeoPlace, rhs: NamedGeoPlace) -> Bool { + public static func ==(lhs: MapGeoAddress, rhs: MapGeoAddress) -> Bool { if lhs.country != rhs.country { return false } @@ -57,9 +48,6 @@ public final class NamedGeoPlace: PostboxCoding, Equatable { if lhs.city != rhs.city { return false } - if lhs.district != rhs.district { - return false - } if lhs.street != rhs.street { return false } @@ -137,21 +125,21 @@ public final class TelegramMediaMap: Media, Equatable { public let longitude: Double public let heading: Int32? public let accuracyRadius: Double? - public let geoPlace: NamedGeoPlace? public let venue: MapVenue? + public let address: MapGeoAddress? public let liveBroadcastingTimeout: Int32? public let liveProximityNotificationRadius: Int32? public let id: MediaId? = nil public let peerIds: [PeerId] = [] - public init(latitude: Double, longitude: Double, heading: Int32?, accuracyRadius: Double?, geoPlace: NamedGeoPlace?, venue: MapVenue?, liveBroadcastingTimeout: Int32?, liveProximityNotificationRadius: Int32?) { + public init(latitude: Double, longitude: Double, heading: Int32?, accuracyRadius: Double?, venue: MapVenue?, address: MapGeoAddress? = nil, liveBroadcastingTimeout: Int32? = nil, liveProximityNotificationRadius: Int32? = nil) { self.latitude = latitude self.longitude = longitude self.heading = heading self.accuracyRadius = accuracyRadius - self.geoPlace = geoPlace self.venue = venue + self.address = address self.liveBroadcastingTimeout = liveBroadcastingTimeout self.liveProximityNotificationRadius = liveProximityNotificationRadius } @@ -161,8 +149,8 @@ public final class TelegramMediaMap: Media, Equatable { self.longitude = decoder.decodeDoubleForKey("lo", orElse: 0.0) self.heading = decoder.decodeOptionalInt32ForKey("hdg") self.accuracyRadius = decoder.decodeOptionalDoubleForKey("acc") - self.geoPlace = decoder.decodeObjectForKey("gp", decoder: { NamedGeoPlace(decoder: $0) }) as? NamedGeoPlace self.venue = decoder.decodeObjectForKey("ve", decoder: { MapVenue(decoder: $0) }) as? MapVenue + self.address = decoder.decodeObjectForKey("adr", decoder: { MapGeoAddress(decoder: $0) }) as? MapGeoAddress self.liveBroadcastingTimeout = decoder.decodeOptionalInt32ForKey("bt") self.liveProximityNotificationRadius = decoder.decodeOptionalInt32ForKey("pnr") } @@ -180,16 +168,16 @@ public final class TelegramMediaMap: Media, Equatable { } else { encoder.encodeNil(forKey: "acc") } - if let geoPlace = self.geoPlace { - encoder.encodeObject(geoPlace, forKey: "gp") - } else { - encoder.encodeNil(forKey: "gp") - } if let venue = self.venue { encoder.encodeObject(venue, forKey: "ve") } else { encoder.encodeNil(forKey: "ve") } + if let address = self.address { + encoder.encodeObject(address, forKey: "adr") + } else { + encoder.encodeNil(forKey: "adr") + } if let liveBroadcastingTimeout = self.liveBroadcastingTimeout { encoder.encodeInt32(liveBroadcastingTimeout, forKey: "bt") } else { @@ -217,9 +205,6 @@ public final class TelegramMediaMap: Media, Equatable { if self.accuracyRadius != other.accuracyRadius { return false } - if self.geoPlace != other.geoPlace { - return false - } if self.venue != other.venue { return false } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift index d388e490a6..854b1f65e6 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/MediaArea.swift @@ -64,6 +64,7 @@ public enum MediaArea: Codable, Equatable { case latitude case longitude case venue + case address case queryId case resultId } @@ -71,6 +72,7 @@ public enum MediaArea: Codable, Equatable { public let latitude: Double public let longitude: Double public let venue: MapVenue? + public let address: MapGeoAddress? public let queryId: Int64? public let resultId: String? @@ -78,12 +80,14 @@ public enum MediaArea: Codable, Equatable { latitude: Double, longitude: Double, venue: MapVenue?, + address: MapGeoAddress?, queryId: Int64?, resultId: String? ) { self.latitude = latitude self.longitude = longitude self.venue = venue + self.address = address self.queryId = queryId self.resultId = resultId } @@ -100,6 +104,12 @@ public enum MediaArea: Codable, Equatable { self.venue = nil } + if let addressData = try container.decodeIfPresent(Data.self, forKey: .address) { + self.address = PostboxDecoder(buffer: MemoryBuffer(data: addressData)).decodeRootObject() as? MapGeoAddress + } else { + self.address = nil + } + self.queryId = try container.decodeIfPresent(Int64.self, forKey: .queryId) self.resultId = try container.decodeIfPresent(String.self, forKey: .resultId) } @@ -117,6 +127,13 @@ public enum MediaArea: Codable, Equatable { try container.encode(venueData, forKey: .venue) } + if let address = self.address { + let encoder = PostboxEncoder() + encoder.encodeRootObject(address) + let addressData = encoder.makeData() + try container.encode(addressData, forKey: .address) + } + try container.encodeIfPresent(self.queryId, forKey: .queryId) try container.encodeIfPresent(self.resultId, forKey: .resultId) } @@ -125,7 +142,8 @@ public enum MediaArea: Codable, Equatable { case venue(coordinates: Coordinates, venue: Venue) case reaction(coordinates: Coordinates, reaction: MessageReaction.Reaction, flags: ReactionFlags) case channelMessage(coordinates: Coordinates, messageId: EngineMessage.Id) - case url(coordinates: Coordinates, url: String) + case link(coordinates: Coordinates, url: String) + public struct ReactionFlags: OptionSet { public var rawValue: Int32 @@ -146,7 +164,7 @@ public enum MediaArea: Codable, Equatable { case venue case reaction case channelMessage - case url + case link } public enum DecodingError: Error { @@ -173,10 +191,10 @@ public enum MediaArea: Codable, Equatable { let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let messageId = try container.decode(MessageId.self, forKey: .value) self = .channelMessage(coordinates: coordinates, messageId: messageId) - case .url: + case .link: let coordinates = try container.decode(MediaArea.Coordinates.self, forKey: .coordinates) let url = try container.decode(String.self, forKey: .value) - self = .url(coordinates: coordinates, url: url) + self = .link(coordinates: coordinates, url: url) } } @@ -197,8 +215,8 @@ public enum MediaArea: Codable, Equatable { try container.encode(MediaAreaType.channelMessage.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(messageId, forKey: .value) - case let .url(coordinates, url): - try container.encode(MediaAreaType.url.rawValue, forKey: .type) + case let .link(coordinates, url): + try container.encode(MediaAreaType.link.rawValue, forKey: .type) try container.encode(coordinates, forKey: .coordinates) try container.encode(url, forKey: .value) } @@ -214,7 +232,7 @@ public extension MediaArea { return coordinates case let .channelMessage(coordinates, _): return coordinates - case let .url(coordinates, _): + case let .link(coordinates, _): return coordinates } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 035bd4af42..7dc1bc5711 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -1260,10 +1260,9 @@ public final class PeerStoryListContext: StoryListContext { } public final class SearchStoryListContext: StoryListContext { - public enum Source { case hashtag(String) - case venue(provider: String, id: String) + case mediaArea(MediaArea) } private final class Impl { @@ -1327,8 +1326,7 @@ public final class SearchStoryListContext: StoryListContext { let accountPeerId = account.peerId var searchHashtag: String? = nil - var venueProvider: String? = nil - var venueId: String? = nil + var area: Api.MediaArea? = nil var flags: Int32 = 0 switch source { @@ -1339,13 +1337,12 @@ public final class SearchStoryListContext: StoryListContext { searchHashtag = query } flags |= (1 << 0) - case .venue(let provider, let id): - venueProvider = provider - venueId = id + case let .mediaArea(mediaArea): + area = apiMediaAreasFromMediaAreas([mediaArea], transaction: nil).first flags |= (1 << 1) } - self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, venueProvider: venueProvider, venueId: venueId, offset: "", limit: Int32(limit))) + self.requestDisposable = (account.network.request(Api.functions.stories.searchPosts(flags: flags, hashtag: searchHashtag, area: area, offset: "", limit: Int32(limit))) |> map { result -> Api.stories.FoundStories? in return result } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index c09e31c721..d129b4709c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -396,7 +396,7 @@ public extension TelegramEngine { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for i in 0 ..< updatedMedia.count { if let media = updatedMedia[i] as? TelegramMediaMap, let _ = media.liveBroadcastingTimeout { - updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, heading: media.heading, accuracyRadius: media.accuracyRadius, geoPlace: media.geoPlace, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1), liveProximityNotificationRadius: nil) + updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, heading: media.heading, accuracyRadius: media.accuracyRadius, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1), liveProximityNotificationRadius: nil) } } return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: updatedMedia)) diff --git a/submodules/TelegramPresentationData/Sources/PresentationData.swift b/submodules/TelegramPresentationData/Sources/PresentationData.swift index 11f2f15c3b..8ad523ddd1 100644 --- a/submodules/TelegramPresentationData/Sources/PresentationData.swift +++ b/submodules/TelegramPresentationData/Sources/PresentationData.swift @@ -69,11 +69,13 @@ public struct PresentationChatBubbleCorners: Equatable, Hashable { public var mainRadius: CGFloat public var auxiliaryRadius: CGFloat public var mergeBubbleCorners: Bool + public var hasTails: Bool - public init(mainRadius: CGFloat, auxiliaryRadius: CGFloat, mergeBubbleCorners: Bool) { + public init(mainRadius: CGFloat, auxiliaryRadius: CGFloat, mergeBubbleCorners: Bool, hasTails: Bool = true) { self.mainRadius = mainRadius self.auxiliaryRadius = auxiliaryRadius self.mergeBubbleCorners = mergeBubbleCorners + self.hasTails = hasTails } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 70ae45f085..6a49e26224 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -133,6 +133,24 @@ public struct PresentationResourcesSettings { drawBorder(context: context, rect: bounds) }) + + public static let bot = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let path = UIBezierPath(roundedRect: bounds, cornerRadius: 7.0) + context.addPath(path.cgPath) + context.clip() + + context.setFillColor(UIColor(rgb: 0x007aff).cgColor) + context.fill(bounds) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Bot"), color: UIColor(rgb: 0xffffff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - image.size.width) / 2.0), y: floorToScreenPixels((bounds.height - image.size.height) / 2.0)), size: image.size)) + } + + drawBorder(context: context, rect: bounds) + }) public static let passport = renderIcon(name: "Settings/Menu/Passport") public static let watch = renderIcon(name: "Settings/Menu/Watch") diff --git a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift index ecfe9f9f54..6aecfd490f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift +++ b/submodules/TelegramUI/Components/Chat/ChatBotInfoItem/Sources/ChatBotInfoItem.swift @@ -477,22 +477,25 @@ public final class ChatBotInfoItemNode: ListViewItemNode { case .longTap, .doubleTap: if let item = self.item, self.backgroundNode.frame.contains(location) { let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false) - switch tapAction.content { - case .none, .ignore: - break - case let .url(url): - item.controllerInteraction.longTap(.url(url.url), nil) - case let .peerMention(peerId, mention, _): - item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) - case let .textMention(name): - item.controllerInteraction.longTap(.mention(name), nil) - case let .botCommand(command): - item.controllerInteraction.longTap(.command(command), nil) - case let .hashtag(_, hashtag): - item.controllerInteraction.longTap(.hashtag(hashtag), nil) - default: - break - } +//TODO:do + let _ = item + let _ = tapAction +// switch tapAction.content { +// case .none, .ignore: +// break +// case let .url(url): +// item.controllerInteraction.longTap(.url(url.url), nil) +// case let .peerMention(peerId, mention, _): +// item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) +// case let .textMention(name): +// item.controllerInteraction.longTap(.mention(name), nil) +// case let .botCommand(command): +// item.controllerInteraction.longTap(.command(command), nil) +// case let .hashtag(_, hashtag): +// item.controllerInteraction.longTap(.hashtag(hashtag), nil) +// default: +// break +// } } default: break diff --git a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift index 32331874e9..e45a3a023c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatHistorySearchContainerNode/Sources/ChatHistorySearchContainerNode.swift @@ -27,7 +27,7 @@ private extension ListMessageItemInteraction { }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - controllerInteraction.longTap(action, message) + controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return controllerInteraction.hiddenMedia }) diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index 13552cf68f..aeac7b18fb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -892,11 +892,19 @@ public final class ChatInlineSearchResultsListComponent: Component { environment: {}, containerSize: availableSize ) + + let placeholderText: String + if query.hasPrefix("$") { + placeholderText = component.presentation.strings.HashtagSearch_NoResultsQueryCashtagDescription(query).string + } else { + placeholderText = component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string + } + let emptyResultsTextSize = self.emptyResultsText.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), + text: .plain(NSAttributedString(string: placeholderText, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 ) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index a7fff6e9af..ea246bef87 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -702,6 +702,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? if case .customChatContents = associatedData.subject { + } else if !presentationData.chatBubbleCorners.hasTails { } else if case let .linear(_, bottom) = position { switch bottom { case .None, .Neighbour(_, .footer, _): diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 64e5e5303a..0a99147972 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -3126,7 +3126,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if !incoming { backgroundType = .outgoing(mergeType) } else { - backgroundType = .incoming(mergeType) + if !item.presentationData.chatBubbleCorners.hasTails { + backgroundType = .incoming(.Extracted) + } else { + backgroundType = .incoming(mergeType) + } } let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper if item.presentationData.theme.theme.forceSync { @@ -4672,13 +4676,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } case let .phone(number): return .action(InternalBubbleTapAction.Action({ [weak self] in - guard let self, let item = self.item, let contentNode = self.contextContentNodeForPhoneNumber(number) else { + guard let self, let item = self.item, let contentNode = self.contextContentNodeForLink(number) else { return } + + item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) - self.addSubnode(contentNode) - - item.controllerInteraction.openPhoneContextMenu(ChatControllerInteraction.OpenPhone(number: number, message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) +// item.controllerInteraction.openPhoneContextMenu(ChatControllerInteraction.OpenPhone(number: number, message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) case let .peerMention(peerId, _, openProfile): return .action(InternalBubbleTapAction.Action { [weak self] in @@ -4747,8 +4751,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } case let .bankCard(number): if let item = self.item { - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.bankCard(number), item.message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(number) else { + return + } + item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.message, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) } case let .tooltip(text, node, rect): @@ -4796,7 +4803,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil case .longTap, .doubleTap, .secondaryTap: if let item = self.item, self.backgroundNode.frame.contains(location) { - let message = item.message +// let message = item.message if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) { return .action(InternalBubbleTapAction.Action {}) @@ -4836,37 +4843,50 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case let .url(url): if tapAction.hasLongTapAction { - return .action(InternalBubbleTapAction.Action({ - item.controllerInteraction.longTap(.url(url.url), message) + return .action(InternalBubbleTapAction.Action({ [weak self] in + let cleanUrl = url.url.replacingOccurrences(of: "mailto:", with: "") + guard let self, let contentNode = self.contextContentNodeForLink(cleanUrl) else { + return + } + item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: false)) } else { disableDefaultPressAnimation = true } case let .phone(number): return .action(InternalBubbleTapAction.Action({ [weak self] in - guard let self, let item = self.item, let contentNode = self.contextContentNodeForPhoneNumber(number) else { + guard let self, let contentNode = self.contextContentNodeForLink(number) else { return } - - self.addSubnode(contentNode) - - item.controllerInteraction.openPhoneContextMenu(ChatControllerInteraction.OpenPhone(number: number, message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) + item.controllerInteraction.longTap(.phone(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }, contextMenuOnLongPress: !tapAction.hasLongTapAction)) case let .peerMention(peerId, mention, _): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.peerMention(peerId, mention), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(mention) else { + return + } + item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .textMention(name): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.mention(name), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(name) else { + return + } + item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .botCommand(command): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.command(command), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(command) else { + return + } + item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case let .hashtag(_, hashtag): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.hashtag(hashtag), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(hashtag) else { + return + } + item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .instantPage: break @@ -4880,13 +4900,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI break case let .timecode(timecode, text): if let mediaMessage = mediaMessage { - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.timecode(timecode, text), mediaMessage) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(text) else { + return + } + item.controllerInteraction.longTap(.timecode(timecode, text), ChatControllerInteraction.LongTapParams(message: mediaMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) } case let .bankCard(number): - return .action(InternalBubbleTapAction.Action { - item.controllerInteraction.longTap(.bankCard(number), message) + return .action(InternalBubbleTapAction.Action { [weak self] in + guard let self, let contentNode = self.contextContentNodeForLink(number) else { + return + } + item.controllerInteraction.longTap(.bankCard(number), ChatControllerInteraction.LongTapParams(message: item.content.firstMessage, contentNode: contentNode, messageNode: self, progress: tapAction.activate?())) }) case .tooltip: break @@ -4921,7 +4947,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return nil } - private func contextContentNodeForPhoneNumber(_ number: String) -> ContextExtractedContentContainingNode? { + private func contextContentNodeForLink(_ link: String) -> ContextExtractedContentContainingNode? { guard let item = self.item else { return nil } @@ -4930,7 +4956,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI let incoming = item.content.effectivelyIncoming(item.context.account.peerId, associatedData: item.associatedData) let textNode = ImmediateTextNode() - textNode.attributedText = NSAttributedString(string: number, font: Font.regular(item.presentationData.fontSize.baseDisplaySize), textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.linkTextColor : item.presentationData.theme.theme.chat.message.outgoing.linkTextColor) + textNode.attributedText = NSAttributedString(string: link, font: Font.regular(item.presentationData.fontSize.baseDisplaySize), textColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.linkTextColor : item.presentationData.theme.theme.chat.message.outgoing.linkTextColor) let textSize = textNode.updateLayout(CGSize(width: 1000.0, height: 100.0)) let backgroundNode = ASDisplayNode() @@ -4951,6 +4977,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI containingNode.contentNode.alpha = 0.0 + self.addSubnode(containingNode) + return containingNode } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift index 1ac60e4517..6804e647b0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemView/Sources/ChatMessageItemView.swift @@ -871,7 +871,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol { if let item = self.item { switch button.action { case let .url(url): - item.controllerInteraction.longTap(.url(url), item.message) + item.controllerInteraction.longTap(.url(url), ChatControllerInteraction.LongTapParams(message: item.message)) default: break } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 524619a59f..416aac8899 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -304,6 +304,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { displayStatus = false } + } else if !item.presentationData.chatBubbleCorners.hasTails { + displayStatus = false } if displayStatus { if incoming { @@ -887,6 +889,36 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } + func makeActivate(_ urlRange: NSRange?) -> (() -> Promise?)? { + return { [weak self] in + guard let self else { + return nil + } + + let promise = Promise() + + self.linkProgressDisposable?.dispose() + + if self.linkProgressRange != nil { + self.linkProgressRange = nil + self.updateLinkProgressState() + } + + self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in + guard let self else { + return + } + let updatedRange: NSRange? = value ? urlRange : nil + if self.linkProgressRange != updatedRange { + self.linkProgressRange = updatedRange + self.updateLinkProgressState() + } + }) + + return promise + } + } + let textNodeFrame = self.textNode.textNode.frame let textLocalPoint = CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY) if let (index, attributes) = self.textNode.textNode.attributesAtPoint(textLocalPoint) { @@ -907,33 +939,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { content = .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)) } - return ChatMessageBubbleContentTapAction(content: content, activate: { [weak self] in - guard let self else { - return nil - } - - let promise = Promise() - - self.linkProgressDisposable?.dispose() - - if self.linkProgressRange != nil { - self.linkProgressRange = nil - self.updateLinkProgressState() - } - - self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in - guard let self else { - return - } - let updatedRange: NSRange? = value ? urlRange : nil - if self.linkProgressRange != updatedRange { - self.linkProgressRange = updatedRange - self.updateLinkProgressState() - } - }) - - return promise - }) + return ChatMessageBubbleContentTapAction(content: content, activate: makeActivate(urlRange)) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { @@ -942,33 +948,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { urlRange = urlRangeValue } - return ChatMessageBubbleContentTapAction(content: .textMention(peerName), activate: { [weak self] in - guard let self else { - return nil - } - - let promise = Promise() - - self.linkProgressDisposable?.dispose() - - if self.linkProgressRange != nil { - self.linkProgressRange = nil - self.updateLinkProgressState() - } - - self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in - guard let self else { - return - } - let updatedRange: NSRange? = value ? urlRange : nil - if self.linkProgressRange != updatedRange { - self.linkProgressRange = updatedRange - self.updateLinkProgressState() - } - }) - - return promise - }) + return ChatMessageBubbleContentTapAction(content: .textMention(peerName), activate: makeActivate(urlRange)) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand)) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { @@ -976,7 +956,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else if let timecode = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Timecode)] as? TelegramTimecode { return ChatMessageBubbleContentTapAction(content: .timecode(timecode.time, timecode.text)) } else if let bankCard = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BankCard)] as? String { - return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard)) + var urlRange: NSRange? + if let (_, _, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.BankCard, index: index) { + urlRange = urlRangeValue + } + return ChatMessageBubbleContentTapAction(content: .bankCard(bankCard), activate: makeActivate(urlRange)) } else if let pre = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre)] as? String { return ChatMessageBubbleContentTapAction(content: .copy(pre)) } else if let code = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Code)] as? String { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 0498b223cc..9d73bee8eb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -22,31 +22,6 @@ import ChatControllerInteraction private let titleFont: UIFont = Font.semibold(15.0) -public func defaultWebpageImageSizeIsSmall(webpage: TelegramMediaWebpageLoadedContent) -> Bool { - let type = websiteType(of: webpage.websiteName) - - let mainMedia: Media? - switch type { - case .instagram, .twitter: - mainMedia = webpage.story ?? webpage.image ?? webpage.file - default: - mainMedia = webpage.story ?? webpage.file ?? webpage.image - } - - if let image = mainMedia as? TelegramMediaImage { - if let type = webpage.type, (["photo", "video", "embed", "gif", "document", "telegram_album"] as [String]).contains(type) { - } else if let type = webpage.type, (["article"] as [String]).contains(type) { - return true - } else if let _ = largestImageRepresentation(image.representations)?.dimensions { - if webpage.instantPage == nil { - return true - } - } - } - - return false -} - public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { private var webPage: TelegramMediaWebpage? diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift index 73f547d839..0cae4ca8fc 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift @@ -363,7 +363,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return self?.getNavigationController() }, chatControllerNode: { [weak self] in return self - }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { [weak self] action, message in + }, presentGlobalOverlayController: { _, _ in }, callPeer: { _, _ in }, longTap: { [weak self] action, params in if let strongSelf = self { switch action { case let .url(url): @@ -428,6 +428,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }) ])]) strongSelf.presentController(actionSheet, .window(.root), nil) + case let .phone(number): + let _ = number + break case let .peerMention(peerId, mention): let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) var items: [ActionSheetItem] = [] @@ -525,7 +528,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { ])]) strongSelf.presentController(actionSheet, .window(.root), nil) case let .timecode(timecode, text): - guard let message = message else { + guard let message = params?.message else { return } let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) @@ -614,7 +617,6 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index ab70fa9a0d..7126d2d620 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -1278,7 +1278,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return [] }, to: &text, entities: &entities) - let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let mediaMap = TelegramMediaMap(latitude: updated.latitude, longitude: updated.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: text, attributes: [], media: [mediaMap], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) diff --git a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift index 6063bcfdeb..9e280a161b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendAudioMessageContextPreview/Sources/ChatSendAudioMessageContextPreview.swift @@ -483,7 +483,6 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 54c52fc2f4..45322ddc2f 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -150,15 +150,13 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol } } - public struct OpenPhone { - public var number: String - public var message: Message - public var contentNode: ContextExtractedContentContainingNode - public var messageNode: ASDisplayNode + public struct LongTapParams { + public var message: Message? + public var contentNode: ContextExtractedContentContainingNode? + public var messageNode: ASDisplayNode? public var progress: Promise? - public init(number: String, message: Message, contentNode: ContextExtractedContentContainingNode, messageNode: ASDisplayNode, progress: Promise? = nil) { - self.number = number + public init(message: Message?, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise? = nil) { self.message = message self.contentNode = contentNode self.messageNode = messageNode @@ -206,7 +204,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let chatControllerNode: () -> ASDisplayNode? public let presentGlobalOverlayController: (ViewController, Any?) -> Void public let callPeer: (PeerId, Bool) -> Void - public let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void + public let longTap: (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void public let openCheckoutOrReceipt: (MessageId) -> Void public let openSearch: () -> Void public let setupReply: (MessageId) -> Void @@ -260,7 +258,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol public let openRecommendedChannelContextMenu: (EnginePeer, UIView, ContextGesture?) -> Void public let openGroupBoostInfo: (EnginePeer.Id?, Int) -> Void public let openStickerEditor: () -> Void - public let openPhoneContextMenu: (OpenPhone) -> Void public let openAgeRestrictedMessageMedia: (Message, @escaping () -> Void) -> Void public let playMessageEffect: (Message) -> Void public let editMessageFactCheck: (MessageId) -> Void @@ -336,7 +333,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol chatControllerNode: @escaping () -> ASDisplayNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, - longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, + longTap: @escaping (ChatControllerInteractionLongTapAction, LongTapParams?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, @@ -390,7 +387,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol openRecommendedChannelContextMenu: @escaping (EnginePeer, UIView, ContextGesture?) -> Void, openGroupBoostInfo: @escaping (EnginePeer.Id?, Int) -> Void, openStickerEditor: @escaping () -> Void, - openPhoneContextMenu: @escaping (OpenPhone) -> Void, openAgeRestrictedMessageMedia: @escaping (Message, @escaping () -> Void) -> Void, playMessageEffect: @escaping (Message) -> Void, editMessageFactCheck: @escaping (MessageId) -> Void, @@ -499,7 +495,6 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol self.openRecommendedChannelContextMenu = openRecommendedChannelContextMenu self.openGroupBoostInfo = openGroupBoostInfo self.openStickerEditor = openStickerEditor - self.openPhoneContextMenu = openPhoneContextMenu self.openAgeRestrictedMessageMedia = openAgeRestrictedMessageMedia self.playMessageEffect = playMessageEffect self.editMessageFactCheck = editMessageFactCheck diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index aeef94f3fa..8aac683731 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -116,12 +116,18 @@ public enum CodableDrawingEntity: Equatable { latitude: entity.location.latitude, longitude: entity.location.longitude, venue: entity.location.venue, + address: entity.location.address, queryId: entity.queryId, resultId: entity.resultId ) ) case let .sticker(entity): - if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { + if case let .link(url, _, _, _, _, _, _) = entity.content { + return .link( + coordinates: coordinates, + url: url + ) + } else if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { var flags: MediaArea.ReactionFlags = [] if case .black = style { flags.insert(.isDark) @@ -135,7 +141,10 @@ public enum CodableDrawingEntity: Equatable { flags: flags ) } else if case let .message(messageIds, _, _, _, _) = entity.content, let messageId = messageIds.first { - return .channelMessage(coordinates: coordinates, messageId: messageId) + return .channelMessage( + coordinates: coordinates, + messageId: messageId + ) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index b280a5231f..cbe92efd82 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -32,12 +32,21 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case sticker case reaction(MessageReaction.Reaction, ReactionStyle) } + + public enum LinkStyle: Int32 { + case white + case black + case whiteCompact + case blackCompact + } + case file(FileMediaReference, FileType) case image(UIImage, ImageType) case animatedImage(Data, UIImage) case video(TelegramMediaFile) case dualVideoReference(Bool) case message([MessageId], CGSize, TelegramMediaFile?, CGRect?, CGFloat?) + case link(String, String, Bool, Bool?, CGSize?, CGSize, LinkStyle) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -77,6 +86,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } + case let .link(lhsUrl, lhsName, lhsPositionBelowText, lhsLargeMedia, lhsSize, lhsCompactSize, lhsStyle): + if case let .link(rhsUrl, rhsName, rhsPositionBelowText, rhsLargeMedia, rhsSize, rhsCompactSize, rhsStyle) = rhs { + return lhsUrl == rhsUrl && lhsName == rhsName && lhsPositionBelowText == rhsPositionBelowText && lhsLargeMedia == rhsLargeMedia && lhsSize == rhsSize && lhsCompactSize == rhsCompactSize && lhsStyle == rhsStyle + } else { + return false + } } } } @@ -97,6 +112,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case messageSize case messageMediaRect case messageMediaCornerRadius + case linkUrl + case linkName + case linkPositionBelowText + case linkLargeMedia + case linkSize + case linkCompactSize + case linkStyle case referenceDrawingSize case position case scale @@ -135,6 +157,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public var secondaryRenderImage: UIImage? public var overlayRenderImage: UIImage? + public var tertiaryRenderImage: UIImage? + public var quaternaryRenderImage: UIImage? + public var center: CGPoint { return self.position } @@ -160,6 +185,13 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = CGSize(width: 512.0, height: 512.0) case let .message(_, size, _, _, _): dimensions = size + case let .link(_, _, _, _, size, compactSize, style): + switch style { + case .white, .black: + dimensions = size ?? compactSize + case .whiteCompact, .blackCompact: + dimensions = compactSize + } } let boundingSize = CGSize(width: size, height: size) @@ -189,6 +221,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true case .message: return !(self.renderSubEntities ?? []).isEmpty + case .link: + return false } } @@ -200,6 +234,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true case .message: return true + case .link: + return true default: return false } @@ -228,7 +264,18 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) - if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { + if let url = try container.decodeIfPresent(String.self, forKey: .linkUrl) { + let name = try container.decode(String.self, forKey: .linkName) + let positionBelowText = try container.decode(Bool.self, forKey: .linkPositionBelowText) + let largeMedia = try container.decodeIfPresent(Bool.self, forKey: .linkLargeMedia) + let size = try container.decodeIfPresent(CGSize.self, forKey: .linkSize) + let compactSize = try container.decode(CGSize.self, forKey: .linkCompactSize) + var linkStyle: Content.LinkStyle = .white + if let style = try container.decodeIfPresent(Int32.self, forKey: .linkStyle) { + linkStyle = DrawingStickerEntity.Content.LinkStyle(rawValue: style) ?? .white + } + self.content = .link(url, name, positionBelowText, largeMedia, size, compactSize, linkStyle) + } else if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .messageFile) let mediaRect = try container.decodeIfPresent(CGRect.self, forKey: .messageMediaRect) @@ -339,6 +386,14 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encodeIfPresent(file, forKey: .messageFile) try container.encodeIfPresent(mediaRect, forKey: .messageMediaRect) try container.encodeIfPresent(mediaCornerRadius, forKey: .messageMediaCornerRadius) + case let .link(link, name, positionBelowText, largeMedia, size, compactSize, style): + try container.encode(link, forKey: .linkUrl) + try container.encode(name, forKey: .linkName) + try container.encode(positionBelowText, forKey: .linkPositionBelowText) + try container.encode(largeMedia, forKey: .linkLargeMedia) + try container.encodeIfPresent(size, forKey: .linkSize) + try container.encode(compactSize, forKey: .linkCompactSize) + try container.encode(style.rawValue, forKey: .linkStyle) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift index 085b757790..721b43da28 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/DrawingMessageRenderer.swift @@ -86,16 +86,18 @@ public final class DrawingMessageRenderer { private let messages: [Message] private let isNight: Bool private let isOverlay: Bool + private let isLink: Bool private let messagesContainerNode: ASDisplayNode private var avatarHeaderNode: ListViewItemHeaderNode? private var messageNodes: [ListViewItemNode]? - init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false) { + init(context: AccountContext, messages: [Message], isNight: Bool = false, isOverlay: Bool = false, isLink: Bool = false) { self.context = context self.messages = messages self.isNight = isNight self.isOverlay = isOverlay + self.isLink = isLink self.messagesContainerNode = ASDisplayNode() self.messagesContainerNode.clipsToBounds = true @@ -187,12 +189,26 @@ public final class DrawingMessageRenderer { let theme = presentationData.theme.withUpdated(preview: true) - let avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[self.messages.first!.author!.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) - - let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: presentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)] + let chatBubbleCorners = PresentationChatBubbleCorners( + mainRadius: presentationData.chatBubbleCorners.mainRadius, + auxiliaryRadius: presentationData.chatBubbleCorners.auxiliaryRadius, + mergeBubbleCorners: presentationData.chatBubbleCorners.mergeBubbleCorners, + hasTails: false + ) + + let avatarHeaderItem: ListViewItemHeader? + if let author = self.messages.first?.author { + avatarHeaderItem = self.context.sharedContext.makeChatMessageAvatarHeaderItem(context: self.context, timestamp: self.messages.first?.timestamp ?? 0, peer: self.messages.first!.peers[author.id]!, message: self.messages.first!, theme: theme, strings: presentationData.strings, wallpaper: presentationData.chatWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder) + } else { + avatarHeaderItem = nil + } + let items: [ListViewItem] = [self.context.sharedContext.makeChatMessagePreviewItem(context: self.context, messages: self.messages, theme: theme, strings: presentationData.strings, wallpaper: presentationData.theme.chat.defaultWallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: nil, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false)] let inset: CGFloat = 16.0 - let leftInset: CGFloat = 37.0 + var leftInset: CGFloat = 37.0 + if self.isLink { + leftInset = -6.0 + } let containerWidth = layout.size.width - inset * 2.0 let params = ListViewItemLayoutParams(width: containerWidth, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, availableHeight: layout.size.height) @@ -264,22 +280,29 @@ public final class DrawingMessageRenderer { } } - let avatarHeaderNode: ListViewItemHeaderNode - if let currentAvatarHeaderNode = self.avatarHeaderNode { - avatarHeaderNode = currentAvatarHeaderNode - avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem) - } else { - avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true) - avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) - self.messagesContainerNode.addSubnode(avatarHeaderNode) - self.avatarHeaderNode = avatarHeaderNode + if let avatarHeaderItem { + let avatarHeaderNode: ListViewItemHeaderNode + if let currentAvatarHeaderNode = self.avatarHeaderNode { + avatarHeaderNode = currentAvatarHeaderNode + avatarHeaderItem.updateNode(avatarHeaderNode, previous: nil, next: avatarHeaderItem) + } else { + avatarHeaderNode = avatarHeaderItem.node(synchronousLoad: true) + avatarHeaderNode.subnodeTransform = CATransform3DMakeScale(-1.0, 1.0, 1.0) + self.messagesContainerNode.addSubnode(avatarHeaderNode) + self.avatarHeaderNode = avatarHeaderNode + } + + avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) + avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) } - - avatarHeaderNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: layout.size.width, height: avatarHeaderItem.height)) - avatarHeaderNode.updateLayout(size: size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right) - let containerSize = CGSize(width: width + leftInset + 6.0, height: height) - self.frame = CGRect(origin: CGPoint(), size: containerSize) + var finalWidth: CGFloat = width + if leftInset > 0.0 { + finalWidth += leftInset + 6.0 + } + + let containerSize = CGSize(width: finalWidth, height: height) + self.frame = CGRect(origin: CGPoint(x: -1000.0, y: 0.0), size: containerSize) self.messagesContainerNode.frame = CGRect(origin: CGPoint(), size: containerSize) return containerSize @@ -306,13 +329,13 @@ public final class DrawingMessageRenderer { private let nightContainerNode: ContainerNode private let overlayContainerNode: ContainerNode - public init(context: AccountContext, messages: [Message], parentView: UIView) { + public init(context: AccountContext, messages: [Message], parentView: UIView, isLink: Bool = false) { self.context = context self.messages = messages - self.dayContainerNode = ContainerNode(context: context, messages: messages) - self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true) - self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true) + self.dayContainerNode = ContainerNode(context: context, messages: messages, isLink: isLink) + self.nightContainerNode = ContainerNode(context: context, messages: messages, isNight: true, isLink: isLink) + self.overlayContainerNode = ContainerNode(context: context, messages: messages, isOverlay: true, isLink: isLink) parentView.addSubview(self.dayContainerNode.view) parentView.addSubview(self.nightContainerNode.view) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 99dc63546e..1e3051dc28 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -84,7 +84,7 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti content = .video(file) case .dualVideoReference: return [] - case .message: + case .message, .link: if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { var entities: [MediaEditorComposerEntity] = [] entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift index 8fadb76212..72690c911d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorDraft.swift @@ -59,6 +59,10 @@ public struct MediaEditorResultPrivacy: Codable, Equatable { } public final class MediaEditorDraft: Codable, Equatable { + enum ReadError: Error { + case generic + } + public static func == (lhs: MediaEditorDraft, rhs: MediaEditorDraft) -> Bool { return lhs.path == rhs.path } @@ -117,7 +121,7 @@ public final class MediaEditorDraft: Codable, Equatable { if let thumbnail = UIImage(data: thumbnailData) { self.thumbnail = thumbnail } else { - fatalError() + throw ReadError.generic } self.dimensions = PixelDimensions( width: try container.decode(Int32.self, forKey: .dimensionsWidth), @@ -128,7 +132,7 @@ public final class MediaEditorDraft: Codable, Equatable { if let values = try? JSONDecoder().decode(MediaEditorValues.self, from: valuesData) { self.values = values } else { - fatalError() + throw ReadError.generic } self.caption = ((try? container.decode(ChatTextInputStateText.self, forKey: .caption)) ?? ChatTextInputStateText()).attributedText() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index d4939961f7..e7c6f106a1 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -60,6 +60,8 @@ swift_library( "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation", "//submodules/Components/HierarchyTrackingLayer", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/WebsiteType", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift new file mode 100644 index 0000000000..d5071b6052 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkOptions.swift @@ -0,0 +1,212 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import Postbox +import TelegramCore +import ContextUI +import ChatPresentationInterfaceState +import AccountContext +import TelegramPresentationData +import WebsiteType + +private enum OptionsId: Hashable { + case link +} + +func presentLinkOptionsController(context: AccountContext, selfController: CreateLinkScreen, sourceNode: ASDisplayNode, url: String, name: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) { + var sources: [ContextController.Source] = [] + + if let source = linkOptions(context: context, selfController: selfController, sourceNode: sourceNode, url: url, text: name, positionBelowText: positionBelowText, largeMedia: largeMedia, webPage: webPage, completion: completion, remove: remove) { + sources.append(source) + } + if sources.isEmpty { + return + } + + let contextController = ContextController( + presentationData: context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme), + configuration: ContextController.Configuration( + sources: sources, + initialId: AnyHashable(OptionsId.link) + ) + ) + selfController.presentInGlobalOverlay(contextController) +} + +private func linkOptions(context: AccountContext, selfController: CreateLinkScreen, sourceNode: ASDisplayNode, url: String, text: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) -> ContextController.Source? { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme) + + let initialUrlPreview = ChatPresentationInterfaceState.UrlPreview(url: url, webPage: webPage, positionBelowText: positionBelowText, largeMedia: largeMedia) + let urlPreview = ValuePromise(initialUrlPreview) + + let linkOptions = urlPreview.get() + |> deliverOnMainQueue + |> map { urlPreview -> ChatControllerSubject.LinkOptions in + var webpageHasLargeMedia = false + if case let .Loaded(content) = webPage.content { + if let isMediaLargeByDefault = content.isMediaLargeByDefault { + if isMediaLargeByDefault { + webpageHasLargeMedia = true + } + } else { + webpageHasLargeMedia = true + } + } + + + let entities = [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .Url)] + + var largeMedia = false + if webpageHasLargeMedia { + if let value = urlPreview.largeMedia { + largeMedia = value + } else if case .Loaded = webPage.content { + largeMedia = false //!defaultWebpageImageSizeIsSmall(webpage: content) + } else { + largeMedia = true + } + } else { + largeMedia = false + } + + return ChatControllerSubject.LinkOptions( + messageText: text, + messageEntities: entities, + hasAlternativeLinks: false, + replyMessageId: nil, + replyQuote: nil, + url: urlPreview.url, + webpage: urlPreview.webPage, + linkBelowText: urlPreview.positionBelowText, + largeMedia: largeMedia + ) + } + |> distinctUntilChanged + + let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: .messageOptions(peerIds: [peerId], ids: [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions))), botStart: nil, mode: .standard(.previewing)) + chatController.canReadHistory.set(false) + + let items = linkOptions + |> deliverOnMainQueue + |> map { linkOptions -> ContextController.Items in + var items: [ContextMenuItem] = [] + + do { + items.append(.action(ContextMenuActionItem(text: linkOptions.linkBelowText ? presentationData.strings.Conversation_MessageOptionsLinkMoveUp : presentationData.strings.Conversation_MessageOptionsLinkMoveDown, icon: { theme in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: linkOptions.linkBelowText ? "message_preview_sort_above" : "message_preview_sort_below" + ), action: { _, f in + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + var updatedUrlPreview = current + updatedUrlPreview.positionBelowText = !current.positionBelowText + urlPreview.set(updatedUrlPreview) + }) + }))) + } + + if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault { + let shrinkTitle: String + let enlargeTitle: String + if let file = content.file, file.isVideo { + shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkVideo + enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeVideo + } else { + shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkImage + enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeImage + } + + items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { _ in + return nil + }, iconAnimation: ContextMenuActionItem.IconAnimation( + name: !linkOptions.largeMedia ? "message_preview_media_large" : "message_preview_media_small" + ), action: { _, f in + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + var updatedUrlPreview = current + if let largeMedia = current.largeMedia { + updatedUrlPreview.largeMedia = !largeMedia + } else { + updatedUrlPreview.largeMedia = false + } + urlPreview.set(updatedUrlPreview) + }) + }))) + } + + if !items.isEmpty { + items.append(.separator) + } + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + let _ = (urlPreview.get() + |> take(1)).start(next: { current in + completion(current.positionBelowText, current.largeMedia) + }) + }))) + + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in + remove() + + f(.default) + }))) + + return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items)) + } + + return ContextController.Source( + id: AnyHashable(OptionsId.link), + title: presentationData.strings.Conversation_MessageOptionsTabLink, + source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), + items: items + ) +} + +final class ChatContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceNode: ASDisplayNode? + weak var sourceView: UIView? + let sourceRect: CGRect? + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool + + init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceNode = sourceNode + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { + self.controller = controller + self.sourceView = sourceView + self.sourceRect = sourceRect + self.passthroughTouches = passthroughTouches + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceView = self.sourceView + let sourceNode = self.sourceNode + let sourceRect = self.sourceRect + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in + if let sourceView = sourceView { + return (sourceView, sourceRect ?? sourceView.bounds) + } else if let sourceNode = sourceNode { + return (sourceNode.view, sourceRect ?? sourceNode.bounds) + } else { + return nil + } + }) + } + + func animatedIn() { + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift new file mode 100644 index 0000000000..011d2c8916 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -0,0 +1,975 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting +import MediaEditor + +private let linkTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let link: CreateLinkScreen.Link? + let webpage: TelegramMediaWebpage? + let state: CreateLinkSheetComponent.State + let dismiss: () -> Void + + init( + context: AccountContext, + link: CreateLinkScreen.Link?, + webpage: TelegramMediaWebpage?, + state: CreateLinkSheetComponent.State, + dismiss: @escaping () -> Void + ) { + self.context = context + self.link = link + self.webpage = webpage + self.state = state + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.link != rhs.link { + return false + } + if lhs.webpage != rhs.webpage { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let cancelButton = Child(Button.self) + let doneButton = Child(Button.self) + let title = Child(Text.self) + let urlSection = Child(ListSectionComponent.self) + let nameSection = Child(ListSectionComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = component.state + + let theme = environment.theme.withModalBlocksBackground() + let strings = environment.strings + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), + availableSize: CGSize(width: context.availableSize.width, height: 1000.0), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let cancelButton = cancelButton.update( + component: Button( + content: AnyComponent( + Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: theme.actionSheet.controlAccentColor + ) + ), + action: { + component.dismiss() + } + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(cancelButton + .position(CGPoint(x: sideInset + cancelButton.size.width / 2.0, y: contentSize.height + cancelButton.size.height / 2.0)) + ) + + let controller = environment.controller + let doneButton = doneButton.update( + component: Button( + content: AnyComponent( + Text( + text: strings.Common_Done, + font: Font.bold(17.0), + color: state.link.isEmpty ? theme.actionSheet.secondaryTextColor : theme.actionSheet.controlAccentColor + ) + ), + isEnabled: !state.link.isEmpty, + action: { [weak state] in + if let controller = controller() as? CreateLinkScreen { + state?.complete(controller: controller) + } + component.dismiss() + } + ), + availableSize: context.availableSize, + transition: .immediate + ) + context.add(doneButton + .position(CGPoint(x: context.availableSize.width - sideInset - doneButton.size.width / 2.0, y: contentSize.height + doneButton.size.height / 2.0)) + ) + + let title = title.update( + component: Text(text: component.link == nil ? "Create Link" : "Edit Link", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, 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 += 40.0 + + var urlItems: [AnyComponentWithIdentity] = [] + if let webpage = state.webpage, case .Loaded = webpage.content, !state.dismissed { + urlItems.append( + AnyComponentWithIdentity( + id: "webpage", + component: AnyComponent( + LinkPreviewComponent( + webpage: webpage, + theme: theme, + strings: strings, + presentLinkOptions: { [weak state] sourceNode in + if let controller = controller() as? CreateLinkScreen { + state?.presentLinkOptions(controller: controller, sourceNode: sourceNode) + } + }, + dismiss: { [weak state] in + state?.dismissed = true + state?.updated(transition: .easeInOut(duration: 0.25)) + } + ) + ) + ) + ) + } + urlItems.append( + AnyComponentWithIdentity( + id: "url", + component: AnyComponent( + LinkFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + text: state.link, + link: true, + placeholderText: "https://somesite.com", + textUpdated: { [weak state] text in + state?.link = text + state?.updated() + }, + tag: linkTag + ) + ) + ) + ) + + let urlSection = urlSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "LINK TO".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: urlItems, + displaySeparators: false + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(urlSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += urlSection.size.height + contentSize.height += 30.0 + + let nameSection = nameSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "LINK NAME (OPTIONAL)".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity( + id: "name", + component: AnyComponent( + LinkFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + text: state.name, + link: false, + placeholderText: "Enter a Name", + textUpdated: { [weak state] text in + state?.name = text + } + ) + ) + ) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(nameSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + nameSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += nameSection.size.height + contentSize.height += 32.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class CreateLinkSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let link: CreateLinkScreen.Link? + + init( + context: AccountContext, + link: CreateLinkScreen.Link? + ) { + self.context = context + self.link = link + } + + static func ==(lhs: CreateLinkSheetComponent, rhs: CreateLinkSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.link != rhs.link { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + fileprivate var link: String = "" { + didSet { + self.linkPromise.set(self.link) + } + } + fileprivate var name: String = "" + fileprivate var webpage: TelegramMediaWebpage? + fileprivate var dismissed = false + + private var positionBelowText = true + private var largeMedia: Bool? = nil + + private let previewDisposable = MetaDisposable() + + private let linkDisposable = MetaDisposable() + private let linkPromise = ValuePromise() + + init( + context: AccountContext, + link: CreateLinkScreen.Link? + ) { + self.context = context + + self.link = link?.url ?? "" + self.name = link?.name ?? "" + + super.init() + + self.linkDisposable.set((self.linkPromise.get() + |> delay(1.5, queue: Queue.mainQueue()) + |> deliverOnMainQueue).startStrict(next: { [weak self] link in + guard let self else { + return + } + + guard !link.isEmpty else { + self.dismissed = false + self.previewDisposable.set(nil) + self.webpage = nil + self.updated(transition: .easeInOut(duration: 0.25)) + return + } + + var link = link + if !link.hasPrefix("http://") && !link.hasPrefix("https://") { + link = "https://\(link)" + } + + if self.dismissed { + self.dismissed = false + self.webpage = nil + } + self.previewDisposable.set( + (webpagePreview(account: context.account, urls: [link]) + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + switch result { + case let .result(result): + self.webpage = result?.webpage + case .progress: + self.webpage = nil + } + self.updated(transition: .easeInOut(duration: 0.25)) + }) + ) + })) + } + + deinit { + self.previewDisposable.dispose() + self.linkDisposable.dispose() + } + + func presentLinkOptions(controller: CreateLinkScreen, sourceNode: ASDisplayNode) { + guard let webpage = self.webpage else { + return + } + var link = self.link + if !link.hasPrefix("http://") && !link.hasPrefix("https://") { + link = "https://\(link)" + } + var name: String = self.name + if name.isEmpty { + name = self.link + } + presentLinkOptionsController(context: self.context, selfController: controller, sourceNode: sourceNode, url: link, name: name, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, webPage: webpage, completion: { [weak self] positionBelowText, largeMedia in + guard let self else { + return + } + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + }, remove: { [weak self] in + guard let self else { + return + } + self.dismissed = true + self.updated(transition: .easeInOut(duration: 0.25)) + }) + } + + func complete(controller: CreateLinkScreen) { + var link = self.link + if !link.hasPrefix("http://") && !link.hasPrefix("https://") { + link = "https://\(link)" + } + + let text = !self.name.isEmpty ? self.name : self.link + + var media: [Media] = [] + if let webpage = self.webpage, !self.dismissed { + media = [webpage] + } + + var attributes: [MessageAttribute] = [] + attributes.append(TextEntitiesMessageAttribute(entities: [.init(range: 0 ..< (text as NSString).length, type: .Url)])) + if !self.dismissed { + attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !self.positionBelowText, forceLargeMedia: self.largeMedia, isManuallyAdded: false, isSafe: true)) + } + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + let whiteString = NSAttributedString(string: text, font: Font.with(size: 36, design: .camera, weight: .semibold), textColor: UIColor(rgb: 0x0a84ff)) + let blackString = NSAttributedString(string: text, font: Font.with(size: 36, design: .camera, weight: .semibold), textColor: UIColor(rgb: 0x64d2ff)) + + let textSize = whiteString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), context: nil) + + let whiteCompactImage = generateImage(CGSize(width: textSize.width + 64.0, height: floor(textSize.height * 1.2)), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.setFillColor(UIColor.white.cgColor) + context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: textSize.height * 0.2).cgPath) + context.fillPath() + + let inset = floor((size.height - 36.0) / 2.0) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Link"), color: UIColor(rgb: 0x0a84ff)) { + context.draw(image.cgImage!, in: CGRect(x: inset, y: inset, width: 36.0, height: 36.0)) + } + + UIGraphicsPushContext(context) + whiteString.draw(at: CGPoint(x: inset + 42.0, y: 2.0)) + UIGraphicsPopContext() + })! + + let blackCompactImage = generateImage(CGSize(width: textSize.width + 64.0, height: floor(textSize.height * 1.2)), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.clear(bounds) + + context.setFillColor(UIColor.black.cgColor) + context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: textSize.height * 0.2).cgPath) + context.fillPath() + + let inset = floor((size.height - 36.0) / 2.0) + if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Link"), color: UIColor(rgb: 0x64d2ff)) { + context.draw(image.cgImage!, in: CGRect(x: inset, y: inset, width: 36.0, height: 36.0)) + } + + UIGraphicsPushContext(context) + blackString.draw(at: CGPoint(x: inset + 42.0, y: 2.0)) + UIGraphicsPopContext() + })! + + let completion = controller.completion + let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: controller.view, isLink: true) + renderer.render(completion: { result in + completion( + link, + CreateLinkScreen.Result( + url: link, + name: self.name, + positionBelowText: self.positionBelowText, + largeMedia: self.largeMedia, + image: !media.isEmpty ? result.dayImage : nil, + nightImage: !media.isEmpty ? result.nightImage : nil, + compactLightImage: whiteCompactImage, + compactDarkImage: blackCompactImage + ) + ) + }) + } + } + + func makeState() -> State { + return State(context: self.context, link: self.link) + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + var webpage = context.state.webpage + if context.state.dismissed { + webpage = nil + } + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + link: context.component.link, + webpage: webpage, + state: context.state, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.dark), + 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 final class CreateLinkScreen: ViewControllerComponentContainer { + public struct Link: Equatable { + let url: String + let name: String? + + init(url: String, name: String?) { + self.url = url + self.name = name + } + } + + public struct Result { + let url: String + let name: String + let positionBelowText: Bool + let largeMedia: Bool? + let image: UIImage? + let nightImage: UIImage? + let compactLightImage: UIImage + let compactDarkImage: UIImage + } + + private let context: AccountContext + fileprivate let completion: (String, CreateLinkScreen.Result) -> Void + + public init( + context: AccountContext, + link: CreateLinkScreen.Link?, + completion: @escaping (String, CreateLinkScreen.Result) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: CreateLinkSheetComponent( + context: context, + link: link + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .dark + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let view = self.node.hostView.findTaggedView(tag: linkTag) as? LinkFieldComponent.View { + view.activateInput() + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class LinkFieldComponent: Component { + typealias EnvironmentType = Empty + + let textColor: UIColor + let placeholderColor: UIColor + let text: String + let link: Bool + let placeholderText: String + let textUpdated: (String) -> Void + let tag: AnyObject? + + init( + textColor: UIColor, + placeholderColor: UIColor, + text: String, + link: Bool, + placeholderText: String, + textUpdated: @escaping (String) -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.placeholderColor = placeholderColor + self.text = text + self.link = link + self.placeholderText = placeholderText + self.textUpdated = textUpdated + self.tag = tag + } + + static func ==(lhs: LinkFieldComponent, rhs: LinkFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate, 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 placeholderView: ComponentView + private let textField: TextFieldNodeView + + private var component: LinkFieldComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + + super.init(frame: frame) + + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) + + self.addSubview(self.textField) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func textChanged(_ sender: Any) { + let text = self.textField.text ?? "" + self.component?.textUpdated(text) + self.placeholderView.view?.isHidden = !text.isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + if newText.count > 128 { + textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + return false + } + return true + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func update(component: LinkFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.textField.textColor = component.textColor + self.textField.text = component.text + self.textField.font = Font.regular(17.0) + self.textField.keyboardAppearance = .dark + + if component.link { + self.textField.keyboardType = .default + self.textField.returnKeyType = .next + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + self.textField.textContentType = .URL + } + + self.component = component + self.state = state + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + let size = CGSize(width: availableSize.width, height: 44.0) + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + + placeholderComponentView.isHidden = !component.text.isEmpty + } + + self.textField.frame = CGRect(x: 15.0, y: 0.0, width: size.width - 30.0, height: 44.0) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class LinkPreviewComponent: Component { + typealias EnvironmentType = Empty + + let webpage: TelegramMediaWebpage + let theme: PresentationTheme + let strings: PresentationStrings + let presentLinkOptions: (ASDisplayNode) -> Void + let dismiss: () -> Void + + init( + webpage: TelegramMediaWebpage, + theme: PresentationTheme, + strings: PresentationStrings, + presentLinkOptions: @escaping (ASDisplayNode) -> Void, + dismiss: @escaping () -> Void + ) { + self.webpage = webpage + self.theme = theme + self.strings = strings + self.presentLinkOptions = presentLinkOptions + self.dismiss = dismiss + } + + static func ==(lhs: LinkPreviewComponent, rhs: LinkPreviewComponent) -> Bool { + if lhs.webpage != rhs.webpage { + return false + } + if lhs.theme !== rhs.theme { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate { + let closeButton: HighlightableButtonNode + let lineNode: ASImageNode + let iconView: UIImageView + let titleNode: TextNode + private var titleString: NSAttributedString? + + let textNode: TextNode + private var textString: NSAttributedString? + + private var component: LinkPreviewComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.closeButton = HighlightableButtonNode() + + self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASImageNode() + self.lineNode.displayWithoutProcessing = true + self.lineNode.displaysAsynchronously = false + + self.iconView = UIImageView() + self.iconView.image = UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon")?.withRenderingMode(.alwaysTemplate) + + self.titleNode = TextNode() + self.titleNode.displaysAsynchronously = false + + self.textNode = TextNode() + self.textNode.displaysAsynchronously = false + + super.init(frame: frame) + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubview(self.iconView) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func closePressed() { + guard let component = self.component else { + return + } + component.dismiss() + } + + private var previousTapTimestamp: Double? + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state, let component = self.component { + let timestamp = CFAbsoluteTimeGetCurrent() + if let previousTapTimestamp = self.previousTapTimestamp, previousTapTimestamp + 1.0 > timestamp { + return + } + self.previousTapTimestamp = CFAbsoluteTimeGetCurrent() + component.presentLinkOptions(self.textNode) + } + } + + func update(component: LinkPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + self.component = component + self.state = state + + if themeUpdated { + self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(component.theme), for: []) + self.lineNode.image = PresentationResourcesChat.chatInputPanelVerticalSeparatorLineImage(component.theme) + self.iconView.tintColor = component.theme.chat.inputPanel.panelControlAccentColor + } + + let bounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 45.0)) + + var authorName = "" + var text = "" + switch component.webpage.content { + case .Pending: + authorName = component.strings.Channel_NotificationLoading + text = ""//component.url + case let .Loaded(content): + if let contentText = content.text { + text = contentText + } else { + if let file = content.file, let mediaKind = mediaContentKind(EngineMedia(file)) { + if content.type == "telegram_background" { + text = component.strings.Message_Wallpaper + } else if content.type == "telegram_theme" { + text = component.strings.Message_Theme + } else { + text = stringForMediaKind(mediaKind, strings: component.strings).0.string + } + } else if content.type == "telegram_theme" { + text = component.strings.Message_Theme + } else if content.type == "video" { + text = stringForMediaKind(.video, strings: component.strings).0.string + } else if content.type == "telegram_story" { + text = stringForMediaKind(.story, strings: component.strings).0.string + } else if let _ = content.image { + text = stringForMediaKind(.image, strings: component.strings).0.string + } + } + + if let title = content.title { + authorName = title + } else if let websiteName = content.websiteName { + authorName = websiteName + } else { + authorName = content.displayUrl + } + + } + + self.titleString = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: component.theme.chat.inputPanel.panelControlAccentColor) + self.textString = NSAttributedString(string: text, font: Font.regular(15.0), textColor: component.theme.chat.inputPanel.primaryTextColor) + + let inset: CGFloat = 0.0 + let leftInset: CGFloat = 55.0 + let textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 55.0 + let textRightInset: CGFloat = 20.0 + + let closeButtonSize = CGSize(width: 44.0, height: bounds.height) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - closeButtonSize.width - inset, y: 2.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + + if let icon = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 7.0 + inset, y: 10.0), size: icon.size) + } + + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeTextLayout = TextNode.asyncLayout(self.textNode) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: self.titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: self.textString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height), alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets())) + + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleLayout.size) + + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textLayout.size) + + let _ = titleApply() + let _ = textApply() + + return bounds.size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 1f7551ba8b..976fd52364 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4173,73 +4173,74 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } }, completion: { [weak self] location, queryId, resultId, address, countryCode in - if let self { - let emojiFile: Signal - if let countryCode { - let flag = flagEmoji(countryCode: countryCode) - emojiFile = self.staticEmojiPack.get() - |> filter { result in - if case .result = result { - return true - } else { - return false - } - } - |> take(1) - |> map { result -> TelegramMediaFile? in - if case let .result(_, items, _) = result, let match = items.first(where: { item in - var displayText: String? - for attribute in item.file.attributes { - if case let .CustomEmoji(_, _, alt, _) = attribute { - displayText = alt - break - } - } - if let displayText, displayText.hasPrefix(flag) { + if let self { + let emojiFile: Signal + if let countryCode { + let flag = flagEmoji(countryCode: countryCode) + emojiFile = self.staticEmojiPack.get() + |> filter { result in + if case .result = result { return true } else { return false } - }) { - return match.file - } else { - return nil } - } - } else { - emojiFile = .single(nil) - } - - let _ = emojiFile.start(next: { [weak self] emojiFile in - guard let self else { - return - } - let title: String - if let venueTitle = location.venue?.title { - title = venueTitle + |> take(1) + |> map { result -> TelegramMediaFile? in + if case let .result(_, items, _) = result, let match = items.first(where: { item in + var displayText: String? + for attribute in item.file.attributes { + if case let .CustomEmoji(_, _, alt, _) = attribute { + displayText = alt + break + } + } + if let displayText, displayText.hasPrefix(flag) { + return true + } else { + return false + } + }) { + return match.file + } else { + return nil + } + } } else { - title = address ?? "Location" + emojiFile = .single(nil) } - let position = existingEntity?.position - let scale = existingEntity?.scale ?? 1.0 - if let existingEntity { - self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) - } - self.interaction?.insertEntity( - DrawingLocationEntity( - title: title, - style: existingEntity?.style ?? .white, - location: location, - icon: emojiFile, - queryId: queryId, - resultId: resultId - ), - scale: scale, - position: position - ) - }) + + let _ = emojiFile.start(next: { [weak self] emojiFile in + guard let self else { + return + } + let title: String + if let venueTitle = location.venue?.title { + title = venueTitle + } else { + title = address ?? "Location" + } + let position = existingEntity?.position + let scale = existingEntity?.scale ?? 1.0 + if let existingEntity { + self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) + } + self.interaction?.insertEntity( + DrawingLocationEntity( + title: title, + style: existingEntity?.style ?? .white, + location: location, + icon: emojiFile, + queryId: queryId, + resultId: resultId + ), + scale: scale, + position: position + ) + }) + } } - }) + ) locationController.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak locationController] transition in if let self, let locationController { let transitionFactor = locationController.modalStyleOverlayTransitionFactor @@ -4477,6 +4478,39 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(contextController, in: .window(.root)) } + func addLink() { + guard let controller = self.controller else { + return + } + + let linkController = CreateLinkScreen(context: controller.context, link: nil, completion: { [weak self] url, result in + guard let self else { + return + } + + let entity = DrawingStickerEntity( + content: .link(url, result.name, result.positionBelowText, result.largeMedia, result.image?.size, result.compactLightImage.size, result.image != nil ? .white : .whiteCompact) + ) + entity.renderImage = result.image + entity.secondaryRenderImage = result.nightImage + entity.tertiaryRenderImage = result.compactLightImage + entity.quaternaryRenderImage = result.compactDarkImage + + let fraction: CGFloat + if let image = result.image { + fraction = max(image.size.width, image.size.height) / 353.0 + } else { + fraction = 1.0 + } + self.interaction?.insertEntity( + entity, + scale: min(6.0, 3.3 * fraction) * 0.5, + position: nil + ) + }) + controller.push(linkController) + } + func addReaction() { guard let controller = self.controller else { return @@ -4750,6 +4784,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller?.dismiss(animated: true) } } + controller.addLink = { [weak self, weak controller] in + if let self { + self.addLink() + + self.stickerScreen = nil + controller?.dismiss(animated: true) + } + } controller.pushController = { [weak self] c in self?.controller?.push(c) } @@ -5676,6 +5718,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self?.node.presentGallery() }))) + //TODO:localize + items.append(.action(ContextMenuActionItem(text: "Link", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + self?.node.addLink() + }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaEditor_Shortcut_Location, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LocationSmall"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 23111a30a1..2a9ae6d1bc 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -556,6 +556,7 @@ private final class PeerInfoInteraction { let editingOpenNameColorSetup: () -> Void let editingOpenInviteLinksSetup: () -> Void let editingOpenDiscussionGroupSetup: () -> Void + let editingOpenStars: () -> Void let editingToggleMessageSignatures: (Bool) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void @@ -623,6 +624,7 @@ private final class PeerInfoInteraction { editingOpenNameColorSetup: @escaping () -> Void, editingOpenInviteLinksSetup: @escaping () -> Void, editingOpenDiscussionGroupSetup: @escaping () -> Void, + editingOpenStars: @escaping () -> Void, editingToggleMessageSignatures: @escaping (Bool) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, @@ -689,6 +691,7 @@ private final class PeerInfoInteraction { self.editingOpenNameColorSetup = editingOpenNameColorSetup self.editingOpenInviteLinksSetup = editingOpenInviteLinksSetup self.editingOpenDiscussionGroupSetup = editingOpenDiscussionGroupSetup + self.editingOpenStars = editingOpenStars self.editingToggleMessageSignatures = editingToggleMessageSignatures self.openParticipantsSection = openParticipantsSection self.openRecentActions = openRecentActions @@ -1717,17 +1720,25 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInfo = 3 let ItemDelete = 4 let ItemUsername = 5 + let ItemStars = 6 - let ItemIntro = 6 - let ItemCommands = 7 - let ItemBotSettings = 8 - let ItemBotInfo = 9 + let ItemIntro = 7 + let ItemCommands = 8 + let ItemBotSettings = 9 + let ItemBotInfo = 10 if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_BotLinks, icon: nil, action: { + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_BotLinks, icon: PresentationResourcesSettings.bot, action: { interaction.editingOpenPublicLinkSetup() })) + if "".isEmpty { + let balance: Int64 = 1000 + items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(balance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: { + interaction.editingOpenStars() + })) + } + items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: { interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive))) })) @@ -2626,6 +2637,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro editingOpenDiscussionGroupSetup: { [weak self] in self?.editingOpenDiscussionGroupSetup() }, + editingOpenStars: { [weak self] in + self?.editingOpenStars() + }, editingToggleMessageSignatures: { [weak self] value in self?.editingToggleMessageSignatures(value: value) }, @@ -3338,7 +3352,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in @@ -8329,6 +8342,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(channelDiscussionGroupSetupController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)) } + private func editingOpenStars() { + guard let starsContext = self.context.starsContext else { + return + } + self.controller?.push(self.context.sharedContext.makeStarsStatisticsScreen(context: self.context, starsContext: starsContext)) + } + private func editingOpenReactionsSetup() { guard let data = self.data, let peer = data.peer else { return @@ -8493,7 +8513,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let context = self.context let presentationData = self.presentationData - let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) let controllerParams = LocationViewParams(sendLiveLocation: { _ in }, stopLiveLocation: { _ in @@ -11930,8 +11950,9 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc self.chatLocation = .peer(id: peerId) } - if isSettings { - self.starsContext = context.starsContext + if isSettings, let starsContext = context.starsContext { + self.starsContext = starsContext + starsContext.load(force: true) } else { self.starsContext = nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift index 7a8cde1f26..634fc91436 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/StorySearchGridScreen.swift @@ -22,13 +22,16 @@ final class StorySearchGridScreenComponent: Component { let context: AccountContext let searchQuery: String + let listContext: SearchStoryListContext? init( context: AccountContext, - searchQuery: String + searchQuery: String, + listContext: SearchStoryListContext? = nil ) { self.context = context self.searchQuery = searchQuery + self.listContext = listContext } static func ==(lhs: StorySearchGridScreenComponent, rhs: StorySearchGridScreenComponent) -> Bool { @@ -115,7 +118,7 @@ final class StorySearchGridScreenComponent: Component { } return self.environment?.controller()?.navigationController as? NavigationController }, - listContext: nil + listContext: component.listContext ) paneNode.isEmptyUpdated = { [weak self] _ in guard let self else { @@ -178,14 +181,16 @@ public class StorySearchGridScreen: ViewControllerComponentContainer { public init( context: AccountContext, - searchQuery: String + searchQuery: String, + listContext: SearchStoryListContext? = nil ) { self.context = context self.searchQuery = searchQuery super.init(context: context, component: StorySearchGridScreenComponent( context: context, - searchQuery: searchQuery + searchQuery: searchQuery, + listContext: listContext ), navigationBarAppearance: .default, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index a9be603144..8bc4a190aa 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -1192,7 +1192,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, chatControllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - chatControllerInteraction.longTap(action, message) + chatControllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return chatControllerInteraction.hiddenMedia diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 66546529ef..1fcd9997cb 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -14,7 +14,8 @@ final class StarsBalanceComponent: Component { let strings: PresentationStrings let dateTimeFormat: PresentationDateTimeFormat let count: Int64 - let purchaseAvailable: Bool + let rate: Double? + let actionAvailable: Bool let buy: () -> Void init( @@ -22,14 +23,16 @@ final class StarsBalanceComponent: Component { strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, count: Int64, - purchaseAvailable: Bool, + rate: Double?, + actionAvailable: Bool, buy: @escaping () -> Void ) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat self.count = count - self.purchaseAvailable = purchaseAvailable + self.rate = rate + self.actionAvailable = actionAvailable self.buy = buy } @@ -43,12 +46,15 @@ final class StarsBalanceComponent: Component { if lhs.dateTimeFormat != rhs.dateTimeFormat { return false } - if lhs.purchaseAvailable != rhs.purchaseAvailable { + if lhs.actionAvailable != rhs.actionAvailable { return false } if lhs.count != rhs.count { return false } + if lhs.rate != rhs.rate { + return false + } return true } @@ -125,7 +131,7 @@ final class StarsBalanceComponent: Component { } contentHeight += subtitleSize.height - if component.purchaseAvailable { + if component.actionAvailable { contentHeight += 12.0 let buttonSize = self.button.update( diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift new file mode 100644 index 0000000000..a19fd9ddc5 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -0,0 +1,742 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import SwiftSignalKit +import ViewControllerComponent +import ComponentDisplayAdapters +import TelegramPresentationData +import AccountContext +import TelegramCore +import Postbox +import MultilineTextComponent +import BalancedTextComponent +import Markdown +import PremiumStarComponent +import ListSectionComponent +import BundleIconComponent +import TextFormat +import UndoUI + +final class StarsStatisticsScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let starsContext: StarsContext + let openTransaction: (StarsContext.State.Transaction) -> Void + let buy: () -> Void + + init( + context: AccountContext, + starsContext: StarsContext, + openTransaction: @escaping (StarsContext.State.Transaction) -> Void, + buy: @escaping () -> Void + ) { + self.context = context + self.starsContext = starsContext + self.openTransaction = openTransaction + self.buy = buy + } + + static func ==(lhs: StarsStatisticsScreenComponent, rhs: StarsStatisticsScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.starsContext !== rhs.starsContext { + return false + } + return true + } + + private final class ScrollViewImpl: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + + override var contentOffset: CGPoint { + set(value) { + var value = value + if value.y > self.contentSize.height - self.bounds.height { + value.y = max(0.0, self.contentSize.height - self.bounds.height) + self.bounces = false + } else { + self.bounces = true + } + super.contentOffset = value + } get { + return super.contentOffset + } + } + } + + class View: UIView, UIScrollViewDelegate { + private let scrollView: ScrollViewImpl + + private var currentSelectedPanelId: AnyHashable? + + private let navigationBackgroundView: BlurredBackgroundView + private let navigationSeparatorLayer: SimpleLayer + private let navigationSeparatorLayerContainer: SimpleLayer + + private let headerView = ComponentView() + private let headerOffsetContainer: UIView + + private let scrollContainerView: UIView + + private let overscroll = ComponentView() + private let fade = ComponentView() + private let starView = ComponentView() + private let titleView = ComponentView() + private let descriptionView = ComponentView() + + private let balanceView = ComponentView() + + private let topBalanceTitleView = ComponentView() + private let topBalanceValueView = ComponentView() + private let topBalanceIconView = ComponentView() + + private let panelContainer = ComponentView() + + private var component: StarsStatisticsScreenComponent? + private weak var state: EmptyComponentState? + private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? + private var controller: (() -> ViewController?)? + + private var enableVelocityTracking: Bool = false + private var previousVelocityM1: CGFloat = 0.0 + private var previousVelocity: CGFloat = 0.0 + + private var ignoreScrolling: Bool = false + + private var stateDisposable: Disposable? + private var starsState: StarsContext.State? + + private var previousBalance: Int64? + + private var allTransactionsContext: StarsTransactionsContext? + private var incomingTransactionsContext: StarsTransactionsContext? + private var outgoingTransactionsContext: StarsTransactionsContext? + + override init(frame: CGRect) { + self.headerOffsetContainer = UIView() + self.headerOffsetContainer.isUserInteractionEnabled = false + + self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + self.navigationBackgroundView.alpha = 0.0 + + self.navigationSeparatorLayer = SimpleLayer() + self.navigationSeparatorLayer.opacity = 0.0 + self.navigationSeparatorLayerContainer = SimpleLayer() + self.navigationSeparatorLayerContainer.opacity = 0.0 + + self.scrollContainerView = UIView() + self.scrollView = ScrollViewImpl() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + 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.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContainerView) + + self.addSubview(self.navigationBackgroundView) + + self.navigationSeparatorLayerContainer.addSublayer(self.navigationSeparatorLayer) + self.layer.addSublayer(self.navigationSeparatorLayerContainer) + + self.addSubview(self.headerOffsetContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.stateDisposable?.dispose() + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.enableVelocityTracking = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + if self.enableVelocityTracking { + self.previousVelocityM1 = self.previousVelocity + if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue { + self.previousVelocity = CGFloat(value) + } + } + + self.updateScrolling(transition: .immediate) + } + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + guard let _ = self.navigationMetrics else { + return + } + + let paneAreaExpansionDistance: CGFloat = 32.0 + let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height + if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint { + targetContentOffset.pointee.y = paneAreaExpansionFinalPoint + self.enableVelocityTracking = false + self.previousVelocity = 0.0 + self.previousVelocityM1 = 0.0 + } + } + + private func updateScrolling(transition: Transition) { + let scrollBounds = self.scrollView.bounds + + let isLockedAtPanels = scrollBounds.maxY == self.scrollView.contentSize.height + + if let navigationMetrics = self.navigationMetrics { + let topInset: CGFloat = navigationMetrics.navigationHeight - 56.0 + + let titleOffset: CGFloat + let titleScale: CGFloat + let titleOffsetDelta = (topInset + 160.0) - (navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) + + var topContentOffset = self.scrollView.contentOffset.y + + let navigationBackgroundAlpha = min(20.0, max(0.0, topContentOffset - 95.0)) / 20.0 + topContentOffset = topContentOffset + max(0.0, min(1.0, topContentOffset / titleOffsetDelta)) * 10.0 + titleOffset = topContentOffset + let fraction = max(0.0, min(1.0, titleOffset / titleOffsetDelta)) + titleScale = 1.0 - fraction * 0.36 + + let headerTransition: Transition = .immediate + + if let starView = self.starView.view { + let starPosition = CGPoint(x: self.scrollView.frame.width / 2.0, y: topInset + starView.bounds.height / 2.0 - 30.0 - titleOffset * titleScale) + + headerTransition.setPosition(view: starView, position: starPosition) + headerTransition.setScale(view: starView, scale: titleScale) + } + + if let titleView = self.titleView.view { + let titlePosition = CGPoint(x: scrollBounds.width / 2.0, y: max(topInset + 160.0 - titleOffset, navigationMetrics.statusBarHeight + (navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0)) + + headerTransition.setPosition(view: titleView, position: titlePosition) + headerTransition.setScale(view: titleView, scale: titleScale) + } + + let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) + animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) + animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) + + let expansionDistance: CGFloat = 32.0 + var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance + expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) + + transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) + if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { + panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) + } + + let topBalanceAlpha = 1.0 - expansionDistanceFactor + if let view = self.topBalanceTitleView.view { + view.alpha = topBalanceAlpha + } + if let view = self.topBalanceValueView.view { + view.alpha = topBalanceAlpha + } + if let view = self.topBalanceIconView.view { + view.alpha = topBalanceAlpha + } + } + + let _ = self.panelContainer.updateEnvironment( + transition: transition, + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: isLockedAtPanels) + } + ) + } + + private var isUpdating = false + func update(component: StarsStatisticsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.component = component + self.state = state + + var balanceUpdated = false + if let starsState = self.starsState { + if let previousBalance, starsState.balance != previousBalance { + balanceUpdated = true + } + self.previousBalance = starsState.balance + } + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + + if self.stateDisposable == nil { + self.stateDisposable = (component.starsContext.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + self.starsState = state + + if !self.isUpdating { + self.state?.updated() + } + }) + } + + var wasLockedAtPanels = false + if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics { + if self.scrollView.bounds.minY > 0.0 && abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel { + wasLockedAtPanels = true + } + } + + self.controller = environment.controller + + self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) + + self.navigationSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor + + let navigationFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: environment.navigationHeight)) + self.navigationBackgroundView.updateColor(color: environment.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.navigationBackgroundView.update(size: navigationFrame.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(view: self.navigationBackgroundView, frame: navigationFrame) + + let navigationSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)) + + transition.setFrame(layer: self.navigationSeparatorLayerContainer, frame: navigationSeparatorFrame) + transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(), size: navigationSeparatorFrame.size)) + + self.backgroundColor = environment.theme.list.blocksBackgroundColor + + var contentHeight: CGFloat = 0.0 + + let sideInsets: CGFloat = environment.safeInsets.left + environment.safeInsets.right + 16 * 2.0 + let bottomInset: CGFloat = environment.safeInsets.bottom + + contentHeight += environment.statusBarHeight + + let starTransition: Transition = .immediate + + var topBackgroundColor = environment.theme.list.plainBackgroundColor + let bottomBackgroundColor = environment.theme.list.blocksBackgroundColor + if environment.theme.overallDarkAppearance { + topBackgroundColor = bottomBackgroundColor + } + + let overscrollSize = self.overscroll.update( + transition: .immediate, + component: AnyComponent(Rectangle(color: topBackgroundColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: -overscrollSize.height), size: overscrollSize) + if let overscrollView = self.overscroll.view { + if overscrollView.superview == nil { + self.scrollView.addSubview(overscrollView) + } + starTransition.setFrame(view: overscrollView, frame: overscrollFrame) + } + + let fadeSize = self.fade.update( + transition: .immediate, + component: AnyComponent(RoundedRectangle( + colors: [ + topBackgroundColor, + bottomBackgroundColor + ], + cornerRadius: 0.0, + gradientDirection: .vertical + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + let fadeFrame = CGRect(origin: CGPoint(x: 0.0, y: -fadeSize.height), size: fadeSize) + if let fadeView = self.fade.view { + if fadeView.superview == nil { + self.scrollView.addSubview(fadeView) + } + starTransition.setFrame(view: fadeView, frame: fadeFrame) + } + + let starSize = self.starView.update( + transition: .immediate, + component: AnyComponent(PremiumStarComponent( + theme: environment.theme, + isIntro: true, + isVisible: true, + hasIdleAnimations: true, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ], + particleColor: UIColor(rgb: 0xf9b004) + )), + environment: {}, + containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0) + ) + let starFrame = CGRect(origin: .zero, size: starSize) + if let starView = self.starView.view { + if starView.superview == nil { + self.insertSubview(starView, aboveSubview: self.scrollView) + } + starTransition.setBounds(view: starView, bounds: starFrame) + } + + let titleSize = self.titleView.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Title, font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ) + ), + environment: {}, + containerSize: availableSize + ) + if let titleView = self.titleView.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + starTransition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize)) + } + + let topBalanceTitleSize = self.topBalanceTitleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stars_Intro_Balance, + font: Font.regular(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + + let topBalanceValueSize = self.topBalanceValueView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator), + font: Font.semibold(14.0), + textColor: environment.theme.actionSheet.primaryTextColor + )), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: 120.0, height: 100.0) + ) + let topBalanceIconSize = self.topBalanceIconView.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil)), + environment: {}, + containerSize: availableSize + ) + + let navigationHeight = environment.navigationHeight - environment.statusBarHeight + let topBalanceOriginY = environment.statusBarHeight + (navigationHeight - topBalanceTitleSize.height - topBalanceValueSize.height) / 2.0 + let topBalanceTitleFrame = CGRect(origin: CGPoint(x: availableSize.width - topBalanceTitleSize.width - 16.0 - environment.safeInsets.right, y: topBalanceOriginY), size: topBalanceTitleSize) + if let topBalanceTitleView = self.topBalanceTitleView.view { + if topBalanceTitleView.superview == nil { + topBalanceTitleView.alpha = 0.0 + self.addSubview(topBalanceTitleView) + } + starTransition.setFrame(view: topBalanceTitleView, frame: topBalanceTitleFrame) + } + + let topBalanceValueFrame = CGRect(origin: CGPoint(x: availableSize.width - topBalanceValueSize.width - 16.0 - environment.safeInsets.right, y: topBalanceTitleFrame.maxY), size: topBalanceValueSize) + if let topBalanceValueView = self.topBalanceValueView.view { + if topBalanceValueView.superview == nil { + topBalanceValueView.alpha = 0.0 + self.addSubview(topBalanceValueView) + } + starTransition.setFrame(view: topBalanceValueView, frame: topBalanceValueFrame) + } + + let topBalanceIconFrame = CGRect(origin: CGPoint(x: topBalanceValueFrame.minX - topBalanceIconSize.width - 2.0, y: floorToScreenPixels(topBalanceValueFrame.midY - topBalanceIconSize.height / 2.0) - UIScreenPixel), size: topBalanceIconSize) + if let topBalanceIconView = self.topBalanceIconView.view { + if topBalanceIconView.superview == nil { + topBalanceIconView.alpha = 0.0 + self.addSubview(topBalanceIconView) + } + starTransition.setFrame(view: topBalanceIconView, frame: topBalanceIconFrame) + } + + contentHeight += 181.0 + + let descriptionSize = self.descriptionView.update( + transition: .immediate, + component: AnyComponent( + BalancedTextComponent( + text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Description, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets - 8.0, height: 240.0) + ) + let descriptionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - descriptionSize.width) / 2.0), y: contentHeight + 20.0 - floor(descriptionSize.height / 2.0)), size: descriptionSize) + if let descriptionView = self.descriptionView.view { + if descriptionView.superview == nil { + self.scrollView.addSubview(descriptionView) + } + + starTransition.setFrame(view: descriptionView, frame: descriptionFrame) + } + + contentHeight += descriptionSize.height + contentHeight += 29.0 + + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + let balanceSize = self.balanceView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( + StarsBalanceComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + count: self.starsState?.balance ?? 0, + rate: nil, + actionAvailable: !premiumConfiguration.areStarsDisabled, + buy: { [weak self] in + guard let self, let component = self.component else { + return + } + component.buy() + } + ) + ))] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let balanceFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - balanceSize.width) / 2.0), y: contentHeight), size: balanceSize) + if let balanceView = self.balanceView.view { + if balanceView.superview == nil { + self.scrollView.addSubview(balanceView) + } + starTransition.setFrame(view: balanceView, frame: balanceFrame) + } + + contentHeight += balanceSize.height + contentHeight += 44.0 + + let initialTransactions = self.starsState?.transactions ?? [] + var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] + if !initialTransactions.isEmpty { + let allTransactionsContext: StarsTransactionsContext + if let current = self.allTransactionsContext { + allTransactionsContext = current + } else { + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .all) + } + + let incomingTransactionsContext: StarsTransactionsContext + if let current = self.incomingTransactionsContext { + incomingTransactionsContext = current + } else { + incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .incoming) + } + + let outgoingTransactionsContext: StarsTransactionsContext + if let current = self.outgoingTransactionsContext { + outgoingTransactionsContext = current + } else { + outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(starsContext: component.starsContext, subject: .outgoing) + } + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: environment.strings.Stars_Intro_AllTransactions, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: allTransactionsContext, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: environment.strings.Stars_Intro_Incoming, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: incomingTransactionsContext, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: environment.strings.Stars_Intro_Outgoing, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: outgoingTransactionsContext, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + } + + var panelTransition = transition + if balanceUpdated { + panelTransition = .easeInOut(duration: 0.25) + } + + if !panelItems.isEmpty { + let panelContainerSize = self.panelContainer.update( + transition: panelTransition, + component: AnyComponent(StarsTransactionsPanelContainerComponent( + theme: environment.theme, + strings: environment.strings, + dateTimeFormat: environment.dateTimeFormat, + insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: bottomInset, right: environment.safeInsets.right), + items: panelItems, + currentPanelUpdated: { [weak self] id, transition in + guard let self else { + return + } + self.currentSelectedPanelId = id + self.state?.updated(transition: transition) + } + )), + environment: { + StarsTransactionsPanelContainerEnvironment(isScrollable: wasLockedAtPanels) + }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight) + ) + if let panelContainerView = self.panelContainer.view { + if panelContainerView.superview == nil { + self.scrollContainerView.addSubview(panelContainerView) + } + transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize)) + } + contentHeight += panelContainerSize.height + } else { + self.panelContainer.view?.removeFromSuperview() + } + + self.ignoreScrolling = true + + let contentOffset = self.scrollView.bounds.minY + transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center) + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + transition.setFrame(view: self.scrollContainerView, frame: CGRect(origin: CGPoint(), size: contentSize)) + + var scrollViewBounds = self.scrollView.bounds + scrollViewBounds.size = availableSize + if wasLockedAtPanels, let panelContainerView = self.panelContainer.view { + scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight + } + transition.setBounds(view: self.scrollView, bounds: scrollViewBounds) + + if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset { + let deltaOffset = self.scrollView.bounds.minY - contentOffset + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) + } + + self.ignoreScrolling = false + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class StarsStatisticsScreen: ViewControllerComponentContainer { + private let context: AccountContext + private let starsContext: StarsContext + + public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) { + self.context = context + self.starsContext = starsContext + + var withdrawImpl: (() -> Void)? + var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + super.init(context: context, component: StarsStatisticsScreenComponent( + context: context, + starsContext: starsContext, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + buy: { + withdrawImpl?() + } + ), navigationBarAppearance: .transparent) + + self.navigationPresentation = .modalInLargeLayout + + openTransactionImpl = { [weak self] transaction in + guard let self else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction) + self.push(controller) + } + + withdrawImpl = { [weak self] in + guard let _ = self else { + return + } + } + + self.starsContext.load(force: false) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 2659ac2f52..5f20863eac 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -529,7 +529,8 @@ final class StarsTransactionsScreenComponent: Component { strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, count: self.starsState?.balance ?? 0, - purchaseAvailable: !premiumConfiguration.areStarsDisabled, + rate: nil, + actionAvailable: !premiumConfiguration.areStarsDisabled, buy: { [weak self] in guard let self, let component = self.component else { return @@ -714,6 +715,8 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { } ), navigationBarAppearance: .transparent) + self.navigationPresentation = .modalInLargeLayout + self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) openTransactionImpl = { [weak self] transaction in diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift new file mode 100644 index 0000000000..07020eeb8e --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsUtils.swift @@ -0,0 +1,6 @@ +import Foundation + +func formatUsdValue(_ value: Int64, rate: Double) -> String { + let formattedValue = String(format: "%0.2f", (Double(value) / 1000000000) * rate) + return "$\(formattedValue)" +} diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 851c034270..db16c8152d 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -542,8 +542,8 @@ public class StickerPickerScreen: ViewController { self.storyStickersContentView?.reactionAction = { [weak self] in self?.controller?.addReaction() } - self.storyStickersContentView?.cameraAction = { [weak self] in - self?.controller?.addCamera() + self.storyStickersContentView?.linkAction = { [weak self] in + self?.controller?.addLink() } } @@ -2029,7 +2029,7 @@ public class StickerPickerScreen: ViewController { public var presentLocationPicker: () -> Void = { } public var presentAudioPicker: () -> Void = { } public var addReaction: () -> Void = { } - public var addCamera: () -> Void = { } + public var addLink: () -> Void = { } public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { self.context = context @@ -2502,15 +2502,39 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { var locationAction: () -> Void = {} var audioAction: () -> Void = {} var reactionAction: () -> Void = {} - var cameraAction: () -> Void = {} + var linkAction: () -> Void = {} func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize { + //TODO:localize let padding: CGFloat = 22.0 let size = self.container.update( transition: transition, component: AnyComponent( ItemStack( [ + AnyComponentWithIdentity( + id: "link", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", + component: AnyComponent( + InteractiveStickerButtonContent( + theme: theme, + title: "LINK", + iconName: "Premium/Link", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.linkAction() + } + }) + ) + ), AnyComponentWithIdentity( id: "location", component: AnyComponent( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e5ce122d6e..2af72518be 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2919,7 +2919,7 @@ final class StoryItemSetContainerSendMessage { } if !hashtag.isEmpty { if peerName == nil { - let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag) + let searchController = component.context.sharedContext.makeStorySearchController(context: component.context, query: hashtag, listContext: nil) navigationController.pushViewController(searchController) } else { let searchController = component.context.sharedContext.makeHashtagSearchController(context: component.context, peer: peer.flatMap(EnginePeer.init), query: hashtag, all: true) @@ -3348,7 +3348,7 @@ final class StoryItemSetContainerSendMessage { switch mediaArea { case let .venue(_, venue): let action = { [weak controller, weak view] in - let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let locationController = LocationViewController( context: context, updatedPresentationData: updatedPresentationData, @@ -3431,8 +3431,30 @@ final class StoryItemSetContainerSendMessage { })) case .reaction: return - case .url: - return + case let .link(_, url): + let action = { + let _ = openUserGeneratedUrl(context: component.context, peerId: component.slice.effectivePeer.id, url: url, concealed: false, skipUrlAuth: false, skipConcealedAlert: false, forceDark: true, present: { [weak controller] c in + controller?.present(c, in: .window(.root)) + }, openResolved: { [weak self, weak view] resolved in + guard let self, let view else { + return + } + self.openResolved(view: view, result: resolved, forceExternal: false, concealed: false) + }, alertDisplayUpdated: { [weak self, weak view] alertController in + guard let self, let view else { + return + } + self.statusController = alertController + view.updateIsProgressPaused() + }) + } + if immediate { + action() + return + } + actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewLink, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { + action() + })) } self.selectedMediaArea = mediaArea diff --git a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift index 6bb6808304..d2082327c7 100644 --- a/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent/Sources/StorySetIndicatorComponent.swift @@ -8,6 +8,7 @@ import Postbox import SwiftSignalKit import AccountContext import PhotoResources +import AvatarNode private final class ShapeImageView: UIView { struct Item: Equatable { @@ -88,10 +89,37 @@ private final class ShapeImageView: UIView { } public final class StorySetIndicatorComponent: Component { + public final class Item: Equatable { + public let storyItem: EngineStoryItem + public let peer: EnginePeer + + public init(storyItem: EngineStoryItem, peer: EnginePeer) { + self.storyItem = storyItem + self.peer = peer + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.storyItem != rhs.storyItem { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + var id: String { + return "\(self.peer.id.toInt64())_\(self.storyItem.id)" + } + } + public let context: AccountContext public let strings: PresentationStrings - public let peer: EnginePeer - public let items: [EngineStoryItem] + public let items: [Item] + public let displayAvatars: Bool public let hasUnseen: Bool public let hasUnseenPrivate: Bool public let totalCount: Int @@ -101,8 +129,8 @@ public final class StorySetIndicatorComponent: Component { public init( context: AccountContext, strings: PresentationStrings, - peer: EnginePeer, - items: [EngineStoryItem], + items: [Item], + displayAvatars: Bool, hasUnseen: Bool, hasUnseenPrivate: Bool, totalCount: Int, @@ -111,8 +139,8 @@ public final class StorySetIndicatorComponent: Component { ) { self.context = context self.strings = strings - self.peer = peer self.items = items + self.displayAvatars = displayAvatars self.hasUnseen = hasUnseen self.hasUnseenPrivate = hasUnseenPrivate self.totalCount = totalCount @@ -127,6 +155,9 @@ public final class StorySetIndicatorComponent: Component { if lhs.items != rhs.items { return false } + if lhs.displayAvatars != rhs.displayAvatars { + return false + } if lhs.hasUnseen != rhs.hasUnseen { return false } @@ -149,13 +180,13 @@ public final class StorySetIndicatorComponent: Component { private(set) var image: UIImage? - init(context: AccountContext, peer: EnginePeer, item: EngineStoryItem, updated: @escaping () -> Void) { + init(context: AccountContext, item: StorySetIndicatorComponent.Item, displayAvatars: Bool, updated: @escaping () -> Void) { self.updated = updated - let peerReference = PeerReference(peer._asPeer()) + let peerReference = PeerReference(item.peer._asPeer()) var messageMedia: EngineMedia? - switch item.media { + switch item.storyItem.media { case let .image(image): messageMedia = .image(image) case let .file(file): @@ -167,60 +198,82 @@ public final class StorySetIndicatorComponent: Component { let reloadMedia = true if reloadMedia, let messageMedia, let peerReference { + var imageSignal: Signal? var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var fetchSignal: Signal? - switch messageMedia { - case let .image(image): - signal = chatMessagePhoto( - postbox: context.account.postbox, - userLocation: .peer(peerReference.id), - userContentType: .story, - photoReference: .story(peer: peerReference, id: item.id, media: image), - synchronousLoad: false, - highQuality: true - ) - if let representation = image.representations.last { + + if displayAvatars { + imageSignal = peerAvatarCompleteImage(postbox: context.account.postbox, network: context.account.network, peer: item.peer, forceProvidedRepresentation: false, representation: nil, size: CGSize(width: 26.0, height: 26.0), round: true, font: avatarPlaceholderFont(size: 13.0), drawLetters: true, fullSize: false, blurred: false) + } else { + switch messageMedia { + case let .image(image): + signal = chatMessagePhoto( + postbox: context.account.postbox, + userLocation: .peer(peerReference.id), + userContentType: .story, + photoReference: .story(peer: peerReference, id: item.storyItem.id, media: image), + synchronousLoad: false, + highQuality: true + ) + if let representation = image.representations.last { + fetchSignal = fetchedMediaResource( + mediaBox: context.account.postbox.mediaBox, + userLocation: .peer(peerReference.id), + userContentType: .story, + reference: ImageMediaReference.story(peer: peerReference, id: item.storyItem.id, media: image).resourceReference(representation.resource) + ) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } + case let .file(file): + signal = mediaGridMessageVideo( + postbox: context.account.postbox, + userLocation: .peer(peerReference.id), + userContentType: .story, + videoReference: .story(peer: peerReference, id: item.storyItem.id, media: file), + onlyFullSize: false, + useLargeThumbnail: true, + synchronousLoad: false, + autoFetchFullSizeThumbnail: true, + overlayColor: nil, + nilForEmptyResult: false, + useMiniThumbnailIfAvailable: false, + blurred: false + ) fetchSignal = fetchedMediaResource( mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peerReference.id), userContentType: .story, - reference: ImageMediaReference.story(peer: peerReference, id: item.id, media: image).resourceReference(representation.resource) + reference: FileMediaReference.story(peer: peerReference, id: item.storyItem.id, media: file).resourceReference(file.resource) ) |> ignoreValues |> `catch` { _ -> Signal in return .complete() } + default: + break } - case let .file(file): - signal = mediaGridMessageVideo( - postbox: context.account.postbox, - userLocation: .peer(peerReference.id), - userContentType: .story, - videoReference: .story(peer: peerReference, id: item.id, media: file), - onlyFullSize: false, - useLargeThumbnail: true, - synchronousLoad: false, - autoFetchFullSizeThumbnail: true, - overlayColor: nil, - nilForEmptyResult: false, - useMiniThumbnailIfAvailable: false, - blurred: false - ) - fetchSignal = fetchedMediaResource( - mediaBox: context.account.postbox.mediaBox, - userLocation: .peer(peerReference.id), - userContentType: .story, - reference: FileMediaReference.story(peer: peerReference, id: item.id, media: file).resourceReference(file.resource) - ) - |> ignoreValues - |> `catch` { _ -> Signal in - return .complete() - } - default: - break } - if let signal { + if let imageSignal { + var wasSynchronous = true + self.imageDisposable = (imageSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + if let result { + self.image = result + if !wasSynchronous { + self.updated() + } + } + }) + wasSynchronous = false + } else if let signal { var wasSynchronous = true self.imageDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] process in @@ -261,7 +314,7 @@ public final class StorySetIndicatorComponent: Component { private let imageView: ShapeImageView private let text = ComponentView() - private var imageContexts: [Int32: ImageContext] = [:] + private var imageContexts: [String: ImageContext] = [:] private var component: StorySetIndicatorComponent? private weak var state: EmptyComponentState? @@ -318,7 +371,7 @@ public final class StorySetIndicatorComponent: Component { let outerDiameter: CGFloat = innerDiameter + innerSpacing * 2.0 + lineWidth * 2.0 let overflow: CGFloat = 14.0 - var validIds: [Int32] = [] + var validIds: [String] = [] var items: [ShapeImageView.Item] = [] for i in 0 ..< min(3, component.items.count) { validIds.append(component.items[i].id) @@ -328,7 +381,7 @@ public final class StorySetIndicatorComponent: Component { imageContext = current } else { var update = false - imageContext = ImageContext(context: component.context, peer: component.peer, item: component.items[i], updated: { [weak self] in + imageContext = ImageContext(context: component.context, item: component.items[i], displayAvatars: component.displayAvatars, updated: { [weak self] in guard let self else { return } @@ -347,7 +400,7 @@ public final class StorySetIndicatorComponent: Component { )) } - var removeIds: [Int32] = [] + var removeIds: [String] = [] for (id, _) in self.imageContexts { if !validIds.contains(id) { removeIds.append(id) @@ -407,7 +460,12 @@ public final class StorySetIndicatorComponent: Component { textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) } - let size = CGSize(width: effectiveItemsWidth + 6.0 + textSize.width, height: outerDiameter) + var width = effectiveItemsWidth + if textSize.width > 0.0 { + width += textSize.width + 6.0 + } + + let size = CGSize(width: width, height: outerDiameter) transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) return size diff --git a/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json new file mode 100644 index 0000000000..297ddbe4b0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf new file mode 100644 index 0000000000..a0e02c51e2 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift new file mode 100644 index 0000000000..3eba806368 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenBankCardContextMenu.swift @@ -0,0 +1,103 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction + +extension ChatControllerImpl { + func openBankCardContextMenu(number: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let _ = (self.context.engine.payments.getBankCardInfo(cardNumber: number) + |> deliverOnMainQueue).start(next: { [weak self] info in + guard let self else { + return + } + params.progress?.set(.single(false)) + + var items: [ContextMenuItem] = [] + + if let info { + for url in info.urls { + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_AddToContacts, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + self.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message)) + })) + ) + } + + if !items.isEmpty { + items.append(.separator) + } + } + + items.append( + .action(ContextMenuActionItem(text: "Copy Card Number", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = number + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_CardNumberCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + if let info { + items.append(.separator) + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append(.action(ContextMenuActionItem(text: info.title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift new file mode 100644 index 0000000000..0d9b72f0d5 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenHashtagContextMenu.swift @@ -0,0 +1,79 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import ChatControllerInteraction + +extension ChatControllerImpl { + func openHashtagContextMenu(hashtag: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: "Search", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Search"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + self.controllerInteraction?.openHashtag(nil, hashtag) + })) + ) + + items.append( + .action(ContextMenuActionItem(text: "Copy Hashtag", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = hashtag + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_HashtagCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift new file mode 100644 index 0000000000..46a627c0c3 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -0,0 +1,122 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import MessageUI +import ChatControllerInteraction +import UrlWhitelist +import OpenInExternalAppUI +import SafariServices + +extension ChatControllerImpl { + func openLinkContextMenu(url: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) + var canAddToReadingList = true + let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url)).count > 1 + + let mailtoString = "mailto:" + var openText = self.presentationData.strings.Conversation_LinkDialogOpen + + if cleanUrl.hasPrefix(mailtoString) { + canAddToReadingList = false + cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) +// isEmail = true + } else if canOpenIn { + openText = self.presentationData.strings.Conversation_FileOpenIn + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: openText, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + guard let self else { + return + } + f(.default) + + if canOpenIn { + self.openUrlIn(url) + } + else { + self.openUrl(url, concealed: false) + } + })) + ) + + if canAddToReadingList { + items.append( + .action(ContextMenuActionItem(text: "Add to Reading List", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + + if let link = URL(string: url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + })) + ) +// / items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in +// // actionSheet?.dismissAnimated() +// // if let link = URL(string: url) { +// // let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) +// // } +// // })) + } + + items.append( + .action(ContextMenuActionItem(text: "Copy Link", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = url + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift new file mode 100644 index 0000000000..dd8a2b1d18 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkLongTap.swift @@ -0,0 +1,402 @@ +import Foundation +import Display +import ChatControllerInteraction +import AccountContext + +extension ChatControllerImpl { + func openLinkLongTap(_ action: ChatControllerInteractionLongTapAction, params: ChatControllerInteraction.LongTapParams?) { + if self.presentationInterfaceState.interfaceState.selectionState != nil { + return + } + + self.dismissAllTooltips() + + (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.cancelInteractiveKeyboardGestures() + self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() + + guard let params else { + return + } + switch action { + case let .url(url): + self.openLinkContextMenu(url: url, params: params) + case let .mention(mention): + self.openMentionContextMenu(username: mention, peerId: nil, params: params) + case let .peerMention(peerId, mention): + self.openMentionContextMenu(username: mention, peerId: peerId, params: params) + case let .command(command): + let _ = command + break +// self.openBotCommandContextMenu(command: command, params: params) + case let .hashtag(hashtag): + self.openHashtagContextMenu(hashtag: hashtag, params: params) + case let .timecode(value, timecode): + let _ = value + let _ = timecode + break +// self.openTimecodeContextMenu(timecode: timecode, params: params) + case let .bankCard(number): + self.openBankCardContextMenu(number: number, params: params) + case let .phone(number): + self.openPhoneContextMenu(number: number, params: params) + } + } +} + +//if let strongSelf = self { +// let presentationData = strongSelf.presentationData +// switch action { +// case let .url(url): +// var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) +// var canAddToReadingList = true +// var canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 +// let mailtoString = "mailto:" +// let telString = "tel:" +// var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen +// var phoneNumber: String? +// +// var isPhoneNumber = false +// var isEmail = false +// var hasOpenAction = true +// +// if cleanUrl.hasPrefix(mailtoString) { +// canAddToReadingList = false +// cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) +// isEmail = true +// } else if cleanUrl.hasPrefix(telString) { +// canAddToReadingList = false +// phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) +// cleanUrl = phoneNumber! +// openText = strongSelf.presentationData.strings.UserInfo_PhoneCall +// canOpenIn = false +// isPhoneNumber = true +// +// if cleanUrl.hasPrefix("+888") { +// hasOpenAction = false +// } +// } else if canOpenIn { +// openText = strongSelf.presentationData.strings.Conversation_FileOpenIn +// } +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// +// var items: [ActionSheetItem] = [] +// items.append(ActionSheetTextItem(title: cleanUrl)) +// if hasOpenAction { +// items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// if canOpenIn { +// strongSelf.openUrlIn(url) +// } else { +// strongSelf.openUrl(url, concealed: false) +// } +// } +// })) +// } +// if let phoneNumber = phoneNumber { +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.controllerInteraction?.addContact(phoneNumber) +// } +// })) +// } +// items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = cleanUrl +// +// let content: UndoOverlayContent +// if isPhoneNumber { +// content = .copy(text: presentationData.strings.Conversation_PhoneCopied) +// } else if isEmail { +// content = .copy(text: presentationData.strings.Conversation_EmailCopied) +// } else if canAddToReadingList { +// content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) +// } else { +// content = .copy(text: presentationData.strings.Conversation_TextCopied) +// } +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// })) +// if canAddToReadingList { +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let link = URL(string: url) { +// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) +// } +// })) +// } +// actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .peerMention(peerId, mention): +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// var items: [ActionSheetItem] = [] +// if !mention.isEmpty { +// items.append(ActionSheetTextItem(title: mention)) +// } +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) +// |> deliverOnMainQueue).startStandalone(next: { peer in +// if let strongSelf = self, let peer = peer { +// strongSelf.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) +// } +// }) +// } +// })) +// if !mention.isEmpty { +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = mention +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// })) +// } +// actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .mention(mention): +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ +// ActionSheetTextItem(title: mention), +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.openPeerMention(mention, sourceMessageId: message?.id) +// } +// }), +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = mention +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// }) +// ]), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .command(command): +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// var items: [ActionSheetItem] = [] +// items.append(ActionSheetTextItem(title: command)) +// if canSendMessagesToChat(strongSelf.presentationInterfaceState) { +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.sendMessages([.message(text: command, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) +// } +// })) +// } +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = command +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// })) +// actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .hashtag(hashtag): +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ +// ActionSheetTextItem(title: hashtag), +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.openHashtag(hashtag, peerName: nil) +// } +// }), +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = hashtag +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_HashtagCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// }) +// ]), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .timecode(timecode, text): +// guard let message = message else { +// return +// } +// +// let context = strongSelf.context +// let chatPresentationInterfaceState = strongSelf.presentationInterfaceState +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// +// var isCopyLink = false +// var isForward = false +// if message.id.namespace == Namespaces.Message.Cloud, let _ = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) { +// isCopyLink = true +// } else if let forwardInfo = message.forwardInfo, let _ = forwardInfo.author as? TelegramChannel { +// isCopyLink = true +// isForward = true +// } +// +// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ +// ActionSheetTextItem(title: text), +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.controllerInteraction?.seekToTimecode(message, timecode, true) +// } +// }), +// ActionSheetButtonItem(title: isCopyLink ? strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink : strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// +// var messageId = message.id +// var channel = message.peers[message.id.peerId] +// if isForward, let forwardMessageId = message.forwardInfo?.sourceMessageId, let forwardAuthor = message.forwardInfo?.author as? TelegramChannel { +// messageId = forwardMessageId +// channel = forwardAuthor +// } +// +// if isCopyLink, let channel = channel as? TelegramChannel { +// var threadId: Int64? +// +// if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { +// threadId = replyThreadMessage.threadId +// } +// let _ = (context.engine.messages.exportMessageLink(peerId: messageId.peerId, messageId: messageId, isThread: threadId != nil) +// |> map { result -> String? in +// return result +// } +// |> deliverOnMainQueue).startStandalone(next: { link in +// if let link = link { +// UIPasteboard.general.string = link + "?t=\(Int32(timecode))" +// +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// +// var warnAboutPrivate = false +// if case .peer = chatPresentationInterfaceState.chatLocation { +// if channel.addressName == nil { +// warnAboutPrivate = true +// } +// } +// Queue.mainQueue().after(0.2, { +// let content: UndoOverlayContent +// if warnAboutPrivate { +// content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) +// } else { +// content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) +// } +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// }) +// } else { +// UIPasteboard.general.string = text +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// } +// }) +// } else { +// UIPasteboard.general.string = text +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// } +// }) +// ]), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.chatDisplayNode.dismissInput() +// strongSelf.present(actionSheet, in: .window(.root)) +// case let .bankCard(number): +// guard let message = message else { +// return +// } +// +// var signal = strongSelf.context.engine.payments.getBankCardInfo(cardNumber: number) +// let disposable: MetaDisposable +// if let current = strongSelf.bankCardDisposable { +// disposable = current +// } else { +// disposable = MetaDisposable() +// strongSelf.bankCardDisposable = disposable +// } +// +// var cancelImpl: (() -> Void)? +// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } +// let progressSignal = Signal { subscriber in +// let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { +// cancelImpl?() +// })) +// strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) +// return ActionDisposable { [weak controller] in +// Queue.mainQueue().async() { +// controller?.dismiss() +// } +// } +// } +// |> runOn(Queue.mainQueue()) +// |> delay(0.15, queue: Queue.mainQueue()) +// let progressDisposable = progressSignal.startStrict() +// +// signal = signal +// |> afterDisposed { +// Queue.mainQueue().async { +// progressDisposable.dispose() +// } +// } +// cancelImpl = { +// disposable.set(nil) +// } +// disposable.set((signal +// |> deliverOnMainQueue).startStrict(next: { [weak self] info in +// if let strongSelf = self, let info = info { +// let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) +// var items: [ActionSheetItem] = [] +// items.append(ActionSheetTextItem(title: info.title)) +// for url in info.urls { +// items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// if let strongSelf = self { +// strongSelf.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message)) +// } +// })) +// } +// items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// UIPasteboard.general.string = number +// +// let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_CardNumberCopied) +// self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) +// })) +// actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// strongSelf.present(actionSheet, in: .window(.root)) +// } +// })) +// +// strongSelf.chatDisplayNode.dismissInput() +// } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift index b332e0a4d0..2506707179 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenPhoneContextMenu.swift @@ -14,38 +14,45 @@ import AvatarNode import UndoUI import MessageUI import PeerInfoUI +import ChatControllerInteraction extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { - func openPhoneContextMenu(number: String, peer: EnginePeer?, message: Message, contentNode: ContextExtractedContentContainingNode, messageNode: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void { - if self.presentationInterfaceState.interfaceState.selectionState != nil { + func openPhoneContextMenu(number: String, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { return } - - self.dismissAllTooltips() - let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer - let gesture: ContextGesture? = anyRecognizer as? ContextGesture + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } - if let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) { - (self.view.window as? WindowHost)?.cancelInteractiveKeyboardGestures() - self.chatDisplayNode.cancelInteractiveKeyboardGestures() - var updatedMessages = messages - for i in 0 ..< updatedMessages.count { - if updatedMessages[i].id == message.id { - let message = updatedMessages.remove(at: i) - updatedMessages.insert(message, at: 0) - break - } + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break } - - self.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts() - - let source: ContextContentSource - if let location = location { - source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) - } else { - source = .extracted(ChatMessagePhoneContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let _ = (self.context.engine.peers.resolvePeerByPhone(phone: number) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return } + params.progress?.set(.single(false)) let phoneNumber: String if let peer, case let .user(user) = peer, let phone = user.phone { @@ -182,7 +189,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { } self.window?.presentInGlobalOverlay(controller) - } + }) } private func inviteToTelegram(numbers: [String]) { @@ -207,7 +214,7 @@ extension ChatControllerImpl: MFMessageComposeViewControllerDelegate { } } -private final class ChatMessagePhoneContextExtractedContentSource: ContextExtractedContentSource { +final class ChatMessageLinkContextExtractedContentSource: ContextExtractedContentSource { let keepInPlace: Bool = false let ignoreContentTouches: Bool = true let blurBackground: Bool = true diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift new file mode 100644 index 0000000000..7d5a25f414 --- /dev/null +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenUsernameContextMenu.swift @@ -0,0 +1,134 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AsyncDisplayKit +import Display +import ContextUI +import UndoUI +import AccountContext +import ChatMessageItemView +import ChatMessageItemCommon +import AvatarNode +import ChatControllerInteraction + +extension ChatControllerImpl { + func openMentionContextMenu(username: String, peerId: EnginePeer.Id?, params: ChatControllerInteraction.LongTapParams) -> Void { + guard let message = params.message, let contentNode = params.contentNode else { + return + } + + guard let messages = self.chatDisplayNode.historyNode.messageGroupInCurrentHistoryView(message.id) else { + return + } + + var updatedMessages = messages + for i in 0 ..< updatedMessages.count { + if updatedMessages[i].id == message.id { + let message = updatedMessages.remove(at: i) + updatedMessages.insert(message, at: 0) + break + } + } + + let recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil// anyRecognizer as? TapLongTapOrDoubleTapGestureRecognizer + let gesture: ContextGesture? = nil // anyRecognizer as? ContextGesture + + let source: ContextContentSource +// if let location = location { +// source = .location(ChatMessageContextLocationContentSource(controller: self, location: messageNode.view.convert(messageNode.bounds, to: nil).origin.offsetBy(dx: location.x, dy: location.y))) +// } else { + source = .extracted(ChatMessageLinkContextExtractedContentSource(chatNode: self.chatDisplayNode, contentNode: contentNode)) +// } + + params.progress?.set(.single(true)) + + let peer: Signal + if let peerId { + peer = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) + } else { + peer = self.context.engine.peers.resolvePeerByName(name: username) + |> mapToSignal { value in + switch value { + case .progress: + return .complete() + case let .result(result): + return .single(result) + } + } + } + + let _ = (peer + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self else { + return + } + params.progress?.set(.single(false)) + + var items: [ContextMenuItem] = [] + if let peer { + items.append( + .action(ContextMenuActionItem(text: self.presentationData.strings.Chat_Context_Phone_SendMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) + })) + ) + } + + items.append( + .action(ContextMenuActionItem(text: "Copy Username", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + + UIPasteboard.general.string = username + + self.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: presentationData.strings.Conversation_UsernameCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + })) + ) + + items.append(.separator) + if let peer { + let avatarSize = CGSize(width: 28.0, height: 28.0) + let avatarSignal = peerAvatarCompleteImage(account: self.context.account, peer: peer, size: avatarSize) + + let subtitle = NSMutableAttributedString(string: self.presentationData.strings.Chat_Context_Phone_ViewProfile + " >") + if let range = subtitle.string.range(of: ">"), let arrowImage = UIImage(bundleImageName: "Item List/InlineTextRightArrow") { + subtitle.addAttribute(.attachment, value: arrowImage, range: NSRange(range, in: subtitle.string)) + subtitle.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: subtitle.string)) + } + + items.append( + .action(ContextMenuActionItem(text: peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), textLayout: .secondLineWithAttributedValue(subtitle), icon: { theme in return nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: avatarSignal), iconPosition: .left, action: { [weak self] _, f in + f(.default) + + guard let self else { + return + } + self.openPeer(peer: peer, navigation: .info(ChatControllerInteractionNavigateToPeer.InfoParams(ignoreInSavedMessages: true)), fromMessage: nil) + })) + ) + } else { + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append( + .action(ContextMenuActionItem(text: "This user doesn't exist on Telegram.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)) + ) + } + + self.canReadHistory.set(false) + + let controller = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))), recognizer: recognizer, gesture: gesture, disableScreenshots: false) + controller.dismissed = { [weak self] in + self?.canReadHistory.set(true) + } + + self.window?.presentInGlobalOverlay(controller) + }) + } +} diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift index e755d19825..2a26689216 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageActionOptions.swift @@ -19,6 +19,7 @@ import TelegramNotices import ChatMessageWebpageBubbleContentNode import PremiumUI import UndoUI +import WebsiteType private enum OptionsId: Hashable { case reply diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7fc2521879..70c122ace2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2367,7 +2367,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> deliverOnMainQueue).startStandalone(next: { coordinate in if let strongSelf = self { if let coordinate = coordinate { - strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + strongSelf.sendMessages([.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) } else { strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {})]), in: .window(.root)) } @@ -2539,363 +2539,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) }) } - }, longTap: { [weak self] action, message in - if let strongSelf = self { - let presentationData = strongSelf.presentationData - switch action { - case let .url(url): - var (cleanUrl, _) = parseUrl(url: url, wasConcealed: false) - var canAddToReadingList = true - var canOpenIn = availableOpenInOptions(context: strongSelf.context, item: .url(url: url)).count > 1 - let mailtoString = "mailto:" - let telString = "tel:" - var openText = strongSelf.presentationData.strings.Conversation_LinkDialogOpen - var phoneNumber: String? - - var isPhoneNumber = false - var isEmail = false - var hasOpenAction = true - - if cleanUrl.hasPrefix(mailtoString) { - canAddToReadingList = false - cleanUrl = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]) - isEmail = true - } else if cleanUrl.hasPrefix(telString) { - canAddToReadingList = false - phoneNumber = String(cleanUrl[cleanUrl.index(cleanUrl.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]) - cleanUrl = phoneNumber! - openText = strongSelf.presentationData.strings.UserInfo_PhoneCall - canOpenIn = false - isPhoneNumber = true - - if cleanUrl.hasPrefix("+888") { - hasOpenAction = false - } - } else if canOpenIn { - openText = strongSelf.presentationData.strings.Conversation_FileOpenIn - } - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: cleanUrl)) - if hasOpenAction { - items.append(ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - if canOpenIn { - strongSelf.openUrlIn(url) - } else { - strongSelf.openUrl(url, concealed: false) - } - } - })) - } - if let phoneNumber = phoneNumber { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddContact, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.addContact(phoneNumber) - } - })) - } - items.append(ActionSheetButtonItem(title: canAddToReadingList ? strongSelf.presentationData.strings.ShareMenu_CopyShareLink : strongSelf.presentationData.strings.Conversation_ContextMenuCopy, color: .accent, action: { [weak actionSheet, weak self] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = cleanUrl - - let content: UndoOverlayContent - if isPhoneNumber { - content = .copy(text: presentationData.strings.Conversation_PhoneCopied) - } else if isEmail { - content = .copy(text: presentationData.strings.Conversation_EmailCopied) - } else if canAddToReadingList { - content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) - } else { - content = .copy(text: presentationData.strings.Conversation_TextCopied) - } - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - if canAddToReadingList { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let link = URL(string: url) { - let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) - } - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .peerMention(peerId, mention): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - if !mention.isEmpty { - items.append(ActionSheetTextItem(title: mention)) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) - |> deliverOnMainQueue).startStandalone(next: { peer in - if let strongSelf = self, let peer = peer { - strongSelf.openPeer(peer: peer, navigation: .chat(textInputState: nil, subject: nil, peekData: nil), fromMessage: nil) - } - }) - } - })) - if !mention.isEmpty { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = mention - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .mention(mention): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: mention), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.openPeerMention(mention, sourceMessageId: message?.id) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = mention - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_UsernameCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .command(command): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: command)) - if canSendMessagesToChat(strongSelf.presentationInterfaceState) { - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ShareMenu_Send, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.sendMessages([.message(text: command, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: strongSelf.chatLocation.threadId, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = command - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .hashtag(hashtag): - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: hashtag), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.openHashtag(hashtag, peerName: nil) - } - }), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = hashtag - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_HashtagCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .timecode(timecode, text): - guard let message = message else { - return - } - - let context = strongSelf.context - let chatPresentationInterfaceState = strongSelf.presentationInterfaceState - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - - var isCopyLink = false - var isForward = false - if message.id.namespace == Namespaces.Message.Cloud, let _ = message.peers[message.id.peerId] as? TelegramChannel, !(message.media.first is TelegramMediaAction) { - isCopyLink = true - } else if let forwardInfo = message.forwardInfo, let _ = forwardInfo.author as? TelegramChannel { - isCopyLink = true - isForward = true - } - - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetTextItem(title: text), - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.seekToTimecode(message, timecode, true) - } - }), - ActionSheetButtonItem(title: isCopyLink ? strongSelf.presentationData.strings.Conversation_ContextMenuCopyLink : strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - - var messageId = message.id - var channel = message.peers[message.id.peerId] - if isForward, let forwardMessageId = message.forwardInfo?.sourceMessageId, let forwardAuthor = message.forwardInfo?.author as? TelegramChannel { - messageId = forwardMessageId - channel = forwardAuthor - } - - if isCopyLink, let channel = channel as? TelegramChannel { - var threadId: Int64? - - if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { - threadId = replyThreadMessage.threadId - } - let _ = (context.engine.messages.exportMessageLink(peerId: messageId.peerId, messageId: messageId, isThread: threadId != nil) - |> map { result -> String? in - return result - } - |> deliverOnMainQueue).startStandalone(next: { link in - if let link = link { - UIPasteboard.general.string = link + "?t=\(Int32(timecode))" - - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - - var warnAboutPrivate = false - if case .peer = chatPresentationInterfaceState.chatLocation { - if channel.addressName == nil { - warnAboutPrivate = true - } - } - Queue.mainQueue().after(0.2, { - let content: UndoOverlayContent - if warnAboutPrivate { - content = .linkCopied(text: presentationData.strings.Conversation_PrivateMessageLinkCopiedLong) - } else { - content = .linkCopied(text: presentationData.strings.Conversation_LinkCopied) - } - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - }) - } else { - UIPasteboard.general.string = text - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - }) - } else { - UIPasteboard.general.string = text - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_TextCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - case let .bankCard(number): - guard let message = message else { - return - } - - var signal = strongSelf.context.engine.payments.getBankCardInfo(cardNumber: number) - let disposable: MetaDisposable - if let current = strongSelf.bankCardDisposable { - disposable = current - } else { - disposable = MetaDisposable() - strongSelf.bankCardDisposable = disposable - } - - var cancelImpl: (() -> Void)? - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { - cancelImpl?() - })) - strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - return ActionDisposable { [weak controller] in - Queue.mainQueue().async() { - controller?.dismiss() - } - } - } - |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) - let progressDisposable = progressSignal.startStrict() - - signal = signal - |> afterDisposed { - Queue.mainQueue().async { - progressDisposable.dispose() - } - } - cancelImpl = { - disposable.set(nil) - } - disposable.set((signal - |> deliverOnMainQueue).startStrict(next: { [weak self] info in - if let strongSelf = self, let info = info { - let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: info.title)) - for url in info.urls { - items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - if let strongSelf = self { - strongSelf.controllerInteraction?.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: false, external: false, message: message)) - } - })) - } - items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - UIPasteboard.general.string = number - - let content: UndoOverlayContent = .copy(text: presentationData.strings.Conversation_CardNumberCopied) - self?.present(UndoOverlayController(presentationData: presentationData, content: content, elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.present(actionSheet, in: .window(.root)) - } - })) - - strongSelf.chatDisplayNode.dismissInput() - } + }, longTap: { [weak self] action, params in + if let self { + self.openLinkLongTap(action, params: params) } }, openCheckoutOrReceipt: { [weak self] messageId in guard let strongSelf = self else { @@ -4707,21 +4353,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } self.openStickerEditor() - }, openPhoneContextMenu: { [weak self] phoneData in - guard let self else { - return - } - phoneData.progress?.set(.single(true)) - - let _ = (self.context.engine.peers.resolvePeerByPhone(phone: phoneData.number) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self else { - return - } - phoneData.progress?.set(.single(false)) - - self.openPhoneContextMenu(number: phoneData.number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) - }) }, openAgeRestrictedMessageMedia: { [weak self] message, reveal in guard let self else { return diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index da9dab3e5f..b489cc44de 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -206,7 +206,7 @@ extension ListMessageItemInteraction { }, openInstantPage: { message, data in controllerInteraction.openInstantPage(message, data) }, longTap: { action, message in - controllerInteraction.longTap(action, message) + controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message)) }, getHiddenMedia: { return controllerInteraction.hiddenMedia }) diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 116f1af799..3dfc566c2b 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -175,7 +175,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 3501a5e786..87f8121e03 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1775,7 +1775,6 @@ public final class SharedAccountContextImpl: SharedAccountContext { }, openRecommendedChannelContextMenu: { _, _, _ in }, openGroupBoostInfo: { _, _ in }, openStickerEditor: { - }, openPhoneContextMenu: { _ in }, openAgeRestrictedMessageMedia: { _, _ in }, playMessageEffect: { _ in }, editMessageFactCheck: { _ in @@ -1908,8 +1907,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return HashtagSearchController(context: context, peer: peer, query: query, all: all) } - public func makeStorySearchController(context: AccountContext, query: String) -> ViewController { - return StorySearchGridScreen(context: context, searchQuery: query) + public func makeStorySearchController(context: AccountContext, query: String, listContext: SearchStoryListContext?) -> ViewController { + return StorySearchGridScreen(context: context, searchQuery: query, listContext: listContext) } public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController { @@ -2638,6 +2637,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController { return StarsTransactionScreen(context: context, subject: .receipt(receipt), action: {}) } + + public func makeStarsStatisticsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController { + return StarsStatisticsScreen(context: context, starsContext: starsContext) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift index 630b6b118a..75b0164c34 100644 --- a/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/WebpagePreviewAccessoryPanelNode.swift @@ -12,8 +12,6 @@ import AccessoryPanelNode import AppBundle final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { - private let webpageDisposable = MetaDisposable() - private(set) var webpage: TelegramMediaWebpage private(set) var url: String @@ -69,11 +67,7 @@ final class WebpagePreviewAccessoryPanelNode: AccessoryPanelNode { self.updateWebpage() } - - deinit { - self.webpageDisposable.dispose() - } - + override func animateIn() { self.iconView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2) } diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index e4a7ce0c92..5c986acfb2 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -202,7 +202,7 @@ final class WatchSendMessageHandler: WatchRequestHandler { messageSignal = .single((.message(text: args.text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: replyMessageId.flatMap { EngineMessageReplySubject(messageId: $0, quote: nil) }, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) - let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) messageSignal = .single((.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: map), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []), peerId)) } else if let args = subscription as? TGBridgeSendStickerMessageSubscription { let peerId = makePeerIdFromBridgeIdentifier(args.peerId) diff --git a/submodules/WebsiteType/BUILD b/submodules/WebsiteType/BUILD index 27297a6dcf..4b08a4a695 100644 --- a/submodules/WebsiteType/BUILD +++ b/submodules/WebsiteType/BUILD @@ -10,7 +10,8 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/TelegramCore:TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/WebsiteType/Sources/WebsiteType.swift b/submodules/WebsiteType/Sources/WebsiteType.swift index a8bf0ad6c1..69407581d2 100644 --- a/submodules/WebsiteType/Sources/WebsiteType.swift +++ b/submodules/WebsiteType/Sources/WebsiteType.swift @@ -1,4 +1,5 @@ import Foundation +import Postbox import TelegramCore public enum WebsiteType { @@ -35,3 +36,29 @@ public func instantPageType(of webpage: TelegramMediaWebpageLoadedContent) -> In return .generic } } + +public func defaultWebpageImageSizeIsSmall(webpage: TelegramMediaWebpageLoadedContent) -> Bool { + let type = websiteType(of: webpage.websiteName) + + let mainMedia: Media? + switch type { + case .instagram, .twitter: + mainMedia = webpage.story ?? webpage.image ?? webpage.file + default: + mainMedia = webpage.story ?? webpage.file ?? webpage.image + } + + if let image = mainMedia as? TelegramMediaImage { + if let type = webpage.type, (["photo", "video", "embed", "gif", "document", "telegram_album"] as [String]).contains(type) { + } else if let type = webpage.type, (["article"] as [String]).contains(type) { + return true + } else if let _ = largestImageRepresentation(image.representations)?.dimensions { + if webpage.instantPage == nil { + return true + } + } + } + + return false +} +