mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Various improvements
This commit is contained in:
parent
e54230f42c
commit
5470ba80ec
@ -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 %@";
|
||||
|
@ -970,7 +970,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, 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?
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<String>()
|
||||
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<Empty>?
|
||||
|
||||
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<Empty>
|
||||
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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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<EnginePeer.Id>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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<Api.stories.FoundStories>) {
|
||||
static func searchPosts(flags: Int32, hashtag: String?, area: Api.MediaArea?, offset: String, limit: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.FoundStories>) {
|
||||
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() {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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, _):
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<Bool>?)? {
|
||||
return { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let promise = Promise<Bool>()
|
||||
|
||||
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<Bool>()
|
||||
|
||||
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<Bool>()
|
||||
|
||||
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 {
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -483,7 +483,6 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
|
||||
}, openRecommendedChannelContextMenu: { _, _, _ in
|
||||
}, openGroupBoostInfo: { _, _ in
|
||||
}, openStickerEditor: {
|
||||
}, openPhoneContextMenu: { _ in
|
||||
}, openAgeRestrictedMessageMedia: { _, _ in
|
||||
}, playMessageEffect: { _ in
|
||||
}, editMessageFactCheck: { _ in
|
||||
|
@ -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<Bool>?
|
||||
|
||||
public init(number: String, message: Message, contentNode: ContextExtractedContentContainingNode, messageNode: ASDisplayNode, progress: Promise<Bool>? = nil) {
|
||||
self.number = number
|
||||
public init(message: Message?, contentNode: ContextExtractedContentContainingNode? = nil, messageNode: ASDisplayNode? = nil, progress: Promise<Bool>? = 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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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<ChatPresentationInterfaceState.UrlPreview>(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() {
|
||||
}
|
||||
}
|
@ -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<Empty>] = []
|
||||
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<String>()
|
||||
|
||||
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<Void>.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<EnvironmentType>(
|
||||
content: AnyComponent<EnvironmentType>(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.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<Empty>
|
||||
private let textField: TextFieldNodeView
|
||||
|
||||
private var component: LinkFieldComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.placeholderView = ComponentView<Empty>()
|
||||
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<EnvironmentType>, 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<EnvironmentType>, 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<EnvironmentType>, 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<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -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<TelegramMediaFile?, NoError>
|
||||
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<TelegramMediaFile?, NoError>
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 })
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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<Empty>()
|
||||
private let headerOffsetContainer: UIView
|
||||
|
||||
private let scrollContainerView: UIView
|
||||
|
||||
private let overscroll = ComponentView<Empty>()
|
||||
private let fade = ComponentView<Empty>()
|
||||
private let starView = ComponentView<Empty>()
|
||||
private let titleView = ComponentView<Empty>()
|
||||
private let descriptionView = ComponentView<Empty>()
|
||||
|
||||
private let balanceView = ComponentView<Empty>()
|
||||
|
||||
private let topBalanceTitleView = ComponentView<Empty>()
|
||||
private let topBalanceValueView = ComponentView<Empty>()
|
||||
private let topBalanceIconView = ComponentView<Empty>()
|
||||
|
||||
private let panelContainer = ComponentView<StarsTransactionsPanelContainerEnvironment>()
|
||||
|
||||
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<CGPoint>) {
|
||||
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<ViewControllerComponentContainer.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<ViewControllerComponentContainer.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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)"
|
||||
}
|
@ -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<StickerPickerInput, NoError>, 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(
|
||||
|
@ -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
|
||||
|
@ -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<UIImage?, NoError>?
|
||||
var signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
||||
|
||||
var fetchSignal: Signal<Never, NoError>?
|
||||
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<Never, NoError> 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<Never, NoError> 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<Never, NoError> 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<Empty>()
|
||||
|
||||
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
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "cash_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Components/Search Bar/Cashtag.imageset/cash_24.pdf
vendored
Normal file
Binary file not shown.
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<Never, NoError> { 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()
|
||||
// }
|
@ -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
|
||||
|
@ -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<EnginePeer?, NoError>
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import TelegramNotices
|
||||
import ChatMessageWebpageBubbleContentNode
|
||||
import PremiumUI
|
||||
import UndoUI
|
||||
import WebsiteType
|
||||
|
||||
private enum OptionsId: Hashable {
|
||||
case reply
|
||||
|
@ -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<Never, NoError> { 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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -175,7 +175,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
|
||||
}, openRecommendedChannelContextMenu: { _, _, _ in
|
||||
}, openGroupBoostInfo: { _, _ in
|
||||
}, openStickerEditor: {
|
||||
}, openPhoneContextMenu: { _ in
|
||||
}, openAgeRestrictedMessageMedia: { _, _ in
|
||||
}, playMessageEffect: { _ in
|
||||
}, editMessageFactCheck: { _ in
|
||||
|
@ -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<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -10,7 +10,8 @@ swift_library(
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user