Various improvements

This commit is contained in:
Ilya Laktyushin 2024-06-07 09:51:45 +04:00
parent e54230f42c
commit 5470ba80ec
97 changed files with 4465 additions and 1180 deletions

View File

@ -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 %@";

View File

@ -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?

View File

@ -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)

View File

@ -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]
}

View File

@ -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 {

View File

@ -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

View File

@ -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()

View File

@ -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
}

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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));

View File

@ -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)
{

View File

@ -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)

View File

@ -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));
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)))

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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:

View File

@ -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

View File

@ -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() {

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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))

View File

@ -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
}
}

View File

@ -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")

View File

@ -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

View File

@ -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
})

View File

@ -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
)

View File

@ -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, _):

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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?

View File

@ -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

View File

@ -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))

View File

@ -483,7 +483,6 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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()

View File

@ -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",

View File

@ -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() {
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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 })

View File

@ -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

View File

@ -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(

View File

@ -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()
}
}

View File

@ -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

View File

@ -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)"
}

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "cash_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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)
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
// }

View File

@ -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

View File

@ -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)
})
}
}

View File

@ -19,6 +19,7 @@ import TelegramNotices
import ChatMessageWebpageBubbleContentNode
import PremiumUI
import UndoUI
import WebsiteType
private enum OptionsId: Hashable {
case reply

View File

@ -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

View File

@ -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
})

View File

@ -175,7 +175,6 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, openRecommendedChannelContextMenu: { _, _, _ in
}, openGroupBoostInfo: { _, _ in
}, openStickerEditor: {
}, openPhoneContextMenu: { _ in
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in

View File

@ -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? {

View File

@ -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)
}

View File

@ -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)

View File

@ -10,7 +10,8 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox",
"//submodules/TelegramCore",
],
visibility = [
"//visibility:public",

View File

@ -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
}