diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 93c61932a1..130afaee33 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11595,4 +11595,14 @@ Sorry for the inconvenience."; "Chat.QuickReplyMediaMessageLimitReachedText_1" = "There can be at most %d message in this chat."; "Chat.QuickReplyMediaMessageLimitReachedText_any" = "There can be at most %d messages in this chat."; +"CollectibleItemInfo.StoreName" = "Fragment"; +"CollectibleItemInfo.UsernameTitle" = "%@ is a collectible username that belongs to"; +"CollectibleItemInfo.UsernameText" = "The %1$@ username was acquired on %2$@ on %3$@ for %4$@ (%5$@)."; +"CollectibleItemInfo.PhoneTitle" = "%@ is a collectible phone number that belongs to"; +"CollectibleItemInfo.PhoneText" = "The %1$@ phone number was acquired on %2$@ on %3$@ for %4$@ (%5$@)."; +"CollectibleItemInfo.ButtonOpenInfo" = "Learn More"; +"CollectibleItemInfo.ButtonCopyUsername" = "Copy Link"; +"CollectibleItemInfo.ButtonCopyPhone" = "Copy Phone Number"; +"CollectibleItemInfo.ShareInlineText.LearnMore" = "Learn more >"; + "Stickers.Edit" = "EDIT"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 6dd57dce8d..8b6d0a1825 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -101,7 +101,7 @@ public enum TextLinkItemActionType { case longTap } -public enum TextLinkItem { +public enum TextLinkItem: Equatable { case url(url: String, concealed: Bool) case mention(String) case hashtag(String?, String) @@ -856,6 +856,15 @@ public protocol AutomaticBusinessMessageSetupScreenInitialData: AnyObject { public protocol ChatbotSetupScreenInitialData: AnyObject { } +public protocol CollectibleItemInfoScreenInitialData: AnyObject { + var collectibleItemInfo: TelegramCollectibleItemInfo { get } +} + +public enum CollectibleItemInfoScreenSubject { + case phoneNumber(String) + case username(String) +} + public protocol SharedAccountContext: AnyObject { var sharedContainerPath: String { get } var basePath: String { get } @@ -951,6 +960,8 @@ public protocol SharedAccountContext: AnyObject { func makeAutomaticBusinessMessageSetupScreenInitialData(context: AccountContext) -> Signal func makeQuickReplySetupScreen(context: AccountContext, initialData: QuickReplySetupScreenInitialData) -> ViewController func makeQuickReplySetupScreenInitialData(context: AccountContext) -> Signal + func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController + func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift index f6412b6200..756cd33f56 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewPeerContentNode.swift @@ -91,6 +91,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer } } + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private let avatarNode: AvatarNode @@ -220,6 +221,10 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } @@ -385,6 +390,7 @@ public enum ShareLoadingState { } public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareContentContainerNode { + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var theme: PresentationTheme @@ -408,6 +414,10 @@ public final class JoinLinkPreviewLoadingContainerNode: ASDisplayNode, ShareCont public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift index 13ebb8e9bd..e4c691aff3 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewContentNode.swift @@ -78,6 +78,9 @@ final class LanguageLinkPreviewContentNode: ASDisplayNode, ShareContentContainer func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/BUILD b/submodules/ShareController/BUILD index 8d91ce2463..439be0fa1f 100644 --- a/submodules/ShareController/BUILD +++ b/submodules/ShareController/BUILD @@ -40,6 +40,9 @@ swift_library( "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/UndoUI", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/LottieComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/ShareController/Sources/ShareContentContainerNode.swift b/submodules/ShareController/Sources/ShareContentContainerNode.swift index 57179bcf47..fe14148588 100644 --- a/submodules/ShareController/Sources/ShareContentContainerNode.swift +++ b/submodules/ShareController/Sources/ShareContentContainerNode.swift @@ -8,6 +8,7 @@ public protocol ShareContentContainerNode: AnyObject { func activate() func deactivate() func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) + func setDidBeginDragging(_ f: (() -> Void)?) func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) func updateTheme(_ theme: PresentationTheme) diff --git a/submodules/ShareController/Sources/ShareController.swift b/submodules/ShareController/Sources/ShareController.swift index 8dde82352f..e77387018c 100644 --- a/submodules/ShareController/Sources/ShareController.swift +++ b/submodules/ShareController/Sources/ShareController.swift @@ -450,6 +450,7 @@ public final class ShareController: ViewController { private let immediatePeerId: PeerId? private let segmentedValues: [ShareControllerSegmentedValue]? private let fromForeignApp: Bool + private let collectibleItemInfo: TelegramCollectibleItemInfo? private let peers = Promise<([(peer: EngineRenderedPeer, presence: EnginePeer.Presence?, requiresPremiumForMessaging: Bool)], EnginePeer)>() private let peersDisposable = MetaDisposable() @@ -484,7 +485,7 @@ public final class ShareController: ViewController { public var parentNavigationController: NavigationController? - public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false) { + public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { self.init( environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), currentContext: ShareControllerAppAccountContext(context: context), @@ -503,11 +504,12 @@ public final class ShareController: ViewController { updatedPresentationData: updatedPresentationData, forceTheme: forceTheme, forcedActionTitle: forcedActionTitle, - shareAsLink: shareAsLink + shareAsLink: shareAsLink, + collectibleItemInfo: collectibleItemInfo ) } - public init(environment: ShareControllerEnvironment, currentContext: ShareControllerAccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [ShareControllerSwitchableAccount] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false) { + public init(environment: ShareControllerEnvironment, currentContext: ShareControllerAccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [ShareControllerSwitchableAccount] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) { self.environment = environment self.currentContext = currentContext self.subject = subject @@ -520,6 +522,7 @@ public final class ShareController: ViewController { self.segmentedValues = segmentedValues self.forceTheme = forceTheme self.shareAsLink = shareAsLink + self.collectibleItemInfo = collectibleItemInfo self.presentationData = updatedPresentationData?.initial ?? environment.presentationData if let forceTheme = self.forceTheme { @@ -717,7 +720,7 @@ public final class ShareController: ViewController { return } strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) - }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory) + }, externalShare: self.externalShare, immediateExternalShare: self.immediateExternalShare, immediatePeerId: self.immediatePeerId, fromForeignApp: self.fromForeignApp, forceTheme: self.forceTheme, fromPublicChannel: fromPublicChannel, segmentedValues: self.segmentedValues, shareStory: self.shareStory, collectibleItemInfo: self.collectibleItemInfo) self.controllerNode.completed = self.completed self.controllerNode.enqueued = self.enqueued self.controllerNode.present = { [weak self] c in diff --git a/submodules/ShareController/Sources/ShareControllerNode.swift b/submodules/ShareController/Sources/ShareControllerNode.swift index 673cb12c01..6c3ab733c3 100644 --- a/submodules/ShareController/Sources/ShareControllerNode.swift +++ b/submodules/ShareController/Sources/ShareControllerNode.swift @@ -9,6 +9,11 @@ import TelegramPresentationData import AccountContext import TelegramIntents import ContextUI +import ComponentFlow +import MultilineTextComponent +import TelegramStringFormatting +import BundleIconComponent +import LottieComponent enum ShareState { case preparing(Bool) @@ -21,6 +26,276 @@ enum ShareExternalState { case done } +private final class ShareContentInfoView: UIView { + private struct Params: Equatable { + var environment: ShareControllerEnvironment + var theme: PresentationTheme + var strings: PresentationStrings + var collectibleItemInfo: TelegramCollectibleItemInfo + var availableSize: CGSize + + init(environment: ShareControllerEnvironment, theme: PresentationTheme, strings: PresentationStrings, collectibleItemInfo: TelegramCollectibleItemInfo, availableSize: CGSize) { + self.environment = environment + self.theme = theme + self.strings = strings + self.collectibleItemInfo = collectibleItemInfo + self.availableSize = availableSize + } + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.environment !== rhs.environment { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.collectibleItemInfo != rhs.collectibleItemInfo { + return false + } + if lhs.availableSize != rhs.availableSize { + return false + } + return true + } + } + + private struct Layout { + var params: Params + var size: CGSize + + init(params: Params, size: CGSize) { + self.params = params + self.size = size + } + } + + private let icon = ComponentView() + private let text = ComponentView() + private var currencySymbolIcon: UIImage? + private var arrowIcon: UIImage? + private let backgroundView: BlurredBackgroundView + + private var currentLayout: Layout? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true) + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(environment: ShareControllerEnvironment, presentationData: PresentationData, collectibleItemInfo: TelegramCollectibleItemInfo, availableSize: CGSize) -> CGSize { + let params = Params( + environment: environment, + theme: presentationData.theme, + strings: presentationData.strings, + collectibleItemInfo: collectibleItemInfo, + availableSize: availableSize + ) + if let currentLayout = self.currentLayout, currentLayout.params == params { + return currentLayout.size + } + let size = self.updateInternal(params: params) + self.currentLayout = Layout(params: params, size: size) + return size + } + + private func updateInternal(params: Params) -> CGSize { + var username: String = "" + if case let .username(value) = params.collectibleItemInfo.subject { + username = value + } + + let textText = NSMutableAttributedString() + + let dateText = stringForDate(timestamp: params.collectibleItemInfo.purchaseDate, strings: params.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(params.collectibleItemInfo.cryptoCurrencyAmount, currency: params.collectibleItemInfo.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(params.collectibleItemInfo.currencyAmount, currency: params.collectibleItemInfo.currency) + + let rawTextString = params.strings.CollectibleItemInfo_UsernameText("@\(username)", params.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(14.0), textColor: .white)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(14.0), range: range.range) + default: + break + } + } + + let currencySymbolRange = (textText.string as NSString).range(of: "~") + + if self.currencySymbolIcon == nil { + if let templateImage = UIImage(bundleImageName: "Peer Info/CollectibleTonSymbolInline") { + self.currencySymbolIcon = generateImage(CGSize(width: templateImage.size.width, height: templateImage.size.height + 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: templateImage.size.width - 2.0, height: templateImage.size.height - 2.0))) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + if currencySymbolRange.location != NSNotFound, let currencySymbolIcon = self.currencySymbolIcon { + textText.replaceCharacters(in: currencySymbolRange, with: "$") + textText.addAttribute(.attachment, value: currencySymbolIcon, range: currencySymbolRange) + + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + let font = Font.semibold(14.0) + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: currencySymbolIcon.size.width + 4.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + textText.addAttribute(NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String), value: runDelegate, range: currencySymbolRange) + } + } + + let accentColor = params.theme.list.itemAccentColor.withMultiplied(hue: 0.933, saturation: 0.61, brightness: 1.0) + if self.arrowIcon == nil { + if let templateImage = UIImage(bundleImageName: "Settings/TextArrowRight") { + let scaleFactor: CGFloat = 0.8 + let imageSize = CGSize(width: floor(templateImage.size.width * scaleFactor), height: floor(templateImage.size.height * scaleFactor)) + self.arrowIcon = generateImage(imageSize, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + textText.append(NSAttributedString(string: "\n\(params.strings.CollectibleItemInfo_ShareInlineText_LearnMore)", attributes: [ + .font: Font.medium(14.0), + .foregroundColor: accentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ])) + if let range = textText.string.range(of: ">"), let arrowIcon = self.arrowIcon { + textText.addAttribute(.attachment, value: arrowIcon, range: NSRange(range, in: textText.string)) + } + + let textInsets = UIEdgeInsets(top: 8.0, left: 50.0, bottom: 8.0, right: 10.0) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(textText), + maximumNumberOfLines: 0, + lineSpacing: 0.185, + highlightColor: accentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let params = self.currentLayout?.params else { + return + } + if let environment = params.environment as? ShareControllerAppEnvironment { + environment.sharedContext.applicationBindings.openUrl(params.collectibleItemInfo.url) + } + } + )), + environment: {}, + containerSize: CGSize(width: params.availableSize.width - textInsets.left - textInsets.right, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + textView.frame = textFrame + } + + let size = CGSize(width: params.availableSize.width, height: textInsets.top + textSize.height + textInsets.bottom) + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ToastCollectibleUsernameEmoji"), + loop: false + )), + environment: {}, + containerSize: CGSize(width: 30.0, height: 30.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: floor((textInsets.left - iconSize.width) * 0.5), y: floor((size.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view as? LottieComponent.View { + if iconView.superview == nil { + self.addSubview(iconView) + iconView.playOnce(delay: 0.1) + } + iconView.frame = iconFrame + } + + self.backgroundView.updateColor(color: UIColor(rgb: 0x1C2023), transition: .immediate) + self.backgroundView.update(size: size, cornerRadius: 16.0, transition: .immediate) + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + + return size + } +} + final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate { private weak var controller: ShareController? private let environment: ShareControllerEnvironment @@ -33,6 +308,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let fromForeignApp: Bool private let fromPublicChannel: Bool private let segmentedValues: [ShareControllerSegmentedValue]? + private let collectibleItemInfo: TelegramCollectibleItemInfo? var selectedSegmentedIndex: Int = 0 private let defaultAction: ShareControllerAction? @@ -48,6 +324,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let contentContainerNode: ASDisplayNode private let contentBackgroundNode: ASImageNode + private var contentInfoView: ShareContentInfoView? private var contentNode: (ASDisplayNode & ShareContentContainerNode)? private var previousContentNode: (ASDisplayNode & ShareContentContainerNode)? @@ -90,7 +367,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate private let showNames = ValuePromise(true) - init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?) { + init(controller: ShareController, environment: ShareControllerEnvironment, presentationData: PresentationData, presetText: String?, defaultAction: ShareControllerAction?, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, presentError: @escaping (String?, String) -> Void, externalShare: Bool, immediateExternalShare: Bool, immediatePeerId: PeerId?, fromForeignApp: Bool, forceTheme: PresentationTheme?, fromPublicChannel: Bool, segmentedValues: [ShareControllerSegmentedValue]?, shareStory: (() -> Void)?, collectibleItemInfo: TelegramCollectibleItemInfo?) { self.controller = controller self.environment = environment self.presentationData = presentationData @@ -102,6 +379,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.presentError = presentError self.fromPublicChannel = fromPublicChannel self.segmentedValues = segmentedValues + self.collectibleItemInfo = collectibleItemInfo self.presetText = presetText @@ -156,6 +434,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.contentBackgroundNode.displayWithoutProcessing = true self.contentBackgroundNode.image = roundedBackground + self.contentInfoView = ShareContentInfoView(frame: CGRect()) + self.actionsBackgroundNode = ASImageNode() self.actionsBackgroundNode.isLayerBacked = true self.actionsBackgroundNode.displayWithoutProcessing = true @@ -356,6 +636,10 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate self.wrappingScrollNode.addSubnode(self.contentBackgroundNode) + if let contentInfoView = self.contentInfoView { + self.wrappingScrollNode.view.addSubview(contentInfoView) + } + self.wrappingScrollNode.addSubnode(self.contentContainerNode) self.contentContainerNode.addSubnode(self.actionSeparatorNode) self.contentContainerNode.addSubnode(self.actionsBackgroundNode) @@ -433,12 +717,14 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } if let searchContentNode = strongSelf.contentNode as? ShareSearchContainerNode { + searchContentNode.setDidBeginDragging(nil) searchContentNode.setContentOffsetUpdated(nil) let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - searchContentNode.contentGridNode.scrollView.contentOffset.y if let sourceFrame = searchContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) { topicsContentNode.animateIn(sourceFrame: sourceFrame, scrollDelta: scrollDelta) } } else if let peersContentNode = strongSelf.peersContentNode { + peersContentNode.setDidBeginDragging(nil) peersContentNode.setContentOffsetUpdated(nil) let scrollDelta = topicsContentNode.contentGridNode.scrollView.contentOffset.y - peersContentNode.contentGridNode.scrollView.contentOffset.y if let sourceFrame = peersContentNode.animateOut(peerId: peer.peerId, scrollDelta: scrollDelta) { @@ -446,6 +732,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + topicsContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) topicsContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -459,6 +748,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate guard let topicsContentNode = self.topicsContentNode else { return } + topicsContentNode.setDidBeginDragging(nil) topicsContentNode.setContentOffsetUpdated(nil) if let searchContentNode = self.contentNode as? ShareSearchContainerNode { @@ -472,6 +762,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } if let searchContentNode = self.contentNode as? ShareSearchContainerNode { + searchContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) searchContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -487,6 +780,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate }) } } else if let peersContentNode = self.peersContentNode { + peersContentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) peersContentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -579,6 +875,7 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate let previous = self.contentNode if let previous = previous { + previous.setDidBeginDragging(nil) previous.setContentOffsetUpdated(nil) if animated { transition = .animated(duration: 0.4, curve: .spring) @@ -597,6 +894,8 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate previous.removeFromSupernode() self.previousContentNode = nil } + + self.contentNodeDidBeginDragging() } else { transition = .immediate } @@ -607,6 +906,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate contentNode.frame = previous.frame contentNode.updateLayout(size: previous.bounds.size, isLandscape: layout.size.width > layout.size.height, bottomInset: bottomGridInset, transition: .immediate) + contentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -635,6 +937,9 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } else { if let contentNode = self.contentNode { + contentNode.setDidBeginDragging({ [weak self] in + self?.contentNodeDidBeginDragging() + }) contentNode.setContentOffsetUpdated({ [weak self] contentOffset, transition in self?.contentNodeOffsetUpdated(contentOffset, transition: transition) }) @@ -737,6 +1042,13 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } } + private func contentNodeDidBeginDragging() { + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + } + } + private func contentNodeOffsetUpdated(_ contentOffset: CGFloat, transition: ContainedViewLayoutTransition) { if let (layout, _, _) = self.containerLayout { var insets = layout.insets(options: [.statusBar, .input]) @@ -770,6 +1082,25 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } transition.updateFrame(node: self.contentBackgroundNode, frame: backgroundFrame) + if let contentInfoView = self.contentInfoView, let collectibleItemInfo = self.collectibleItemInfo { + let contentInfoSize = contentInfoView.update( + environment: self.environment, + presentationData: self.presentationData, + collectibleItemInfo: collectibleItemInfo, + availableSize: CGSize(width: backgroundFrame.width, height: 1000.0) + ) + let contentInfoFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0 - contentInfoSize.height), size: contentInfoSize) + + if contentInfoView.bounds.isEmpty { + if contentInfoFrame.minY < 0.0 { + contentInfoView.alpha = 0.0 + } + } + + transition.updatePosition(layer: contentInfoView.layer, position: contentInfoFrame.center) + transition.updateBounds(layer: contentInfoView.layer, bounds: CGRect(origin: CGPoint(), size: contentInfoFrame.size)) + } + if let animateContentNodeOffsetFromBackgroundOffset = self.animateContentNodeOffsetFromBackgroundOffset { self.animateContentNodeOffsetFromBackgroundOffset = nil let offset = backgroundFrame.minY - animateContentNodeOffsetFromBackgroundOffset @@ -1019,6 +1350,11 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate } self.animatingOut = true + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + Transition.easeInOut(duration: 0.2).setAlpha(view: contentInfoView, alpha: 0.0) + Transition.easeInOut(duration: 0.2).setScale(view: contentInfoView, scale: 0.5) + } + if self.contentNode != nil { var dimCompleted = false var offsetCompleted = false @@ -1223,6 +1559,12 @@ final class ShareControllerNode: ViewControllerTracingNode, UIScrollViewDelegate return result } if self.bounds.contains(point) { + if let contentInfoView = self.contentInfoView, contentInfoView.alpha != 0.0 { + if let result = contentInfoView.hitTest(self.view.convert(point, to: contentInfoView), with: event) { + return result + } + } + if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) && !self.cancelButtonNode.bounds.contains(self.convert(point, to: self.cancelButtonNode)) { return self.dimNode.view } diff --git a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift index c643f1ef46..1d6c98dede 100644 --- a/submodules/ShareController/Sources/ShareLoadingContainerNode.swift +++ b/submodules/ShareController/Sources/ShareLoadingContainerNode.swift @@ -94,6 +94,9 @@ public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContain public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } @@ -308,6 +311,9 @@ public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareConte public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) { } + public func setDidBeginDragging(_ f: (() -> Void)?) { + } + public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/SharePeersContainerNode.swift b/submodules/ShareController/Sources/SharePeersContainerNode.swift index 60e45b0d7f..1d4d9439c0 100644 --- a/submodules/ShareController/Sources/SharePeersContainerNode.swift +++ b/submodules/ShareController/Sources/SharePeersContainerNode.swift @@ -126,6 +126,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { private let segmentedValues: [ShareControllerSegmentedValue]? + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var openSearch: (() -> Void)? @@ -297,6 +298,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -350,6 +355,10 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode { self.ensurePeerVisibleOnLayout = peerId } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/ShareSearchContainerNode.swift b/submodules/ShareController/Sources/ShareSearchContainerNode.swift index 3376a93e7d..31c048c033 100644 --- a/submodules/ShareController/Sources/ShareSearchContainerNode.swift +++ b/submodules/ShareController/Sources/ShareSearchContainerNode.swift @@ -194,6 +194,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { private let searchNode: ShareSearchBarNode private let cancelButtonNode: HighlightableButtonNode + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var cancel: (() -> Void)? @@ -240,6 +241,14 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.addSubnode(self.cancelButtonNode) self.addSubnode(self.contentSeparatorNode) + self.recentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } + self.recentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in if let strongSelf = self, !strongSelf.recentGridNode.isHidden { strongSelf.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -466,6 +475,10 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode { self.ensurePeerVisibleOnLayout = peerId } + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift index 5319df1494..5462d91491 100644 --- a/submodules/ShareController/Sources/ShareTopicsContainerNode.swift +++ b/submodules/ShareController/Sources/ShareTopicsContainerNode.swift @@ -174,6 +174,7 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { private let contentSubtitleNode: ASTextNode private let backNode: CancelButtonNode + private var contentDidBeginDragging: (() -> Void)? private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)? private var validLayout: (CGSize, CGFloat)? @@ -249,6 +250,10 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) + + self.contentGridNode.scrollingInitiated = { [weak self] in + self?.contentDidBeginDragging?() + } self.contentGridNode.presentationLayoutUpdated = { [weak self] presentationLayout, transition in self?.gridPresentationLayoutUpdated(presentationLayout, transition: transition) @@ -286,6 +291,10 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentGridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } } + + func setDidBeginDragging(_ f: (() -> Void)?) { + self.contentDidBeginDragging = f + } func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f @@ -458,8 +467,6 @@ final class ShareTopicsContainerNode: ASDisplayNode, ShareContentContainerNode { self.contentSubtitleNode.frame = originalSubtitleFrame transition.updateFrame(node: self.contentSubtitleNode, frame: subtitleFrame) - - self.contentOffsetUpdated?(presentationLayout.contentOffset.y, actualTransition) } } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 062a5adc26..472254e753 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -1321,7 +1321,7 @@ public extension Api { return parser(reader) } else { - telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found") + telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found") return nil } } diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index 55646590ff..186d35e138 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -686,6 +686,9 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod func setEnsurePeerVisibleOnLayout(_ peerId: PeerId?) { } + func setDidBeginDragging(_ f: (() -> Void)?) { + } + func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) { self.contentOffsetUpdated = f } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index d0bc67b725..38876a16b8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -30,6 +30,59 @@ public final class OpaqueChatInterfaceState { } } +public final class TelegramCollectibleItemInfo: Equatable { + public enum Subject: Equatable { + case username(String) + case phoneNumber(String) + } + + public let subject: Subject + public let purchaseDate: Int32 + public let currency: String + public let currencyAmount: Int64 + public let cryptoCurrency: String + public let cryptoCurrencyAmount: Int64 + public let url: String + + public init(subject: Subject, purchaseDate: Int32, currency: String, currencyAmount: Int64, cryptoCurrency: String, cryptoCurrencyAmount: Int64, url: String) { + self.subject = subject + self.purchaseDate = purchaseDate + self.currency = currency + self.currencyAmount = currencyAmount + self.cryptoCurrency = cryptoCurrency + self.cryptoCurrencyAmount = cryptoCurrencyAmount + self.url = url + } + + public static func ==(lhs: TelegramCollectibleItemInfo, rhs: TelegramCollectibleItemInfo) -> Bool { + if lhs === rhs { + return true + } + if lhs.subject != rhs.subject { + return false + } + if lhs.purchaseDate != rhs.purchaseDate { + return false + } + if lhs.currency != rhs.currency { + return false + } + if lhs.currencyAmount != rhs.currencyAmount { + return false + } + if lhs.cryptoCurrency != rhs.cryptoCurrency { + return false + } + if lhs.cryptoCurrencyAmount != rhs.cryptoCurrencyAmount { + return false + } + if lhs.url != rhs.url { + return false + } + return true + } +} + public extension TelegramEngine { enum NextUnreadChannelLocation: Equatable { case same @@ -1387,6 +1440,56 @@ public extension TelegramEngine { }) }).start() } + + public func getCollectibleUsernameInfo(username: String) -> Signal { + return self.account.network.request(Api.functions.fragment.getCollectibleInfo(collectible: .inputCollectibleUsername(username: username))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> TelegramCollectibleItemInfo? in + guard let result else { + return nil + } + switch result { + case let .collectibleInfo(purchaseDate, currency, amount, cryptoCurrency, cryptoAmount, url): + return TelegramCollectibleItemInfo( + subject: .username(username), + purchaseDate: purchaseDate, + currency: currency, + currencyAmount: amount, + cryptoCurrency: cryptoCurrency, + cryptoCurrencyAmount: cryptoAmount, + url: url + ) + } + } + } + + public func getCollectiblePhoneNumberInfo(phoneNumber: String) -> Signal { + return self.account.network.request(Api.functions.fragment.getCollectibleInfo(collectible: .inputCollectiblePhone(phone: phoneNumber))) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> TelegramCollectibleItemInfo? in + guard let result else { + return nil + } + switch result { + case let .collectibleInfo(purchaseDate, currency, amount, cryptoCurrency, cryptoAmount, url): + return TelegramCollectibleItemInfo( + subject: .phoneNumber(phoneNumber), + purchaseDate: purchaseDate, + currency: currency, + currencyAmount: amount, + cryptoCurrency: cryptoCurrency, + cryptoCurrencyAmount: cryptoAmount, + url: url + ) + } + } + } } } diff --git a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift index 2986a87c58..faf6979c99 100644 --- a/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift +++ b/submodules/TelegramStringFormatting/Sources/CurrencyFormat.swift @@ -1,15 +1,15 @@ import Foundation import AppBundle -private final class CurrencyFormatterEntry { - let symbol: String - let thousandsSeparator: String - let decimalSeparator: String - let symbolOnLeft: Bool - let spaceBetweenAmountAndSymbol: Bool - let decimalDigits: Int +public final class CurrencyFormatterEntry { + public let symbol: String + public let thousandsSeparator: String + public let decimalSeparator: String + public let symbolOnLeft: Bool + public let spaceBetweenAmountAndSymbol: Bool + public let decimalDigits: Int - init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { self.symbol = symbol self.thousandsSeparator = thousandsSeparator self.decimalSeparator = decimalSeparator @@ -191,8 +191,8 @@ public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { } } -public func formatCurrencyAmountCustom(_ amount: Int64, currency: String) -> (String, String, Bool) { - if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { +public func formatCurrencyAmountCustom(_ amount: Int64, currency: String, customFormat: CurrencyFormatterEntry? = nil) -> (String, String, Bool) { + if let entry = customFormat ?? currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { var result = "" if amount < 0 { result.append("-") diff --git a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift index 8cdc962093..8483290226 100644 --- a/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/PresenceStrings.swift @@ -143,7 +143,6 @@ public func stringForCompactDate(timestamp: Int32, strings: PresentationStrings, var timeinfo: tm = tm() localtime_r(&t, &timeinfo) - //TODO:localize return "\(shortStringForDayOfWeek(strings: strings, day: timeinfo.tm_wday)) \(timeinfo.tm_mday) \(monthAtIndex(Int(timeinfo.tm_mon), strings: strings))" } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index b8a93acb65..3ff80453f1 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -437,6 +437,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen", "//submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen", "//submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController", + "//submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen" ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 9b1fd4efd9..becde7514e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -140,6 +140,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Settings/PeerNameColorItem", "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/TelegramUI/Components/TextLoadingEffect", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 8093277652..0929d6f58f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -7,6 +7,8 @@ import UIKit import AppBundle import TelegramStringFormatting import ContextUI +import SwiftSignalKit +import TextLoadingEffect enum PeerInfoScreenLabeledValueTextColor { case primary @@ -22,6 +24,23 @@ enum PeerInfoScreenLabeledValueIcon { case qrCode } +private struct TextLinkItemSource: Equatable { + enum Target { + case primary + case additional + } + + let item: TextLinkItem + let target: Target + let range: NSRange? + + init(item: TextLinkItem, target: Target, range: NSRange?) { + self.item = item + self.target = target + self.range = range + } +} + final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let id: AnyHashable let label: String @@ -30,9 +49,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { let textColor: PeerInfoScreenLabeledValueTextColor let textBehavior: PeerInfoScreenLabeledValueTextBehavior let icon: PeerInfoScreenLabeledValueIcon? - let action: ((ASDisplayNode) -> Void)? + let action: ((ASDisplayNode, Promise?) -> Void)? let longTapAction: ((ASDisplayNode) -> Void)? - let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? + let linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? let iconAction: (() -> Void)? let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? let requestLayout: () -> Void @@ -45,9 +64,9 @@ final class PeerInfoScreenLabeledValueItem: PeerInfoScreenItem { textColor: PeerInfoScreenLabeledValueTextColor = .primary, textBehavior: PeerInfoScreenLabeledValueTextBehavior = .singleLine, icon: PeerInfoScreenLabeledValueIcon? = nil, - action: ((ASDisplayNode) -> Void)?, + action: ((ASDisplayNode, Promise?) -> Void)?, longTapAction: ((ASDisplayNode) -> Void)? = nil, - linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void)? = nil, + linkItemAction: ((TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void)? = nil, iconAction: (() -> Void)? = nil, contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil, requestLayout: @escaping () -> Void @@ -116,9 +135,14 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { private let activateArea: AccessibilityAreaNode + private var validLayout: (width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool)? private var item: PeerInfoScreenLabeledValueItem? private var theme: PresentationTheme? + private var linkProgressView: TextLoadingEffectView? + private var linkItemWithProgress: TextLinkItemSource? + private var linkItemProgressDisposable: Disposable? + private var isExpanded: Bool = false override init() { @@ -259,6 +283,10 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } } + deinit { + self.linkItemProgressDisposable?.dispose() + } + @objc private func expandPressed() { self.isExpanded = true self.item?.requestLayout() @@ -313,11 +341,57 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { case .tap, .longTap: if let item = self.item { if let linkItem = self.linkItemAtPoint(location) { - item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first) + self.linkItemProgressDisposable?.dispose() + let progressValue = Promise(false) + self.linkItemProgressDisposable = (progressValue.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + var currentLinkItem: TextLinkItemSource? + if value { + currentLinkItem = linkItem + } + if self.linkItemWithProgress != currentLinkItem { + self.linkItemWithProgress = currentLinkItem + + if let validLayout = self.validLayout { + let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + } + } + }) + + item.linkItemAction?(gesture == .tap ? .tap : .longTap, linkItem.item, self.linkHighlightingNode ?? self, self.linkHighlightingNode?.rects.first, progressValue) } else if case .longTap = gesture { item.longTapAction?(self) } else if case .tap = gesture { - item.action?(self.contextSourceNode) + var linkItem: TextLinkItemSource? + if let attributedText = self.textNode.attributedText { + linkItem = TextLinkItemSource(item: .url(url: "", concealed: false), target: .primary, range: NSRange(location: 0, length: attributedText.length)) + } + self.linkItemProgressDisposable?.dispose() + let progressValue = Promise(false) + self.linkItemProgressDisposable = (progressValue.get() + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let self else { + return + } + var currentLinkItem: TextLinkItemSource? + if value { + currentLinkItem = linkItem + } + if self.linkItemWithProgress != currentLinkItem { + self.linkItemWithProgress = currentLinkItem + + if let validLayout = self.validLayout { + let _ = self.update(width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) + } + } + }) + + item.action?(self.contextSourceNode, progressValue) } } default: @@ -334,13 +408,15 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { return 10.0 } + self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners) + self.item = item self.theme = presentationData.theme if let action = item.action { self.selectionNode.pressed = { [weak self] in if let strongSelf = self { - action(strongSelf.contextSourceNode) + action(strongSelf.contextSourceNode, nil) } } } else { @@ -553,30 +629,100 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } self.contextSourceNode.contentRect = extractedRect + if let linkItemWithProgress = self.linkItemWithProgress, let range = linkItemWithProgress.range { + let linkProgressView: TextLoadingEffectView + if let current = self.linkProgressView { + linkProgressView = current + } else { + linkProgressView = TextLoadingEffectView(frame: CGRect()) + self.linkProgressView = linkProgressView + self.contextSourceNode.contentNode.view.addSubview(linkProgressView) + } + + let progressColor: UIColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1) + + let targetTextNode: TextNode + switch linkItemWithProgress.target { + case .primary: + targetTextNode = self.textNode + case .additional: + targetTextNode = self.additionalTextNode + } + linkProgressView.frame = targetTextNode.frame + linkProgressView.update(color: progressColor, textNode: targetTextNode, range: range) + } else { + if let linkProgressView = self.linkProgressView { + self.linkProgressView = nil + linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in + linkProgressView?.removeFromSuperview() + }) + } + } + return height } - private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItem? { + private func linkItemAtPoint(_ point: CGPoint) -> TextLinkItemSource? { let textNodeFrame = self.textNode.frame - if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + var item: TextLinkItem? + var urlRange: NSRange? if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - return .url(url: url, concealed: false) + item = .url(url: url, concealed: false) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue + } } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .mention(peerName) + item = .mention(peerName) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention).rawValue, index: index) { + urlRange = urlRangeValue + } } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + item = .hashtag(hashtag.peerName, hashtag.hashtag) + + if let (_, _, urlRangeValue) = self.textNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag).rawValue, index: index) { + urlRange = urlRangeValue + } + } else { + item = nil + } + if let item { + return TextLinkItemSource(item: item, target: .primary, range: urlRange) } else { return nil } } let additionalTextNodeFrame = self.additionalTextNode.frame - if let (_, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.minY)) { + if let (index, attributes) = self.additionalTextNode.attributesAtPoint(CGPoint(x: point.x - additionalTextNodeFrame.minX, y: point.y - additionalTextNodeFrame.minY)) { + var item: TextLinkItem? + var urlRange: NSRange? + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - return .url(url: url, concealed: false) + item = .url(url: url, concealed: false) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) { + urlRange = urlRangeValue + } } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - return .mention(peerName) + item = .mention(peerName) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention).rawValue, index: index) { + urlRange = urlRangeValue + } } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - return .hashtag(hashtag.peerName, hashtag.hashtag) + item = .hashtag(hashtag.peerName, hashtag.hashtag) + + if let (_, _, urlRangeValue) = self.additionalTextNode.attributeSubstringWithRange(name: NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag).rawValue, index: index) { + urlRange = urlRangeValue + } + } else { + item = nil + } + + if let item { + return TextLinkItemSource(item: item, target: .additional, range: urlRange) } else { return nil } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 74439e4e9f..4b28227d77 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -531,7 +531,7 @@ private enum TopicsLimitedReason { private final class PeerInfoInteraction { let openChat: () -> Void - let openUsername: (String) -> Void + let openUsername: (String, Bool, Promise?) -> Void let openPhone: (String, ASDisplayNode, ContextGesture?) -> Void let editingOpenNotificationSettings: () -> Void let editingOpenSoundSettings: () -> Void @@ -585,7 +585,7 @@ private final class PeerInfoInteraction { let openEditing: () -> Void init( - openUsername: @escaping (String) -> Void, + openUsername: @escaping (String, Bool, Promise?) -> Void, openPhone: @escaping (String, ASDisplayNode, ContextGesture?) -> Void, editingOpenNotificationSettings: @escaping () -> Void, editingOpenSoundSettings: @escaping () -> Void, @@ -1092,7 +1092,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let bioContextAction: (ASDisplayNode) -> Void = { sourceNode in interaction.openPeerInfoContextMenu(.bio, sourceNode, nil) } - let bioLinkAction: (TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?) -> Void = { action, item, _, _ in + let bioLinkAction: (TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise?) -> Void = { action, item, _, _, _ in interaction.performBioLinkAction(action, item) } @@ -1109,7 +1109,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese } else { label = presentationData.strings.ContactInfo_PhoneLabelMobile } - items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node in + items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 2, label: label, text: formattedPhone, textColor: .accent, action: { node, _ in interaction.openPhone(phone, node, nil) }, longTapAction: nil, contextAction: { node, gesture, _ in interaction.openPhone(phone, node, gesture) @@ -1132,14 +1132,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalText: additionalUsernames, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(mainUsername) + action: { _, progress in + interaction.openUsername(mainUsername, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil) - }, linkItemAction: { type, item, _, _ in + }, linkItemAction: { type, item, _, _, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...])) + interaction.openUsername(String(username[username.index(username.startIndex, offsetBy: 1)...]), false, progress) } } }, iconAction: { @@ -1307,14 +1307,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese text: linkText, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(linkText) + action: { _, progress in + interaction.openUsername(linkText, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: linkText), sourceNode, nil) - }, linkItemAction: { type, item, _, _ in + }, linkItemAction: { type, item, _, _, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1)))) + interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1))), false, progress) } } }, iconAction: { @@ -1360,14 +1360,14 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese additionalText: additionalUsernames, textColor: .accent, icon: .qrCode, - action: { _ in - interaction.openUsername(mainUsername) + action: { _, progress in + interaction.openUsername(mainUsername, true, progress) }, longTapAction: { sourceNode in interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil) - }, linkItemAction: { type, item, sourceNode, sourceRect in + }, linkItemAction: { type, item, sourceNode, sourceRect, progress in if case .tap = type { if case let .mention(username) = item { - interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1)))) + interaction.openUsername(String(username.suffix(from: username.index(username.startIndex, offsetBy: 1))), false, progress) } } else if case .longTap = type { if case let .mention(username) = item { @@ -2393,8 +2393,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.paneContainerNode.parentController = controller self._interaction = PeerInfoInteraction( - openUsername: { [weak self] value in - self?.openUsername(value: value) + openUsername: { [weak self] value, isMainUsername, progress in + self?.openUsername(value: value, isMainUsername: isMainUsername, progress: progress) }, openPhone: { [weak self] value, node, gesture in self?.openPhone(value: value, node: node, gesture: gesture) @@ -6416,7 +6416,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro contextController.pushItems(items: .single(ContextController.Items(content: .list(subItems)))) } - private func openUsername(value: String) { + private func openUsername(value: String, isMainUsername: Bool, progress: Promise?) { let url: String if value.hasPrefix("https://") { url = value @@ -6424,70 +6424,101 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro url = "https://t.me/\(value)" } - let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData) - shareController.completed = { [weak self] peerIds in - guard let strongSelf = self else { + let openShare: (TelegramCollectibleItemInfo?) -> Void = { [weak self] collectibleItemInfo in + guard let self else { return } - let _ = (strongSelf.context.engine.data.get( - EngineDataList( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) - ) - ) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + let shareController = ShareController(context: self.context, subject: .url(url), updatedPresentationData: self.controller?.updatedPresentationData, collectibleItemInfo: collectibleItemInfo) + shareController.completed = { [weak self] peerIds in guard let strongSelf = self else { return } - - let peers = peerList.compactMap { $0 } - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - - let text: String - var savedMessages = false - if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { - text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One - savedMessages = true - } else { - if peers.count == 1, let peer = peers.first { - let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string - } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string - } else if let peer = peers.first { - let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string - } else { - text = "" + let _ = (strongSelf.context.engine.data.get( + EngineDataList( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init) + ) + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerList in + guard let strongSelf = self else { + return } + + let peers = peerList.compactMap { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let text: String + var savedMessages = false + if peerIds.count == 1, let peerId = peerIds.first, peerId == strongSelf.context.account.peerId { + text = presentationData.strings.UserInfo_LinkForwardTooltip_SavedMessages_One + savedMessages = true + } else { + if peers.count == 1, let peer = peers.first { + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_Chat_One(peerName).string + } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : firstPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : secondPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_TwoChats_One(firstPeerName, secondPeerName).string + } else if let peer = peers.first { + let peerName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + text = presentationData.strings.UserInfo_LinkForwardTooltip_ManyChats_One(peerName, "\(peers.count - 1)").string + } else { + text = "" + } + } + + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in + if savedMessages, let self, action == .info { + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + guard let navigationController = self.controller?.navigationController as? NavigationController else { + return + } + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) + }) + } + return false + }), in: .current) + }) + } + shareController.actionCompleted = { [weak self] in + if let strongSelf = self { + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) + } + } + self.view.endEditing(true) + self.controller?.present(shareController, in: .window(.root)) + } + + if let pathComponents = URL(string: url)?.pathComponents, pathComponents.count >= 2, !pathComponents[1].isEmpty { + let namePart = pathComponents[1] + progress?.set(.single(true)) + let _ = (self.context.sharedContext.makeCollectibleItemInfoScreenInitialData(context: self.context, peerId: self.peerId, subject: .username(namePart)) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: savedMessages, text: text), elevatedLayout: false, animateInAsReplacement: true, action: { action in - if savedMessages, let self, action == .info { - let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer else { - return - } - guard let navigationController = self.controller?.navigationController as? NavigationController else { - return - } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), forceOpenChat: true)) - }) + progress?.set(.single(false)) + + if let initialData { + if isMainUsername { + openShare(initialData.collectibleItemInfo) + } else { + self.view.endEditing(true) + self.controller?.push(self.context.sharedContext.makeCollectibleItemInfoScreen(context: self.context, initialData: initialData)) } - return false - }), in: .current) + } else { + openShare(nil) + } }) + } else { + openShare(nil) } - shareController.actionCompleted = { [weak self] in - if let strongSelf = self { - let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } - strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current) - } - } - self.view.endEditing(true) - self.controller?.present(shareController, in: .window(.root)) } private func requestCall(isVideo: Bool, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil, result: ((ContextMenuActionResult) -> Void)? = nil, backAction: ((ContextControllerProtocol) -> Void)? = nil) { @@ -6697,10 +6728,27 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } var actions = ContextController.Items(content: .list(items)) if isAnonymousNumber && !accountIsFromUS { + let collectibleInfo = Promise() + collectibleInfo.set(strongSelf.context.sharedContext.makeCollectibleItemInfoScreenInitialData(context: strongSelf.context, peerId: strongSelf.peerId, subject: .phoneNumber(value))) + actions.tip = .animatedEmoji(text: strongSelf.presentationData.strings.UserInfo_AnonymousNumberInfo, arguments: nil, file: nil, action: { [weak self] in - if let strongSelf = self { - strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://fragment.com/numbers", forceExternal: true, presentationData: strongSelf.presentationData, navigationController: nil, dismissInput: {}) + guard let self else { + return } + + let _ = (collectibleInfo.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] initialData in + guard let self else { + return + } + if let initialData { + self.view.endEditing(true) + self.controller?.push(self.context.sharedContext.makeCollectibleItemInfoScreen(context: self.context, initialData: initialData)) + } else { + self.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://fragment.com/numbers", forceExternal: true, presentationData: self.presentationData, navigationController: nil, dismissInput: {}) + } + }) }) } let contextController = ContextController(presentationData: strongSelf.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture) diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD new file mode 100644 index 0000000000..7a12d1fc72 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/BUILD @@ -0,0 +1,40 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CollectibleItemInfoScreen", + module_name = "CollectibleItemInfoScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/ComponentFlow", + "//submodules/Components/ViewControllerComponent", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/Components/MultilineTextComponent", + "//submodules/Components/BalancedTextComponent", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/Components/SheetComponent", + "//submodules/PresentationDataUtils", + "//submodules/Components/SolidRoundedButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", + "//submodules/Markdown", + "//submodules/TelegramStringFormatting", + "//submodules/AvatarNode", + "//submodules/PhoneNumberFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift new file mode 100644 index 0000000000..df6c8248b0 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreen.swift @@ -0,0 +1,772 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import ViewControllerComponent +import AccountContext +import SheetComponent +import ButtonComponent +import PlainButtonComponent +import TelegramCore +import SwiftSignalKit +import MultilineTextComponent +import BalancedTextComponent +import TelegramStringFormatting +import AvatarNode +import TelegramPresentationData +import PhoneNumberFormat +import BundleIconComponent + +private final class PeerBadgeComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let strings: PresentationStrings + let peer: EnginePeer + + init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + peer: EnginePeer + ) { + self.context = context + self.theme = theme + self.strings = strings + self.peer = peer + } + + static func ==(lhs: PeerBadgeComponent, rhs: PeerBadgeComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let background = ComponentView() + private let title = ComponentView() + private var avatarNode: AvatarNode? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerBadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height: CGFloat = 32.0 + let avatarPadding: CGFloat = 1.0 + + let avatarDiameter = height - avatarPadding * 2.0 + let avatarTextSpacing: CGFloat = 4.0 + let rightTextInset: CGFloat = 12.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer.displayTitle(strings: component.strings, displayOrder: .firstLast), font: Font.medium(15.0), textColor: component.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarPadding - avatarDiameter - avatarTextSpacing - rightTextInset, height: height) + ) + let titleFrame = CGRect(origin: CGPoint(x: avatarPadding + avatarDiameter + avatarTextSpacing, y: floorToScreenPixels((height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let avatarNode: AvatarNode + if let current = self.avatarNode { + avatarNode = current + } else { + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + self.avatarNode = avatarNode + self.addSubview(avatarNode.view) + } + + let avatarFrame = CGRect(origin: CGPoint(x: avatarPadding, y: avatarPadding), size: CGSize(width: avatarDiameter, height: avatarDiameter)) + avatarNode.frame = avatarFrame + avatarNode.updateSize(size: avatarFrame.size) + avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + + let size = CGSize(width: avatarPadding + avatarDiameter + avatarTextSpacing + titleSize.width + rightTextInset, height: height) + + let _ = self.background.update( + transition: transition, + component: AnyComponent(RoundedRectangle(color: component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: nil)), + environment: {}, + containerSize: size + ) + if let backgroundView = self.background.view { + if backgroundView.superview == nil { + self.insertSubview(backgroundView, at: 0) + } + transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class CollectibleItemInfoScreenContentComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: CollectibleItemInfoScreen.InitialData + let dismiss: () -> Void + + init( + context: AccountContext, + initialData: CollectibleItemInfoScreen.InitialData, + dismiss: @escaping () -> Void + ) { + self.context = context + self.initialData = initialData + self.dismiss = dismiss + } + + static func ==(lhs: CollectibleItemInfoScreenContentComponent, rhs: CollectibleItemInfoScreenContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let iconBackground = ComponentView() + private let icon = ComponentView() + private let title = ComponentView() + private let peerBadge = ComponentView() + private let text = ComponentView() + private let button = ComponentView() + private let copyButton = ComponentView() + + private var component: CollectibleItemInfoScreenContentComponent? + + private var currencySymbolIcon: UIImage? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[EnvironmentType.self].value + + let sideInset: CGFloat = 16.0 + let contentSideInset: CGFloat = sideInset + 16.0 + + var contentHeight: CGFloat = 0.0 + contentHeight += 30.0 + + let iconBackgroundSize = self.iconBackground.update( + transition: transition, + component: AnyComponent(RoundedRectangle(color: environment.theme.list.itemCheckColors.fillColor, cornerRadius: nil)), + environment: {}, + containerSize: CGSize(width: 90.0, height: 90.0) + ) + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconBackgroundSize.width) * 0.5), y: contentHeight), size: iconBackgroundSize) + if let iconBackgroundView = self.iconBackground.view { + if iconBackgroundView.superview == nil { + self.addSubview(iconBackgroundView) + } + transition.setFrame(view: iconBackgroundView, frame: iconBackgroundFrame) + } + contentHeight += iconBackgroundSize.height + contentHeight += 16.0 + + let iconSize = self.icon.update( + transition: transition, + component: AnyComponent(BundleIconComponent( + name: "Peer Info/CollectibleUsernameInfoTitleIcon", + tintColor: environment.theme.list.itemCheckColors.foregroundColor + )), + environment: {}, + containerSize: iconBackgroundFrame.size + ) + let iconFrame = CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - iconSize.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - iconSize.height) * 0.5)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: iconFrame) + } + + let titleText = NSMutableAttributedString() + let textText = NSMutableAttributedString() + switch component.initialData.subject { + case let .username(username): + let rawTitleString = environment.strings.CollectibleItemInfo_UsernameTitle("@\(username.username)") + titleText.append(NSAttributedString(string: rawTitleString.string, font: Font.semibold(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTitleString.ranges { + titleText.addAttributes([ + .foregroundColor: environment.theme.list.itemAccentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ], range: range.range) + } + + let dateText = stringForDate(timestamp: username.info.purchaseDate, strings: environment.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(username.info.cryptoCurrencyAmount, currency: username.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(username.info.currencyAmount, currency: username.info.currency) + + let rawTextString = environment.strings.CollectibleItemInfo_UsernameText("@\(username.username)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + default: + break + } + } + case let .phoneNumber(phoneNumber): + let formattedPhoneNumber = formatPhoneNumber(context: component.context, number: phoneNumber.phoneNumber) + + let rawTitleString = environment.strings.CollectibleItemInfo_PhoneTitle("\(formattedPhoneNumber)") + titleText.append(NSAttributedString(string: rawTitleString.string, font: Font.semibold(16.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTitleString.ranges { + titleText.addAttributes([ + .foregroundColor: environment.theme.list.itemAccentColor, + NSAttributedString.Key(rawValue: "URL"): "" + ], range: range.range) + } + + let dateText = stringForDate(timestamp: phoneNumber.info.purchaseDate, strings: environment.strings) + + let (rawCryptoCurrencyText, cryptoCurrencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.cryptoCurrencyAmount, currency: phoneNumber.info.cryptoCurrency, customFormat: CurrencyFormatterEntry( + symbol: "~", + thousandsSeparator: ",", + decimalSeparator: ".", + symbolOnLeft: true, + spaceBetweenAmountAndSymbol: false, + decimalDigits: 9 + )) + var cryptoCurrencyText = rawCryptoCurrencyText + while cryptoCurrencyText.hasSuffix("0") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + if cryptoCurrencyText.hasSuffix(".") { + cryptoCurrencyText = String(cryptoCurrencyText[cryptoCurrencyText.startIndex ..< cryptoCurrencyText.index(before: cryptoCurrencyText.endIndex)]) + } + + let (currencyText, currencySign, _) = formatCurrencyAmountCustom(phoneNumber.info.currencyAmount, currency: phoneNumber.info.currency) + + let rawTextString = environment.strings.CollectibleItemInfo_PhoneText("\(formattedPhoneNumber)", environment.strings.CollectibleItemInfo_StoreName, dateText, "\(cryptoCurrencySign)\(cryptoCurrencyText)", "\(currencySign)\(currencyText)") + textText.append(NSAttributedString(string: rawTextString.string, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)) + for range in rawTextString.ranges { + switch range.index { + case 0: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 1: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + case 3: + textText.addAttribute(.font, value: Font.semibold(15.0), range: range.range) + default: + break + } + } + } + + let currencySymbolRange = (textText.string as NSString).range(of: "~") + + if self.currencySymbolIcon == nil { + if let templateImage = UIImage(bundleImageName: "Peer Info/CollectibleTonSymbolInline") { + self.currencySymbolIcon = generateImage(CGSize(width: templateImage.size.width, height: templateImage.size.height + 2.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let cgImage = templateImage.cgImage { + context.draw(cgImage, in: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: templateImage.size)) + } + })?.withRenderingMode(.alwaysTemplate) + } + } + + if currencySymbolRange.location != NSNotFound, let currencySymbolIcon = self.currencySymbolIcon { + textText.replaceCharacters(in: currencySymbolRange, with: "$") + textText.addAttribute(.attachment, value: currencySymbolIcon, range: currencySymbolRange) + + final class RunDelegateData { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + + init(ascent: CGFloat, descent: CGFloat, width: CGFloat) { + self.ascent = ascent + self.descent = descent + self.width = width + } + } + let font = Font.semibold(15.0) + let runDelegateData = RunDelegateData( + ascent: font.ascender, + descent: font.descender, + width: currencySymbolIcon.size.width + 4.0 + ) + var callbacks = CTRunDelegateCallbacks( + version: kCTRunDelegateCurrentVersion, + dealloc: { dataRef in + Unmanaged.fromOpaque(dataRef).release() + }, + getAscent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().ascent + }, + getDescent: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().descent + }, + getWidth: { dataRef in + let data = Unmanaged.fromOpaque(dataRef) + return data.takeUnretainedValue().width + } + ) + + if let runDelegate = CTRunDelegateCreate(&callbacks, Unmanaged.passRetained(runDelegateData).toOpaque()) { + textText.addAttribute(NSAttributedString.Key(rawValue: kCTRunDelegateAttributeName as String), value: runDelegate, range: currencySymbolRange) + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(titleText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.185 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + contentHeight += titleSize.height + contentHeight += 7.0 + + if let peer = component.initialData.peer { + let peerBadgeSize = self.peerBadge.update( + transition: transition, + component: AnyComponent(PeerBadgeComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: peer + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let peerBadgeFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - peerBadgeSize.width) * 0.5), y: contentHeight), size: peerBadgeSize) + if let peerBadgeView = self.peerBadge.view { + if peerBadgeView.superview == nil { + self.addSubview(peerBadgeView) + } + transition.setFrame(view: peerBadgeView, frame: peerBadgeFrame) + } + contentHeight += peerBadgeSize.height + contentHeight += 23.0 + } + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(textText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.185 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - contentSideInset * 2.0, height: 1000.0) + ) + let textFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentHeight), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setPosition(view: textView, position: textFrame.center) + textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size) + } + contentHeight += textSize.height + contentHeight += 21.0 + + let buttonSize = self.button.update( + transition: transition, + component: AnyComponent(ButtonComponent( + background: ButtonComponent.Background( + color: environment.theme.list.itemCheckColors.fillColor, + foreground: environment.theme.list.itemCheckColors.foregroundColor, + pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8) + ), + content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent( + Text(text: environment.strings.CollectibleItemInfo_ButtonOpenInfo, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor) + )), + isEnabled: true, + displaysProgress: false, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + + switch component.initialData.subject { + case let .username(username): + component.context.sharedContext.applicationBindings.openUrl(username.info.url) + case let .phoneNumber(phoneNumber): + component.context.sharedContext.applicationBindings.openUrl(phoneNumber.info.url) + } + + component.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + contentHeight += buttonSize.height + contentHeight += 5.0 + + let copyButtonTitle: String + switch component.initialData.subject { + case .username: + copyButtonTitle = environment.strings.CollectibleItemInfo_ButtonCopyUsername + case .phoneNumber: + copyButtonTitle = environment.strings.CollectibleItemInfo_ButtonCopyPhone + } + + + let copyButtonSize = self.copyButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: copyButtonTitle, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)) + )), + background: nil, + effectAlignment: .center, + minSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0), + contentInsets: UIEdgeInsets(), + action: { [weak self] in + guard let self, let component = self.component else { + return + } + + switch component.initialData.subject { + case let .username(username): + UIPasteboard.general.string = "https://t.me/\(username.username)" + case let .phoneNumber(phoneNumber): + let formattedPhoneNumber = formatPhoneNumber(context: component.context, number: phoneNumber.phoneNumber) + UIPasteboard.general.string = formattedPhoneNumber + } + + component.dismiss() + }, + isEnabled: true, + animateAlpha: true, + animateScale: false, + animateContents: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let copyButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: copyButtonSize) + if let copyButtonView = self.copyButton.view { + if copyButtonView.superview == nil { + self.addSubview(copyButtonView) + } + transition.setFrame(view: copyButtonView, frame: copyButtonFrame) + } + contentHeight += copyButtonSize.height - 9.0 + + if environment.safeInsets.bottom.isZero { + contentHeight += 16.0 + } else { + contentHeight += environment.safeInsets.bottom + 14.0 + } + + return CGSize(width: availableSize.width, height: contentHeight) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class CollectibleItemInfoScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: CollectibleItemInfoScreen.InitialData + + init( + context: AccountContext, + initialData: CollectibleItemInfoScreen.InitialData + ) { + self.context = context + self.initialData = initialData + } + + static func ==(lhs: CollectibleItemInfoScreenComponent, rhs: CollectibleItemInfoScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + return true + } + + final class View: UIView { + private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>() + private let sheetAnimateOut = ActionSlot>() + + private var component: CollectibleItemInfoScreenComponent? + private var environment: EnvironmentType? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: CollectibleItemInfoScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + self.environment = environment + + let sheetEnvironment = SheetComponentEnvironment( + isDisplaying: environment.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { [weak self] _ in + guard let self, let environment = self.environment else { + return + } + self.sheetAnimateOut.invoke(Action { _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + }) + } + ) + let _ = self.sheet.update( + transition: transition, + component: AnyComponent(SheetComponent( + content: AnyComponent(CollectibleItemInfoScreenContentComponent( + context: component.context, + initialData: component.initialData, + dismiss: { [weak self] in + guard let self else { + return + } + self.sheetAnimateOut.invoke(Action { [weak self] _ in + if let controller = environment.controller() { + controller.dismiss(completion: nil) + } + + guard let self else { + return + } + //TODO:open info + let _ = self + }) + } + )), + backgroundColor: .color(environment.theme.list.plainBackgroundColor), + animateOut: self.sheetAnimateOut + )), + environment: { + environment + sheetEnvironment + }, + containerSize: availableSize + ) + if let sheetView = self.sheet.view { + if sheetView.superview == nil { + self.addSubview(sheetView) + } + transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class CollectibleItemInfoScreen: ViewControllerComponentContainer { + fileprivate enum ResolvedSubject { + struct Username { + var username: String + var info: TelegramCollectibleItemInfo + + init(username: String, info: TelegramCollectibleItemInfo) { + self.username = username + self.info = info + } + } + + struct PhoneNumber { + var phoneNumber: String + var info: TelegramCollectibleItemInfo + + init(phoneNumber: String, info: TelegramCollectibleItemInfo) { + self.phoneNumber = phoneNumber + self.info = info + } + } + + case username(Username) + case phoneNumber(PhoneNumber) + } + + public final class InitialData: CollectibleItemInfoScreenInitialData { + fileprivate let peer: EnginePeer? + fileprivate let subject: ResolvedSubject + + fileprivate init(peer: EnginePeer?, subject: ResolvedSubject) { + self.peer = peer + self.subject = subject + } + + public var collectibleItemInfo: TelegramCollectibleItemInfo { + switch self.subject { + case let .username(username): + return username.info + case let .phoneNumber(phoneNumber): + return phoneNumber.info + } + } + } + + public init(context: AccountContext, initialData: InitialData) { + super.init(context: context, component: CollectibleItemInfoScreenComponent( + context: context, + initialData: initialData + ), navigationBarAppearance: .none) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public static func initialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal { + switch subject { + case let .username(username): + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + context.engine.peers.getCollectibleUsernameInfo(username: username) + ) + |> map { peer, result -> CollectibleItemInfoScreenInitialData? in + guard let result else { + return nil + } + return InitialData(peer: peer, subject: .username(ResolvedSubject.Username( + username: username, + info: result + ))) + } + case let .phoneNumber(phoneNumber): + return combineLatest( + context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ), + context.engine.peers.getCollectiblePhoneNumberInfo(phoneNumber: phoneNumber) + ) + |> map { peer, result -> CollectibleItemInfoScreenInitialData? in + guard let result else { + return nil + } + return InitialData(peer: peer, subject: .phoneNumber(ResolvedSubject.PhoneNumber( + phoneNumber: phoneNumber, + info: result + ))) + } + } + } + + deinit { + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + } +} diff --git a/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift new file mode 100644 index 0000000000..288cbffad0 --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/CollectibleItemInfoScreen/Sources/CollectibleItemInfoScreenContentComponent.swift @@ -0,0 +1,333 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import AppBundle +import BundleIconComponent +import Markdown +import TelegramCore + +/*public final class CollectibleItemInfoScreenContentComponent: Component { + public let theme: PresentationTheme + public let strings: PresentationStrings + public let settings: GlobalPrivacySettings + public let openSettings: () -> Void + + public init( + theme: PresentationTheme, + strings: PresentationStrings, + settings: GlobalPrivacySettings, + openSettings: @escaping () -> Void + ) { + self.theme = theme + self.strings = strings + self.settings = settings + self.openSettings = openSettings + } + + public static func ==(lhs: CollectibleItemInfoScreenContentComponent, rhs: CollectibleItemInfoScreenContentComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.settings != rhs.settings { + return false + } + return true + } + + private final class Item { + let icon = ComponentView() + let title = ComponentView() + let text = ComponentView() + + init() { + } + } + + public final class View: UIView { + private let scrollView: UIScrollView + private let iconBackground: UIImageView + private let iconForeground: UIImageView + + private let title = ComponentView() + private let mainText = ComponentView() + + private var chevronImage: UIImage? + + private var items: [Item] = [] + + private var component: CollectibleItemInfoScreenContentComponent? + + public override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.iconBackground = UIImageView() + self.iconForeground = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.scrollView) + + self.scrollView.delaysContentTouches = false + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.clipsToBounds = false + + self.scrollView.addSubview(self.iconBackground) + self.scrollView.addSubview(self.iconForeground) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let result = super.hitTest(point, with: event) { + return result + } else { + return nil + } + } + + func update(component: CollectibleItemInfoScreenContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + let sideIconInset: CGFloat = 40.0 + + var contentHeight: CGFloat = 0.0 + + let iconSize: CGFloat = 90.0 + if self.iconBackground.image == nil { + let backgroundColors = component.theme.chatList.pinnedArchiveAvatarColor.backgroundColors.colors + let colors: NSArray = [backgroundColors.1.cgColor, backgroundColors.0.cgColor] + self.iconBackground.image = generateGradientFilledCircleImage(diameter: iconSize, colors: colors) + } + let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize) * 0.5), y: contentHeight), size: CGSize(width: iconSize, height: iconSize)) + transition.setFrame(view: self.iconBackground, frame: iconBackgroundFrame) + + if self.iconForeground.image == nil { + self.iconForeground.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/ArchiveIconLarge"), color: .white) + } + if let image = self.iconForeground.image { + transition.setFrame(view: self.iconForeground, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) + } + + contentHeight += iconSize + contentHeight += 15.0 + + let titleString = NSMutableAttributedString() + titleString.append(NSAttributedString(string: component.strings.ArchiveInfo_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + let imageAttachment = NSTextAttachment() + imageAttachment.image = self.iconBackground.image + titleString.append(NSAttributedString(attachment: imageAttachment)) + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(titleString), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 16.0 + + let text: String + if component.settings.keepArchivedUnmuted { + text = component.strings.ArchiveInfo_TextKeepArchivedUnmuted + } else { + text = component.strings.ArchiveInfo_TextKeepArchivedDefault + } + + let mainText = NSMutableAttributedString() + mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + bold: MarkdownAttributeSet( + font: Font.semibold(15.0), + textColor: component.theme.list.itemSecondaryTextColor + ), + link: MarkdownAttributeSet( + font: Font.regular(15.0), + textColor: component.theme.list.itemAccentColor, + additionalAttributes: [:] + ), + linkAttribute: { attributes in + return ("URL", "") + } + ))) + if self.chevronImage == nil { + self.chevronImage = UIImage(bundleImageName: "Settings/TextArrowRight") + } + if let range = mainText.string.range(of: ">"), let chevronImage = self.chevronImage { + mainText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: mainText.string)) + } + + let mainTextSize = self.mainText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(mainText), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2, + highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.1), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + }, + tapAction: { [weak self] _, _ in + guard let self, let component = self.component else { + return + } + component.openSettings() + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + if let mainTextView = self.mainText.view { + if mainTextView.superview == nil { + self.scrollView.addSubview(mainTextView) + } + transition.setFrame(view: mainTextView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - mainTextSize.width) * 0.5), y: contentHeight), size: mainTextSize)) + } + contentHeight += mainTextSize.height + + contentHeight += 24.0 + + struct ItemDesc { + var icon: String + var title: String + var text: String + } + let itemDescs: [ItemDesc] = [ + ItemDesc( + icon: "Chat List/Archive/IconArchived", + title: component.strings.ArchiveInfo_ChatsTitle, + text: component.strings.ArchiveInfo_ChatsText + ), + ItemDesc( + icon: "Chat List/Archive/IconHide", + title: component.strings.ArchiveInfo_HideTitle, + text: component.strings.ArchiveInfo_HideText + ), + ItemDesc( + icon: "Chat List/Archive/IconStories", + title: component.strings.ArchiveInfo_StoriesTitle, + text: component.strings.ArchiveInfo_StoriesText + ) + ] + for i in 0 ..< itemDescs.count { + if i != 0 { + contentHeight += 24.0 + } + + let item: Item + if self.items.count > i { + item = self.items[i] + } else { + item = Item() + self.items.append(item) + } + + let itemDesc = itemDescs[i] + + let iconSize = item.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: itemDesc.icon, + tintColor: component.theme.list.itemAccentColor + )), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + let titleSize = item.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.title, font: Font.semibold(15.0), textColor: component.theme.list.itemPrimaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + let textSize = item.text.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: itemDesc.text, font: Font.regular(15.0), textColor: component.theme.list.itemSecondaryTextColor)), + maximumNumberOfLines: 0, + lineSpacing: 0.18 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - sideIconInset, height: 1000.0) + ) + + if let iconView = item.icon.view { + if iconView.superview == nil { + self.scrollView.addSubview(iconView) + } + transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight + 4.0), size: iconSize)) + } + + if let titleView = item.title.view { + if titleView.superview == nil { + self.scrollView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: titleSize)) + } + contentHeight += titleSize.height + contentHeight += 2.0 + + if let textView = item.text.view { + if textView.superview == nil { + self.scrollView.addSubview(textView) + } + transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: sideInset + sideIconInset, y: contentHeight), size: textSize)) + } + contentHeight += textSize.height + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + let size = CGSize(width: availableSize.width, height: min(availableSize.height, contentSize.height)) + if self.scrollView.bounds.size != size || self.scrollView.contentSize != contentSize { + self.scrollView.frame = CGRect(origin: CGPoint(), size: size) + self.scrollView.contentSize = contentSize + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} +*/ diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json new file mode 100644 index 0000000000..4488755ba2 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ton_12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf new file mode 100644 index 0000000000..238337b6c9 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleTonSymbolInline.imageset/ton_12.pdf @@ -0,0 +1,83 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.143555 0.130127 cm +0.000000 0.000000 0.000000 scn +5.156074 3.095078 m +1.539963 9.669825 l +5.156074 9.669825 l +5.156074 3.095078 l +h +6.556074 3.095079 m +10.172184 9.669825 l +6.556074 9.669825 l +6.556074 3.095079 l +h +1.201709 11.069824 m +0.288984 11.069824 -0.289608 10.091264 0.150250 9.291522 c +4.804615 0.829041 l +5.260527 0.000110 6.451622 0.000113 6.907533 0.829041 c +11.561897 9.291523 l +12.001758 10.091268 11.423159 11.069824 10.510438 11.069824 c +1.201709 11.069824 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 553 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 12.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000643 00000 n +0000000665 00000 n +0000000838 00000 n +0000000912 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +971 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json new file mode 100644 index 0000000000..6a062695f3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "username.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf new file mode 100644 index 0000000000..548f72b27c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Peer Info/CollectibleUsernameInfoTitleIcon.imageset/username.pdf @@ -0,0 +1,100 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 19.254883 19.005127 cm +0.000000 0.000000 0.000000 scn +0.000000 25.994989 m +0.000000 40.351631 11.638357 51.989990 25.995001 51.989990 c +40.351643 51.989990 51.989998 40.351631 51.989998 25.994989 c +51.989998 19.994989 l +51.989998 15.579475 48.410515 11.999992 43.994999 11.999992 c +40.680130 11.999992 37.836452 14.017384 36.624741 16.891380 c +34.058002 13.897156 30.248056 11.999989 25.995001 11.999989 c +18.265776 11.999989 12.000000 18.265766 12.000000 25.994989 c +12.000000 33.724213 18.265776 39.989990 25.995001 39.989990 c +29.919403 39.989990 33.466534 38.374702 36.007809 35.772713 c +36.097725 36.791267 36.953087 37.589989 37.994999 37.589989 c +39.096806 37.589989 39.989998 36.696800 39.989998 35.594986 c +39.989998 26.005434 l +39.990002 25.994989 l +39.989998 25.984545 l +39.989998 19.994995 l +39.989998 17.783092 41.783100 15.989990 43.994999 15.989990 c +46.206898 15.989990 48.000000 17.783092 48.000000 19.994989 c +48.000000 25.994989 l +48.000000 38.148018 38.148026 47.999989 25.995001 47.999989 c +13.841973 47.999989 3.990000 38.148018 3.990000 25.994989 c +3.990000 25.793646 3.992699 25.592970 3.998061 25.392994 c +4.317001 13.518284 14.043282 3.989990 25.995001 3.989990 c +29.449270 3.989990 32.712318 4.784481 35.616467 6.198845 c +36.607044 6.681271 37.801151 6.269333 38.283577 5.278755 c +38.766006 4.288174 38.354069 3.094070 37.363487 2.611641 c +33.926983 0.938011 30.067709 -0.000008 25.995001 -0.000008 c +11.638357 -0.000008 0.000000 11.638348 0.000000 25.994989 c +h +25.995001 35.999992 m +31.520609 35.999992 35.999996 31.520597 35.999996 25.994989 c +35.999996 20.469381 31.520609 15.989994 25.995001 15.989994 c +20.469393 15.989994 15.990000 20.469381 15.990000 25.994989 c +15.990000 31.520597 20.469393 35.999992 25.995001 35.999992 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1836 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 90.000000 90.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001926 00000 n +0000001949 00000 n +0000002122 00000 n +0000002196 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2255 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/ToastCollectibleUsernameEmoji.tgs b/submodules/TelegramUI/Resources/Animations/ToastCollectibleUsernameEmoji.tgs new file mode 100644 index 0000000000..f93fd6d746 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/ToastCollectibleUsernameEmoji.tgs differ diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b834c54245..99a8efb455 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -55,6 +55,7 @@ import ChatbotSetupScreen import BusinessLocationSetupScreen import BusinessHoursSetupScreen import AutomaticBusinessMessageSetupScreen +import CollectibleItemInfoScreen private final class AccountUserInterfaceInUseContext { let subscribers = Bag<(Bool) -> Void>() @@ -1923,6 +1924,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { return QuickReplySetupScreen.initialData(context: context) } + public func makeCollectibleItemInfoScreen(context: AccountContext, initialData: CollectibleItemInfoScreenInitialData) -> ViewController { + return CollectibleItemInfoScreen(context: context, initialData: initialData as! CollectibleItemInfoScreen.InitialData) + } + + public func makeCollectibleItemInfoScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, subject: CollectibleItemInfoScreenSubject) -> Signal { + return CollectibleItemInfoScreen.initialData(context: context, peerId: peerId, subject: subject) + } + public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController { var modal = true let mappedSource: PremiumSource