diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e567fb6ecb..43657d0071 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12490,3 +12490,15 @@ Sorry for the inconvenience."; "WebApp.Miniapp" = "miniapp"; "WebApp.Share" = "Share"; + +"Stars.Purchase.GiftStars" = "Gift Stars"; +"Stars.Purchase.GiftInfo" = "With Stars, **%1$@** will be able to unlock content and services on Telegram. [See Examples >]()"; +"Notification.StarsGift.Sent" = "%1$@ sent you a gift for %2$@"; +"Notification.StarsGift.SentYou" = "You sent a gift for %@"; + +"Notification.StarsGift.Title_1" = "%@ Star"; +"Notification.StarsGift.Title_any" = "%@ Stars"; +"Notification.StarsGift.Subtitle" = "Use Stars to unlock content and services on Telegram."; +"Notification.StarsGift.SubtitleYou" = "With Stars, %@ will be able to unlock content and services on Telegram."; + +"Bot.Settings" = "Bot Settings"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index d3c799cb2c..3d464347d6 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1040,7 +1040,7 @@ public protocol SharedAccountContext: AnyObject { func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController func makePremiumLimitController(context: AccountContext, subject: PremiumLimitSubject, count: Int32, forceDark: Bool, cancel: @escaping () -> Void, action: @escaping () -> Bool) -> ViewController - func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController + func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController func makePremiumPrivacyControllerController(context: AccountContext, subject: PremiumPrivacySubject, peerId: EnginePeer.Id) -> ViewController func makePremiumBoostLevelsController(context: AccountContext, peerId: EnginePeer.Id, subject: BoostSubject, boostStatus: ChannelBoostStatus, myBoostStatus: MyBoostStatus, forceDark: Bool, openStats: (() -> Void)?) -> ViewController @@ -1064,13 +1064,14 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController - func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction, peer: EnginePeer) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/ContactMultiselectionController.swift b/submodules/AccountContext/Sources/ContactMultiselectionController.swift index 8bbc7dd68d..1bd063097a 100644 --- a/submodules/AccountContext/Sources/ContactMultiselectionController.swift +++ b/submodules/AccountContext/Sources/ContactMultiselectionController.swift @@ -78,7 +78,7 @@ public enum ContactMultiselectionControllerMode { case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool) case channelCreation case chatSelection(ChatSelection) - case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool) + case premiumGifting(birthdays: [EnginePeer.Id: TelegramBirthday]?, selectToday: Bool, hasActions: Bool) case requestedUsersSelection } diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 46d6134111..821887ec3f 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -49,6 +49,7 @@ public enum PremiumGiftSource: Equatable { case attachMenu case settings([EnginePeer.Id: TelegramBirthday]?) case chatList([EnginePeer.Id: TelegramBirthday]?) + case stars([EnginePeer.Id: TelegramBirthday]?) case channelBoost case deeplink(String?) } @@ -121,6 +122,14 @@ public enum BoostSubject: Equatable { case noAds } +public enum StarsPurchasePurpose: Equatable { + case generic + case transfer(peerId: EnginePeer.Id, requiredStars: Int64) + case subscription(peerId: EnginePeer.Id, requiredStars: Int64, renew: Bool) + case gift(peerId: EnginePeer.Id) + case unlockMedia(requiredStars: Int64) +} + public struct PremiumConfiguration { public static var defaultValue: PremiumConfiguration { return PremiumConfiguration( diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 89f45ecd9f..978d2f936d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -173,6 +173,10 @@ public extension AttachmentContainable { return nil } + var minimizedProgress: Float? { + return nil + } + var isPanGestureEnabled: (() -> Bool)? { return nil } @@ -336,7 +340,9 @@ public class AttachmentController: ViewController, MinimizableController { public private(set) var minimizedTopEdgeOffset: CGFloat? public private(set) var minimizedBounds: CGRect? - public private(set) var minimizedIcon: UIImage? + public var minimizedIcon: UIImage? { + return self.mainController.minimizedIcon + } private final class Node: ASDisplayNode { private weak var controller: AttachmentController? diff --git a/submodules/AttachmentUI/Sources/BackButtonNode.swift b/submodules/AttachmentUI/Sources/BackButtonNode.swift new file mode 100644 index 0000000000..5da245a593 --- /dev/null +++ b/submodules/AttachmentUI/Sources/BackButtonNode.swift @@ -0,0 +1,157 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import TelegramPresentationData + +public class WebAppCancelButtonNode: ASDisplayNode { + public enum State { + case cancel + case back + } + + public let buttonNode: HighlightTrackingButtonNode + private let arrowNode: ASImageNode + private let labelNode: ImmediateTextNode + + public var state: State = .cancel + + private var color: UIColor? + + private var _theme: PresentationTheme + public var theme: PresentationTheme { + get { + return self._theme + } + set { + self._theme = newValue + self.setState(self.state, animated: false, animateScale: false, force: true) + } + } + private let strings: PresentationStrings + + private weak var colorSnapshotView: UIView? + + public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { + let previousColor = self.color + self.color = color + + if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange { + if let snapshotView = self.view.snapshotContentTree() { + snapshotView.frame = self.bounds + self.view.addSubview(snapshotView) + self.colorSnapshotView = snapshotView + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) + } + } + self.setState(self.state, animated: false, animateScale: false, force: true) + } + + public init(theme: PresentationTheme, strings: PresentationStrings) { + self._theme = theme + self.strings = strings + + self.buttonNode = HighlightTrackingButtonNode() + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + + self.labelNode = ImmediateTextNode() + self.labelNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.buttonNode) + self.buttonNode.addSubnode(self.arrowNode) + self.buttonNode.addSubnode(self.labelNode) + + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") + strongSelf.arrowNode.alpha = 0.4 + strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") + strongSelf.labelNode.alpha = 0.4 + } else { + strongSelf.arrowNode.alpha = 1.0 + strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.labelNode.alpha = 1.0 + strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + + self.setState(.cancel, animated: false, force: true) + } + + public func setTheme(_ theme: PresentationTheme, animated: Bool) { + self._theme = theme + var animated = animated + if self.animatingStateChange { + animated = false + } + self.setState(self.state, animated: animated, animateScale: false, force: true) + } + + private var animatingStateChange = false + public func setState(_ state: State, animated: Bool, animateScale: Bool = true, force: Bool = false) { + guard self.state != state || force else { + return + } + self.state = state + + if let colorSnapshotView = self.colorSnapshotView { + self.colorSnapshotView = nil + colorSnapshotView.removeFromSuperview() + } + + if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() { + self.animatingStateChange = true + snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform + self.view.addSubview(snapshotView) + + let duration: Double = animateScale ? 0.25 : 0.3 + if animateScale { + snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false) + } + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + self.animatingStateChange = false + }) + + if animateScale { + self.buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.25) + } + self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + } + + let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor + + self.arrowNode.isHidden = state == .cancel + self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Close : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) + + let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) + + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) + self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) + if let image = self.arrowNode.image { + self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) + } + self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize) + self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(state == .back ? 11.0 : 0.0, 0.0, 0.0) + } + + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height)) + self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size) + self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size) + + return CGSize(width: 70.0, height: 56.0) + } +} diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index d3e337a910..d92faaf470 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramCore", "//submodules/TelegramPresentationData", "//submodules/TelegramUIPreferences", + "//submodules/PresentationDataUtils", "//submodules/AppBundle", "//submodules/InstantPageUI", "//submodules/ContextUI", @@ -30,6 +31,13 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/Pasteboard", "//submodules/SaveToCameraRoll", + "//submodules/TelegramUI/Components/NavigationStackComponent", + "//submodules/LocationUI", + "//submodules/OpenInExternalAppUI", + "//submodules/GalleryUI", + "//submodules/TelegramUI/Components/ContextReferenceButtonComponent", + "//submodules/Svg", + "//submodules/PromptUI", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserContent.swift b/submodules/BrowserUI/Sources/BrowserContent.swift index df4444a903..b3b85aa900 100644 --- a/submodules/BrowserUI/Sources/BrowserContent.swift +++ b/submodules/BrowserUI/Sources/BrowserContent.swift @@ -1,7 +1,9 @@ import Foundation import UIKit +import Display import ComponentFlow import SwiftSignalKit +import WebKit final class BrowserContentState: Equatable { enum ContentType: Equatable { @@ -9,28 +11,62 @@ final class BrowserContentState: Equatable { case instantPage } + struct HistoryItem: Equatable { + let url: String + let title: String + let uuid: UUID? + let webItem: WKBackForwardListItem? + + init(url: String, title: String, uuid: UUID) { + self.url = url + self.title = title + self.uuid = uuid + self.webItem = nil + } + + init(webItem: WKBackForwardListItem) { + self.url = webItem.url.absoluteString + self.title = webItem.title ?? "" + self.uuid = nil + self.webItem = nil + } + } + let title: String let url: String let estimatedProgress: Double + let readingProgress: Double let contentType: ContentType + let favicon: UIImage? - var canGoBack: Bool - var canGoForward: Bool + let canGoBack: Bool + let canGoForward: Bool + + let backList: [HistoryItem] + let forwardList: [HistoryItem] init( title: String, url: String, estimatedProgress: Double, + readingProgress: Double, contentType: ContentType, + favicon: UIImage? = nil, canGoBack: Bool = false, - canGoForward: Bool = false + canGoForward: Bool = false, + backList: [HistoryItem] = [], + forwardList: [HistoryItem] = [] ) { self.title = title self.url = url self.estimatedProgress = estimatedProgress + self.readingProgress = readingProgress self.contentType = contentType + self.favicon = favicon self.canGoBack = canGoBack self.canGoForward = canGoForward + self.backList = backList + self.forwardList = forwardList } static func == (lhs: BrowserContentState, rhs: BrowserContentState) -> Bool { @@ -43,42 +79,80 @@ final class BrowserContentState: Equatable { if lhs.estimatedProgress != rhs.estimatedProgress { return false } + if lhs.readingProgress != rhs.readingProgress { + return false + } if lhs.contentType != rhs.contentType { return false } + if (lhs.favicon == nil) != (rhs.favicon == nil) { + return false + } if lhs.canGoBack != rhs.canGoBack { return false } if lhs.canGoForward != rhs.canGoForward { return false } + if lhs.backList != rhs.backList { + return false + } + if lhs.forwardList != rhs.forwardList { + return false + } return true } func withUpdatedTitle(_ title: String) -> BrowserContentState { - return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedUrl(_ url: String) -> BrowserContentState { - return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: canGoBack, canGoForward: self.canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList) } func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState { - return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, contentType: self.contentType, canGoBack: self.canGoBack, canGoForward: canGoForward) + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList) + } + + func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList) + } + + func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState { + return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList) } } protocol BrowserContent: UIView { + var uuid: UUID { get } + + var currentState: BrowserContentState { get } var state: Signal { get } + var pushContent: (BrowserScreen.Subject) -> Void { get set } + var present: (ViewController, Any?) -> Void { get set } + var presentInGlobalOverlay: (ViewController) -> Void { get set } + var getNavigationController: () -> NavigationController? { get set } + + var minimize: () -> Void { get set } + var onScrollingUpdate: (ContentScrollingUpdate) -> Void { get set } func reload() @@ -86,6 +160,7 @@ protocol BrowserContent: UIView { func navigateBack() func navigateForward() + func navigateTo(historyItem: BrowserContentState.HistoryItem) func setFontSize(_ fontSize: CGFloat) func setForceSerif(_ force: Bool) diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 9138570b48..25a950e245 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -17,14 +17,30 @@ import ContextUI import Pasteboard import SaveToCameraRoll import ShareController +import SafariServices +import LocationUI +import OpenInExternalAppUI +import GalleryUI -private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegate { +final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate { private let context: AccountContext - private let webPage: TelegramMediaWebpage private let presentationData: PresentationData private let theme: InstantPageTheme private let sourceLocation: InstantPageSourceLocation + private var webPage: TelegramMediaWebpage? + + let uuid: UUID + + var currentState: BrowserContentState { + return self._state + } + private var _state: BrowserContentState + private let statePromise: Promise + var state: Signal { + return self.statePromise.get() + } + private var initialAnchor: String? private var pendingAnchor: String? private var initialState: InstantPageStoredState? @@ -48,34 +64,66 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat var currentAccessibilityAreas: [AccessibilityAreaNode] = [] + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } - var openMedia: (InstantPageMedia) -> Void = { _ in } - var longPressMedia: (InstantPageMedia) -> Void = { _ in } + var minimize: () -> Void = { } + var openPeer: (EnginePeer) -> Void = { _ in } - var openUrl: (InstantPageUrlItem) -> Void = { _ in } - var activatePinchPreview: ((PinchSourceContainerNode) -> Void)? - var pinchPreviewFinished: ((InstantPageNode) -> Void)? var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var push: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } + private var webpageDisposable: Disposable? + private let hiddenMediaDisposable = MetaDisposable() + private let loadWebpageDisposable = MetaDisposable() + private let resolveUrlDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable() + + private let loadProgress = ValuePromise(1.0, ignoreRepeated: true) + private let readingProgress = ValuePromise(1.0, ignoreRepeated: true) private var containerLayout: (size: CGSize, insets: UIEdgeInsets)? + private var setupScrollOffsetOnLayout = false - init(context: AccountContext, webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation) { + init(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation) { self.context = context self.webPage = webPage self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = instantPageThemeForType(.light, settings: .defaultSettings) self.sourceLocation = sourceLocation + self.uuid = UUID() + + let title: String + if case let .Loaded(content) = webPage.content { + title = content.title ?? "" + } else { + title = "" + } + + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage) + self.statePromise = Promise(self._state) + self.scrollNode = ASScrollNode() self.scrollNode.backgroundColor = self.theme.pageBackgroundColor self.scrollNodeFooter = ASDisplayNode() self.scrollNodeFooter.backgroundColor = self.theme.panelBackgroundColor - super.init() + super.init(frame: .zero) + + self.statePromise.set(.single(self._state) + |> then( + combineLatest( + self.loadProgress.get(), + self.readingProgress.get() + ) + |> map { estimatedProgress, readingProgress in + return BrowserContentState(title: title, url: url, estimatedProgress: estimatedProgress, readingProgress: readingProgress, contentType: .instantPage) + } + )) self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.scrollNodeFooter) @@ -101,9 +149,21 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat } } self.scrollNode.view.addGestureRecognizer(recognizer) + + self.webpageDisposable = (actualizedWebpage(account: context.account, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + self.webPage = result + self.updateWebPage(result, anchor: self.initialAnchor) + }) } deinit { + self.webpageDisposable?.dispose() + self.hiddenMediaDisposable.dispose() + self.loadWebpageDisposable.dispose() + self.resolveUrlDisposable.dispose() self.updateLayoutDisposable.dispose() } @@ -184,25 +244,160 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat } } + private func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) { + if self.webPage != webPage { + if self.webPage != nil && self.currentLayout != nil { + if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) { + self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view) + snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in + snaphotView?.removeFromSuperview() + }) + } + } + + self.setupScrollOffsetOnLayout = self.webPage == nil + self.webPage = webPage + if let anchor = anchor { + self.initialAnchor = anchor.removingPercentEncoding + } else if let state = state { + self.initialState = state + if !state.details.isEmpty { + var storedExpandedDetails: [Int: Bool] = [:] + for state in state.details { + storedExpandedDetails[Int(clamping: state.index)] = state.expanded + } + self.currentExpandedDetails = storedExpandedDetails + } + } + self.currentLayout = nil + self.updatePageLayout() + + self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) + self.requestLayout(transition: .immediate) + + if let webPage = webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + self.loadProgress.set(1.0) + + if let anchor = self.pendingAnchor { + self.pendingAnchor = nil + self.scrollToAnchor(anchor) + } + } + } + } + + private func requestLayout(transition: ContainedViewLayoutTransition) { + guard let (size, insets) = self.containerLayout else { + return + } + self.updateLayout(size: size, insets: insets, transition: transition) + } + + func reload() { + } + + func stop() { + } + + func navigateBack() { + + } + + func navigateForward() { + + } + + func navigateTo(historyItem: BrowserContentState.HistoryItem) { + + } + + func setFontSize(_ fontSize: CGFloat) { + + } + + func setForceSerif(_ force: Bool) { + + } + + func setSearch(_ query: String?, completion: ((Int) -> Void)?) { + + } + + func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { + + } + + func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { + + } + + func scrollToTop() { + let scrollView = self.scrollNode.view + scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) + } + + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { + self.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition) + } + func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.containerLayout = (size, insets) - + + var updateVisibleItems = false + let resetContentOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty + var scrollInsets = insets scrollInsets.top = 0.0 if self.scrollNode.view.contentInset != insets { self.scrollNode.view.contentInset = scrollInsets self.scrollNode.view.scrollIndicatorInsets = scrollInsets } - self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)) - if self.currentLayout?.contentSize.width != size.width { - self.updatePageLayout() + let scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)) + let scrollFrameUpdated = self.scrollNode.bounds.size != scrollFrame.size + if scrollFrameUpdated { + let widthUpdated = self.scrollNode.bounds.size.width != scrollFrame.width + self.scrollNode.frame = scrollFrame + if widthUpdated { + self.updatePageLayout() + } + updateVisibleItems = true + } + + if resetContentOffset { + var didSetScrollOffset = false + var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top) + if let state = self.initialState { + didSetScrollOffset = true + contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset)) + } + else if let anchor = self.initialAnchor, !anchor.isEmpty { + self.initialAnchor = nil + if let items = self.currentLayout?.items { + didSetScrollOffset = true + if let (item, lineOffset, _, _) = self.findAnchorItem(anchor, items: items) { + contentOffset = CGPoint(x: 0.0, y: item.frame.minY + lineOffset - self.scrollNode.view.contentInset.top) + } + } + } else { + didSetScrollOffset = true + } + self.scrollNode.view.contentOffset = contentOffset + if didSetScrollOffset { + //update scroll event + if self.currentLayout != nil { + self.setupScrollOffsetOnLayout = false + } + } + } + + if updateVisibleItems { self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) } } private func updatePageLayout() { - guard let (size, insets) = self.containerLayout else { + guard let (size, insets) = self.containerLayout, let webPage = self.webPage else { return } @@ -355,32 +550,9 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat }, longPressMedia: { [weak self] media in self?.longPressMedia(media) }, activatePinchPreview: { [weak self] sourceNode in - let _ = self -// guard let strongSelf = self, let controller = strongSelf.controller else { -// return -// } -// let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { -// guard let strongSelf = self else { -// return CGRect() -// } -// -// let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY)) -// return strongSelf.view.convert(localRect, to: nil) -// }) -// controller.window?.presentInGlobalOverlay(pinchController) + self?.activatePinchPreview(sourceNode: sourceNode) }, pinchPreviewFinished: { [weak self] itemNode in - let _ = self -// guard let strongSelf = self else { -// return -// } -// for (_, listItemNode) in strongSelf.visibleItemsWithNodes { -// if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { -// if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { -// listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) -// break -// } -// } -// } + self?.pinchPreviewFinished(itemNode: itemNode) }, openPeer: { [weak self] peerId in self?.openPeer(peerId) }, openUrl: { [weak self] url in @@ -547,6 +719,13 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat )) } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.readingProgress.set(readingProgress) } private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { @@ -645,6 +824,230 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat return nil } + private func openUrl(_ url: InstantPageUrlItem) { + var baseUrl = url.url + var anchor: String? + if let anchorRange = url.url.range(of: "#") { + anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding + baseUrl = String(baseUrl[.. deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.loadProgress.set(0.07) + switch result { + case let .externalUrl(externalUrl): + if let webpageId = url.webpageId { + var anchor: String? + if let anchorRange = externalUrl.range(of: "#") { + anchor = String(externalUrl[anchorRange.upperBound...]) + } + strongSelf.loadWebpageDisposable.set((webpagePreviewWithProgress(account: strongSelf.context.account, urls: [externalUrl], webpageId: webpageId) + |> deliverOnMainQueue).start(next: { result in + if let strongSelf = self { + switch result { + case let .result(webpageResult): + if let webpageResult = webpageResult, case .Loaded = webpageResult.webpage.content { + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.instantPage(webPage: webpageResult.webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation)) + } + break + case let .progress(progress): + strongSelf.loadProgress.set(CGFloat(0.07 + progress * (1.0 - 0.07))) + } + } + })) + } else { + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.webPage(url: externalUrl)) + } + case let .instantView(webpage, anchor): + strongSelf.loadProgress.set(1.0) + strongSelf.pushContent(.instantPage(webPage: webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation)) + default: + strongSelf.loadProgress.set(1.0) + strongSelf.minimize() + strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), forceExternal: false, openPeer: { peer, navigation in + switch navigation { + case let .chat(_, subject, peekData): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, peekData: peekData)) + } + case let .withBotStartPayload(botStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botStart: botStart, keepStack: .always)) + } + case let .withAttachBot(attachBotStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), attachBotStart: attachBotStart)) + } + case let .withBotApp(botAppStart): + if let navigationController = strongSelf.getNavigationController() { + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), botAppStart: botAppStart)) + } + case .info: + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) + |> deliverOnMainQueue).start(next: { peer in + if let strongSelf = self, let peer = peer { + if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + strongSelf.getNavigationController()?.pushViewController(controller) + } + } + }) + default: + break + } + }, + sendFile: nil, + sendSticker: nil, + sendEmoji: nil, + requestMessageActionUrlAuth: nil, + joinVoiceChat: nil, + present: { c, a in + self?.present(c, a) + }, dismissInput: { [weak self] in + self?.endEditing(true) + }, contentContext: nil, progress: nil, completion: nil) + } + } + })) + } + + private func openUrlIn(_ url: InstantPageUrlItem) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in + if let self { + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + } + }) + self.present(actionSheet, nil) + } + + private func openMedia(_ media: InstantPageMedia) { + guard let items = self.currentLayout?.items, let webPage = self.webPage else { + return + } + + func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] { + var medias: [InstantPageMedia] = [] + for item in items { + if let detailsItem = item as? InstantPageDetailsItem { + medias.append(contentsOf: mediasFromItems(detailsItem.items)) + } else { + if let item = item as? InstantPageImageItem, item.interactive { + medias.append(contentsOf: item.medias) + } else if let item = item as? InstantPagePlayableVideoItem, item.interactive { + medias.append(contentsOf: item.medias) + } + } + } + return medias + } + + if case let .geo(map) = media.media { + let controllerParams = LocationViewParams(sendLiveLocation: { _ in + }, stopLiveLocation: { _ in + }, openUrl: { _ in }, openPeer: { _ in + }, showAll: false) + + let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + + let controller = LocationViewController(context: self.context, subject: EngineMessage(message), params: controllerParams) + self.push(controller) + return + } + + if case let .file(file) = media.media, (file.isVoice || file.isMusic) { + var medias: [InstantPageMedia] = [] + var initialIndex = 0 + for item in items { + for itemMedia in item.medias { + if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) { + if itemMedia.index == media.index { + initialIndex = medias.count + } + medias.append(itemMedia) + } + } + } + self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play)) + return + } + + var fromPlayingVideo = false + + var entries: [InstantPageGalleryEntry] = [] + if case let .webpage(webPage) = media.media { + entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil)) + } else if case let .file(file) = media.media, file.isAnimated { + fromPlayingVideo = true + entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil)) + } else { + fromPlayingVideo = true + var medias: [InstantPageMedia] = mediasFromItems(items) + medias = medias.filter { item in + switch item.media { + case .image, .file: + return true + default: + return false + } + } + + for media in medias { + entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count)))) + } + } + + var centralIndex: Int? + for i in 0 ..< entries.count { + if entries[i].media == media { + centralIndex = i + break + } + } + + if let centralIndex = centralIndex { + let controller = InstantPageGalleryController(context: self.context, userLocation: self.sourceLocation.userLocation, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in + }, baseNavigationController: self.getNavigationController()) + self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithNodes { + itemNode.updateHiddenMedia(media: entry?.media) + } + } + })) + controller.openUrl = { [weak self] url in + self?.openUrl(url) + } + self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in + if let strongSelf = self { + for (_, itemNode) in strongSelf.visibleItemsWithNodes { + if let transitionNode = itemNode.transitionNode(media: entry.media) { + return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in + if let strongSelf = self { + strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view) + } + }) + } + } + } + return nil + })) + } + } + private func longPressMedia(_ media: InstantPageMedia) { let controller = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let self, let image = media.media._asMedia() as? TelegramMediaImage { @@ -657,74 +1060,94 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat let _ = saveToCameraRoll(context: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start() } }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in - if let self, let image = media.media._asMedia() as? TelegramMediaImage { - self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(self.webPage), media: image), resource: $0.resource)) }))), nil) + if let self, let webPage = self.webPage, let image = media.media._asMedia() as? TelegramMediaImage { + self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) } })], catchTapsOutside: true) self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - if let self { - for (_, itemNode) in self.visibleItemsWithNodes { - if let (node, _, _) = itemNode.transitionNode(media: media) { - return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds) - } - } + if let _ = self { +// for (_, itemNode) in self.visibleItemsWithNodes { +// if let (node, _, _) = itemNode.transitionNode(media: media) { +// return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds) +// } +// } } return nil })) } + private func activatePinchPreview(sourceNode: PinchSourceContainerNode) { + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { [weak self] in + guard let self else { + return CGRect() + } + let localRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height)) + return self.convert(localRect, to: nil) + }) + self.presentInGlobalOverlay(pinchController) + } + + private func pinchPreviewFinished(itemNode: ASDisplayNode) { + for (_, listItemNode) in self.visibleItemsWithNodes { + if let listItemNode = listItemNode as? InstantPagePeerReferenceNode { + if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 { + listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25) + break + } + } + } + } + @objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: - if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation { + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { switch gesture { case .tap: - break -// if let url = self.urlForTapLocation(location) { -// self.openUrl(url) -// } + if let url = self.urlForTapLocation(location) { + self.openUrl(url) + } case .longTap: - break -// if let theme = self.theme, let url = self.urlForTapLocation(location) { -// let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1 -// let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen -// let actionSheet = ActionSheetController(instantPageTheme: theme) -// actionSheet.setItemGroups([ActionSheetItemGroup(items: [ -// ActionSheetTextItem(title: url.url), -// ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in -// actionSheet?.dismissAnimated() -// if let strongSelf = self { -// if canOpenIn { -// strongSelf.openUrlIn(url) -// } else { -// strongSelf.openUrl(url) -// } -// } -// }), -// ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in -// actionSheet?.dismissAnimated() -// UIPasteboard.general.string = url.url -// }), -// ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in -// actionSheet?.dismissAnimated() -// if let link = URL(string: url.url) { -// let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) -// } -// }) -// ]), ActionSheetItemGroup(items: [ -// ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in -// actionSheet?.dismissAnimated() -// }) -// ])]) -// self.present(actionSheet, nil) -// } else if let (item, parentOffset) = self.textItemAtLocation(location) { -// let textFrame = item.frame -// var itemRects = item.lineRects() -// for i in 0 ..< itemRects.count { -// itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0) -// } -// self.updateTextSelectionRects(itemRects, text: item.plainText()) -// } + if let url = self.urlForTapLocation(location) { + let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1 + let openText = canOpenIn ? self.presentationData.strings.Conversation_FileOpenIn : self.presentationData.strings.Conversation_LinkDialogOpen + let actionSheet = ActionSheetController(instantPageTheme: self.theme) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: url.url), + ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + if canOpenIn { + strongSelf.openUrlIn(url) + } else { + strongSelf.openUrl(url) + } + } + }), + ActionSheetButtonItem(title: self.presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + UIPasteboard.general.string = url.url + }), + ActionSheetButtonItem(title: self.presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let link = URL(string: url.url) { + let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil) + } + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.present(actionSheet, nil) + } else if let (item, parentOffset) = self.textItemAtLocation(location) { + let textFrame = item.frame + var itemRects = item.lineRects() + for i in 0 ..< itemRects.count { + itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0) + } + self.updateTextSelectionRects(itemRects, text: item.plainText()) + } default: break } @@ -917,7 +1340,7 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat } self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true) } - } else if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage, !instantPage.isComplete { + } else if case let .Loaded(content) = self.webPage?.content, let instantPage = content.instantPage, !instantPage.isComplete { // self.loadProgress.set(0.5) self.pendingAnchor = anchor } @@ -952,89 +1375,3 @@ private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegat self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated) } } - -final class BrowserInstantPageContent: UIView, BrowserContent { - var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } - - private var _state: BrowserContentState - private let statePromise: Promise - - private let webPage: TelegramMediaWebpage - private var initialized = false - - private let instantPageNode: InstantPageContainerNode - - var state: Signal { - return self.statePromise.get() - } - - init(context: AccountContext, webPage: TelegramMediaWebpage, url: String, sourceLocation: InstantPageSourceLocation) { - self.webPage = webPage - - let title: String - if case let .Loaded(content) = webPage.content { - title = content.title ?? "" - } else { - title = "" - } - - self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage) - self.statePromise = Promise(self._state) - - self.instantPageNode = InstantPageContainerNode(context: context, webPage: webPage, sourceLocation: sourceLocation) - - super.init(frame: .zero) - - self.addSubnode(self.instantPageNode) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func reload() { - } - - func stop() { - } - - func navigateBack() { - - } - - func navigateForward() { - - } - - func setFontSize(_ fontSize: CGFloat) { - - } - - func setForceSerif(_ force: Bool) { - - } - - func setSearch(_ query: String?, completion: ((Int) -> Void)?) { - - } - - func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) { - - } - - func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) { - - } - - func scrollToTop() { - let scrollView = self.instantPageNode.scrollNode.view - scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true) - } - - func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) { -// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), additionalInsets: .zero, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false) - self.instantPageNode.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition) - self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) - //transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0))) - } -} diff --git a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift index c930499783..78f8778de2 100644 --- a/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserNavigationBarComponent.swift @@ -219,7 +219,7 @@ final class BrowserNavigationBarComponent: CombinedComponent { let maxCenterInset = max(centerLeftInset, centerRightInset) if !leftItemList.isEmpty || !rightItemList.isEmpty { - availableWidth -= 20.0 + availableWidth -= 28.0 } let centerItem = context.component.centerItem.flatMap { item in diff --git a/submodules/BrowserUI/Sources/BrowserScreen.swift b/submodules/BrowserUI/Sources/BrowserScreen.swift index c98dc9da0b..b8b9cc08f6 100644 --- a/submodules/BrowserUI/Sources/BrowserScreen.swift +++ b/submodules/BrowserUI/Sources/BrowserScreen.swift @@ -16,6 +16,7 @@ import OpenInExternalAppUI import MultilineTextComponent import MinimizedContainer import InstantPageUI +import NavigationStackComponent private let settingsTag = GenericComponentViewTag() @@ -26,6 +27,7 @@ private final class BrowserScreenComponent: CombinedComponent { let contentState: BrowserContentState? let presentationState: BrowserPresentationState let performAction: ActionSlot + let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void let panelCollapseFraction: CGFloat init( @@ -33,12 +35,14 @@ private final class BrowserScreenComponent: CombinedComponent { contentState: BrowserContentState?, presentationState: BrowserPresentationState, performAction: ActionSlot, + performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void, panelCollapseFraction: CGFloat ) { self.context = context self.contentState = contentState self.presentationState = presentationState self.performAction = performAction + self.performHoldAction = performHoldAction self.panelCollapseFraction = panelCollapseFraction } @@ -72,7 +76,8 @@ private final class BrowserScreenComponent: CombinedComponent { return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let performAction = context.component.performAction - + let performHoldAction = context.component.performHoldAction + let navigationContent: AnyComponentWithIdentity? var navigationLeftItems: [AnyComponentWithIdentity] var navigationRightItems: [AnyComponentWithIdentity] @@ -172,7 +177,7 @@ private final class BrowserScreenComponent: CombinedComponent { leftItems: navigationLeftItems, rightItems: navigationRightItems, centerItem: navigationContent, - readingProgress: 0.0, + readingProgress: context.component.contentState?.readingProgress ?? 0.0, loadingProgress: context.component.contentState?.estimatedProgress, collapseFraction: collapseFraction ), @@ -206,7 +211,8 @@ private final class BrowserScreenComponent: CombinedComponent { textColor: environment.theme.rootController.navigationBar.primaryTextColor, canGoBack: context.component.contentState?.canGoBack ?? false, canGoForward: context.component.contentState?.canGoForward ?? false, - performAction: performAction + performAction: performAction, + performHoldAction: performHoldAction ) ) ) @@ -275,17 +281,18 @@ public class BrowserScreen: ViewController, MinimizableController { private weak var controller: BrowserScreen? private let context: AccountContext - private let contentContainerView: UIView - fileprivate var content: BrowserContent? + private let contentContainerView = UIView() + fileprivate let contentNavigationContainer = ComponentView() + fileprivate var content: [BrowserContent] = [] - private var contentState: BrowserContentState? - private var contentStateDisposable: Disposable? + fileprivate var contentState: BrowserContentState? + private var contentStateDisposable = MetaDisposable() private var presentationState: BrowserPresentationState - private let performAction: ActionSlot + private let performAction = ActionSlot() - fileprivate let componentHost: ComponentView + fileprivate let componentHost = ComponentView() private var presentationData: PresentationData private var validLayout: (ContainerViewLayout, CGFloat)? @@ -296,41 +303,13 @@ public class BrowserScreen: ViewController, MinimizableController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.presentationState = BrowserPresentationState(fontSize: 100, fontIsSerif: false, isSearching: false, searchResultIndex: 0, searchResultCount: 0, searchQueryIsEmpty: true) - - self.performAction = ActionSlot() - - self.contentContainerView = UIView() - self.contentContainerView.clipsToBounds = true - - self.componentHost = ComponentView() - + super.init() - let content: BrowserContent - switch controller.subject { - case let .webPage(url): - content = BrowserWebContent(context: controller.context, url: url) - case let .instantPage(webPage, sourceLocation): - content = BrowserInstantPageContent(context: controller.context, webPage: webPage, url: webPage.content.url ?? "", sourceLocation: sourceLocation) - } - - self.content = content - self.contentStateDisposable = (content.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - strongSelf.controller?.title = state.title - strongSelf.contentState = state - strongSelf.requestLayout(transition: .easeInOut(duration: 0.25)) - }).strict() - - self.content?.onScrollingUpdate = { [weak self] update in - self?.onContentScrollingUpdate(update) - } - + self.pushContent(controller.subject, transition: .immediate) + self.performAction.connect { [weak self] action in - guard let self, let content = self.content, let url = self.contentState?.url else { + guard let self, let content = self.content.last, let url = self.contentState?.url else { return } switch action { @@ -341,7 +320,11 @@ public class BrowserScreen: ViewController, MinimizableController { case .stop: content.stop() case .navigateBack: - content.navigateBack() + if content.currentState.canGoBack { + content.navigateBack() + } else { + self.popContent(transition: .spring(duration: 0.4)) + } case .navigateForward: content.navigateForward() case .share: @@ -458,22 +441,139 @@ public class BrowserScreen: ViewController, MinimizableController { } deinit { - self.contentStateDisposable?.dispose() + self.contentStateDisposable.dispose() } override func didLoad() { super.didLoad() + self.contentContainerView.clipsToBounds = true self.view.addSubview(self.contentContainerView) - if let content = self.content { - self.contentContainerView.addSubview(content) - } } func updatePresentationState(animated: Bool = false, _ f: (BrowserPresentationState) -> BrowserPresentationState) { self.presentationState = f(self.presentationState) self.requestLayout(transition: animated ? .easeInOut(duration: 0.2) : .immediate) } + + func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) { + let browserContent: BrowserContent + switch content { + case let .webPage(url): + browserContent = BrowserWebContent(context: self.context, url: url) + case let .instantPage(webPage, anchor, sourceLocation): + let instantPageContent = BrowserInstantPageContent(context: self.context, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation) + instantPageContent.openPeer = { [weak self] peer in + guard let self else { + return + } + self.openPeer(peer) + } + browserContent = instantPageContent + } + browserContent.pushContent = { [weak self] content in + guard let self else { + return + } + self.pushContent(content, transition: .spring(duration: 0.4)) + } + browserContent.present = { [weak self] c, a in + guard let self, let controller = self.controller else { + return + } + controller.present(c, in: .window(.root), with: a) + } + browserContent.presentInGlobalOverlay = { [weak self] c in + guard let self, let controller = self.controller else { + return + } + controller.presentInGlobalOverlay(c) + } + browserContent.getNavigationController = { [weak self] in + return self?.controller?.navigationController as? NavigationController + } + browserContent.minimize = { [weak self] in + guard let self else { + return + } + self.minimize() + } + + self.content.append(browserContent) + self.requestLayout(transition: transition) + + self.setupContentStateUpdates() + } + + func popContent(transition: ComponentTransition) { + self.content.removeLast() + self.requestLayout(transition: transition) + + self.setupContentStateUpdates() + } + + func openPeer(_ peer: EnginePeer) { + guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { + return + } + self.minimize() + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), animated: true)) + } + + private func setupContentStateUpdates() { + for content in self.content { + content.onScrollingUpdate = { _ in } + } + + guard let content = self.content.last else { + self.controller?.title = "" + self.contentState = nil + self.contentStateDisposable.set(nil) + self.requestLayout(transition: .easeInOut(duration: 0.25)) + return + } + + var previousState = BrowserContentState(title: "", url: "", estimatedProgress: 1.0, readingProgress: 0.0, contentType: .webPage, canGoBack: false, canGoForward: false, backList: [], forwardList: []) + if self.content.count > 1 { + for content in self.content.prefix(upTo: self.content.count - 1) { + var backList = previousState.backList + backList.append(BrowserContentState.HistoryItem(url: content.currentState.url, title: content.currentState.title, uuid: content.uuid)) + previousState = previousState.withUpdatedBackList(backList) + } + } + + self.contentStateDisposable.set((content.state + |> deliverOnMainQueue).startStrict(next: { [weak self] state in + guard let self else { + return + } + var backList = state.backList + backList.insert(contentsOf: previousState.backList, at: 0) + + var canGoBack = state.canGoBack + if !backList.isEmpty { + canGoBack = true + } + + let previousState = self.contentState + let state = state.withUpdatedCanGoBack(canGoBack).withUpdatedBackList(backList) + self.controller?.title = state.title + self.contentState = state + + let transition: ComponentTransition + if let previousState, previousState.withUpdatedReadingProgress(state.readingProgress) == state { + transition = .immediate + } else { + transition = .easeInOut(duration: 0.25) + } + + self.requestLayout(transition: transition) + })) + + content.onScrollingUpdate = { [weak self] update in + self?.onContentScrollingUpdate(update) + } + } func minimize() { guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { @@ -598,7 +698,7 @@ public class BrowserScreen: ViewController, MinimizableController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) - if result == self.componentHost.view, let content = self.content { + if result == self.componentHost.view, let content = self.content.last { return content.hitTest(self.view.convert(point, to: content), with: event) } return result @@ -654,6 +754,51 @@ public class BrowserScreen: ViewController, MinimizableController { } } + func navigateTo(_ item: BrowserContentState.HistoryItem) { + if let _ = item.webItem { + if let last = self.content.last { + last.navigateTo(historyItem: item) + } + } else if let uuid = item.uuid { + var newContent = self.content + while newContent.last?.uuid != uuid { + newContent.removeLast() + } + self.content = newContent + self.requestLayout(transition: .spring(duration: 0.4)) + } + } + + func performHoldAction(view: UIView, gesture: ContextGesture?, action: BrowserScreen.Action) { + guard let controller = self.controller, let contentState = self.contentState else { + return + } + + let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: view)) + var items: [ContextMenuItem] = [] + switch action { + case .navigateBack: + for item in contentState.backList { + items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in + self?.navigateTo(item) + action(.default) + }))) + } + case .navigateForward: + for item in contentState.forwardList { + items.append(.action(ContextMenuActionItem(text: item.title, textLayout: .secondLineWithValue(item.url), icon: { _ in return nil }, action: { [weak self] (_, action) in + self?.navigateTo(item) + action(.default) + }))) + } + default: + return + } + + let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items)))) + self.controller?.present(contextController, in: .window(.root)) + } + func requestLayout(transition: ComponentTransition) { if let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) @@ -694,6 +839,11 @@ public class BrowserScreen: ViewController, MinimizableController { contentState: self.contentState, presentationState: self.presentationState, performAction: self.performAction, + performHoldAction: { [weak self] view, gesture, action in + if let self { + self.performHoldAction(view: view, gesture: gesture, action: action) + } + }, panelCollapseFraction: self.scrollingPanelOffsetFraction ) ), @@ -711,12 +861,48 @@ public class BrowserScreen: ViewController, MinimizableController { transition.setFrame(view: componentView, frame: CGRect(origin: .zero, size: componentSize)) } transition.setFrame(view: self.contentContainerView, frame: CGRect(origin: .zero, size: layout.size)) - if let content = self.content { - let collapsedHeight: CGFloat = 24.0 - let topInset: CGFloat = environment.statusBarHeight + navigationBarHeight * (1.0 - self.scrollingPanelOffsetFraction) + collapsedHeight * self.scrollingPanelOffsetFraction - let bottomInset = 49.0 + layout.intrinsicInsets.bottom - content.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: bottomInset, right: layout.safeInsets.right), transition: transition) - transition.setFrame(view: content, frame: CGRect(origin: .zero, size: layout.size)) + + var items: [AnyComponentWithIdentity] = [] + for content in self.content { + items.append( + AnyComponentWithIdentity(id: content.uuid, component: AnyComponent( + BrowserContentComponent( + content: content, + insets: UIEdgeInsets( + top: environment.statusBarHeight, + left: layout.safeInsets.left, + bottom: layout.intrinsicInsets.bottom, + right: layout.safeInsets.right + ), + navigationBarHeight: navigationBarHeight, + scrollingPanelOffsetFraction: self.scrollingPanelOffsetFraction + ) + )) + ) + } + + let _ = self.contentNavigationContainer.update( + transition: transition, + component: AnyComponent( + NavigationStackComponent( + items: items, + requestPop: { [weak self] in + guard let self else { + return + } + self.popContent(transition: .spring(duration: 0.4)) + } + ) + ), + environment: {}, + containerSize: layout.size + ) + let navigationFrame = CGRect(origin: .zero, size: layout.size) + if let view = self.contentNavigationContainer.view { + if view.superview == nil { + self.contentContainerView.addSubview(view) + } + transition.setFrame(view: view, frame: navigationFrame) } self.navigationBarHeight = environment.navigationHeight @@ -726,7 +912,7 @@ public class BrowserScreen: ViewController, MinimizableController { public enum Subject { case webPage(url: String) - case instantPage(webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation) + case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) } private let context: AccountContext @@ -743,7 +929,7 @@ public class BrowserScreen: ViewController, MinimizableController { self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown) self.scrollToTop = { [weak self] in - (self?.displayNode as? Node)?.content?.scrollToTop() + self?.node.content.last?.scrollToTop() } } @@ -751,6 +937,10 @@ public class BrowserScreen: ViewController, MinimizableController { preconditionFailure() } + private var node: Node { + return self.displayNode as! Node + } + override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -760,11 +950,30 @@ public class BrowserScreen: ViewController, MinimizableController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - (self.displayNode as! Node).containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) + self.node.containerLayoutUpdated(layout: layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.height, transition: ComponentTransition(transition)) } public var isMinimized = false public var isMinimizable = true + + public var minimizedIcon: UIImage? { + if let contentState = self.node.contentState { + switch contentState.contentType { + case .webPage: + return contentState.favicon + case .instantPage: + return UIImage(bundleImageName: "Chat/Message/AttachedContentInstantIcon")?.withRenderingMode(.alwaysTemplate) + } + } + return nil + } + + public var minimizedProgress: Float? { + if let contentState = self.node.contentState { + return Float(contentState.readingProgress) + } + return nil + } } private final class BrowserReferenceContentSource: ContextReferenceContentSource { @@ -780,3 +989,70 @@ private final class BrowserReferenceContentSource: ContextReferenceContentSource return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class BrowserContentComponent: Component { + let content: BrowserContent + let insets: UIEdgeInsets + let navigationBarHeight: CGFloat + let scrollingPanelOffsetFraction: CGFloat + + init( + content: BrowserContent, + insets: UIEdgeInsets, + navigationBarHeight: CGFloat, + scrollingPanelOffsetFraction: CGFloat + ) { + self.content = content + self.insets = insets + self.navigationBarHeight = navigationBarHeight + self.scrollingPanelOffsetFraction = scrollingPanelOffsetFraction + } + + static func ==(lhs: BrowserContentComponent, rhs: BrowserContentComponent) -> Bool { + if lhs.content.uuid != rhs.content.uuid { + return false + } + if lhs.insets != rhs.insets { + return false + } + if lhs.navigationBarHeight != rhs.navigationBarHeight { + return false + } + if lhs.scrollingPanelOffsetFraction != rhs.scrollingPanelOffsetFraction { + return false + } + return true + } + + final class View: UIView { + init() { + super.init(frame: CGRect()) + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: BrowserContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + if component.content.superview !== self { + self.addSubview(component.content) + } + + let collapsedHeight: CGFloat = 24.0 + let topInset: CGFloat = component.insets.top + component.navigationBarHeight * (1.0 - component.scrollingPanelOffsetFraction) + collapsedHeight * component.scrollingPanelOffsetFraction + let bottomInset = 49.0 + component.insets.bottom + component.content.updateLayout(size: availableSize, insets: UIEdgeInsets(top: topInset, left: component.insets.left, bottom: bottomInset, right: component.insets.right), transition: transition) + transition.setFrame(view: component.content, frame: CGRect(origin: .zero, size: availableSize)) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift index d5e7dbc808..7a752140ae 100644 --- a/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift +++ b/submodules/BrowserUI/Sources/BrowserToolbarComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import BlurredBackgroundComponent import BundleIconComponent import TelegramPresentationData +import ContextReferenceButtonComponent final class BrowserToolbarComponent: CombinedComponent { let backgroundColor: UIColor @@ -123,17 +124,20 @@ final class NavigationToolbarContentComponent: CombinedComponent { let canGoBack: Bool let canGoForward: Bool let performAction: ActionSlot + let performHoldAction: (UIView, ContextGesture?, BrowserScreen.Action) -> Void init( textColor: UIColor, canGoBack: Bool, canGoForward: Bool, - performAction: ActionSlot + performAction: ActionSlot, + performHoldAction: @escaping (UIView, ContextGesture?, BrowserScreen.Action) -> Void ) { self.textColor = textColor self.canGoBack = canGoBack self.canGoForward = canGoForward self.performAction = performAction + self.performHoldAction = performHoldAction } static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool { @@ -150,32 +154,41 @@ final class NavigationToolbarContentComponent: CombinedComponent { } static var body: Body { - let back = Child(Button.self) - let forward = Child(Button.self) + let back = Child(ContextReferenceButtonComponent.self) + let forward = Child(ContextReferenceButtonComponent.self) let share = Child(Button.self) let openIn = Child(Button.self) return { context in let availableSize = context.availableSize let performAction = context.component.performAction + let performHoldAction = context.component.performHoldAction let sideInset: CGFloat = 5.0 let buttonSize = CGSize(width: 50.0, height: availableSize.height) let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0 + let canGoBack = context.component.canGoBack let back = back.update( - component: Button( + component: ContextReferenceButtonComponent( content: AnyComponent( BundleIconComponent( name: "Instant View/Back", - tintColor: context.component.textColor + tintColor: canGoBack ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4) ) ), - isEnabled: context.component.canGoBack, - action: { - performAction.invoke(.navigateBack) + minSize: buttonSize, + action: { view, gesture in + guard canGoBack else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateBack) + } else { + performAction.invoke(.navigateBack) + } } - ).minSize(buttonSize), + ), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) @@ -183,19 +196,27 @@ final class NavigationToolbarContentComponent: CombinedComponent { .position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0)) ) + let canGoForward = context.component.canGoForward let forward = forward.update( - component: Button( + component: ContextReferenceButtonComponent( content: AnyComponent( BundleIconComponent( name: "Instant View/Forward", - tintColor: context.component.textColor + tintColor: canGoForward ? context.component.textColor : context.component.textColor.withAlphaComponent(0.4) ) ), - isEnabled: context.component.canGoForward, - action: { - performAction.invoke(.navigateForward) + minSize: buttonSize, + action: { view, gesture in + guard canGoForward else { + return + } + if let gesture { + performHoldAction(view, gesture, .navigateForward) + } else { + performAction.invoke(.navigateForward) + } } - ).minSize(buttonSize), + ), availableSize: buttonSize, transition: .easeInOut(duration: 0.2) ) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 584fe86c42..6eb5aa1674 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1,14 +1,20 @@ import Foundation import UIKit +import Display import ComponentFlow import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences +import PresentationDataUtils import AccountContext import WebKit import AppBundle +import PromptUI +import SafariServices +import ShareController +import UndoUI private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -80,19 +86,36 @@ private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler { } } -final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { +final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { + private let context: AccountContext + private let webView: WKWebView + let uuid: UUID + private var _state: BrowserContentState private let statePromise: Promise + var currentState: BrowserContentState { + return self._state + } var state: Signal { return self.statePromise.get() } + private let faviconDisposable = MetaDisposable() + + var pushContent: (BrowserScreen.Subject) -> Void = { _ in } var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in } + var minimize: () -> Void = { } + var present: (ViewController, Any?) -> Void = { _, _ in } + var presentInGlobalOverlay: (ViewController) -> Void = { _ in } + var getNavigationController: () -> NavigationController? = { return nil } init(context: AccountContext, url: String) { + self.context = context + self.uuid = UUID() + let configuration = WKWebViewConfiguration() if context.sharedContext.immediateExperimentalUISettings.browserExperiment { @@ -101,7 +124,7 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { } self.webView = WKWebView(frame: CGRect(), configuration: configuration) - self.webView.allowsLinkPreview = false + self.webView.allowsLinkPreview = true if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.webView.scrollView.contentInsetAdjustmentBehavior = .never @@ -115,13 +138,15 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { title = parsedUrl.host ?? "" } - self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .webPage) + self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .webPage) self.statePromise = Promise(self._state) super.init(frame: .zero) self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self + self.webView.navigationDelegate = self + self.webView.uiDelegate = self self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil) self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) @@ -141,6 +166,8 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack)) self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward)) + + self.faviconDisposable.dispose() } func setFontSize(_ fontSize: CGFloat) { @@ -262,6 +289,12 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { self.webView.goForward() } + func navigateTo(historyItem: BrowserContentState.HistoryItem) { + if let webItem = historyItem.webItem { + self.webView.go(to: webItem) + } + } + func scrollToTop() { self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true) } @@ -277,25 +310,25 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))) } + private func updateState(_ f: (BrowserContentState) -> BrowserContentState) { + let updated = f(self._state) + self._state = updated + self.statePromise.set(.single(self._state)) + } + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let updateState: ((BrowserContentState) -> BrowserContentState) -> Void = { f in - let updated = f(self._state) - self._state = updated - self.statePromise.set(.single(self._state)) - } - if keyPath == "title" { - updateState { $0.withUpdatedTitle(self.webView.title ?? "") } + self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") } } else if keyPath == "URL" { - updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } + self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") } self.didSetupSearch = false } else if keyPath == "estimatedProgress" { - updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } + self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) } } else if keyPath == "canGoBack" { - updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } + self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) } self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack } else if keyPath == "canGoForward" { - updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } + self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) } } } @@ -344,5 +377,234 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate { )) } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) + + var readingProgress: CGFloat = 0.0 + if !scrollView.contentSize.height.isZero { + let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top) + readingProgress = max(0.0, min(1.0, value)) + } + self.updateState { + $0.withUpdatedReadingProgress(readingProgress) + } + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.updateState { + $0 + .withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) }) + .withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) }) + } + + self.parseFavicon() + } + + @available(iOSApplicationExtension 15.0, iOS 15.0, *) + func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { + decisionHandler(.prompt) + } + + func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + if !completed { + completed = true + completionHandler() + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler() + } + } + } + self.present(alertController, nil) + } + + func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + var completed = false + let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + if !completed { + completed = true + completionHandler(false) + } + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { + if !completed { + completed = true + completionHandler(true) + } + })]) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler(false) + } + } + } + self.present(alertController, nil) + } + + func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { + var completed = false + let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in + if !completed { + completed = true + if let value = value { + completionHandler(value) + } else { + completionHandler(nil) + } + } + }) + promptController.dismissed = { byOutsideTap in + if byOutsideTap { + if !completed { + completed = true + completionHandler(nil) + } + } + } + self.present(promptController, nil) + } + + @available(iOS 13.0, *) + func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { + guard let url = elementInfo.linkURL else { + completionHandler(nil) + return + } + //TODO:localize + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in + return UIMenu(title: "", children: [ + UIAction(title: "Open", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in + self?.open(url: url.absoluteString, new: false) + }), + UIAction(title: "Open in New Tab", image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in + self?.open(url: url.absoluteString, new: true) + }), + UIAction(title: "Add to Reading List", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in + let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil) + }), + UIAction(title: "Copy Link", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in + UIPasteboard.general.string = url.absoluteString + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + }), + UIAction(title: "Share", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in + self?.share(url: url.absoluteString) + }) + ]) + } + completionHandler(configuration) + } + + private func open(url: String, new: Bool) { + let subject: BrowserScreen.Subject = .webPage(url: url) + if new, let navigationController = self.getNavigationController() { + self.minimize() + let controller = BrowserScreen(context: self.context, subject: subject) + navigationController.pushViewController(controller) + } else { + self.pushContent(subject) + } + } + + private func share(url: String) { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let shareController = ShareController(context: self.context, subject: .url(url)) + shareController.actionCompleted = { [weak self] in + self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + } + self.present(shareController, nil) + } + + private func parseFavicon() { + struct Favicon: Equatable, Hashable { + let url: String + let dimensions: PixelDimensions? + + func hash(into hasher: inout Hasher) { + hasher.combine(self.url) + if let dimensions = self.dimensions { + hasher.combine(dimensions.width) + hasher.combine(dimensions.height) + } + } + } + + let js = """ + var favicons = []; + var nodeList = document.getElementsByTagName('link'); + for (var i = 0; i < nodeList.length; i++) + { + if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')) + { + const node = nodeList[i]; + favicons.push({ + url: node.getAttribute('href'), + sizes: node.getAttribute('sizes') + }); + } + } + favicons; + """ + self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in + guard let self, let favicons = jsResult as? [Any] else { + return + } + var result = Set(); + for favicon in favicons { + if let faviconDict = favicon as? [String: Any], let urlString = faviconDict["url"] as? String { + if let url = URL(string: urlString, relativeTo: self.webView.url) { + let sizesString = faviconDict["sizes"] as? String; + let sizeStrings = sizesString?.components(separatedBy: "x") ?? [] + if (sizeStrings.count == 2) { + let width = Int(sizeStrings[0]) + let height = Int(sizeStrings[1]) + let dimensions: PixelDimensions? + if let width, let height { + dimensions = PixelDimensions(width: Int32(width), height: Int32(height)) + } else { + dimensions = nil + } + result.insert(Favicon(url: url.absoluteString, dimensions: dimensions)) + } else { + result.insert(Favicon(url: url.absoluteString, dimensions: nil)) + } + } + } + } + + if result.isEmpty, let webViewUrl = self.webView.url { + let schemeAndHostUrl = webViewUrl.deletingPathExtension() + let url = schemeAndHostUrl.appendingPathComponent("favicon.ico") + result.insert(Favicon(url: url.absoluteString, dimensions: nil)) + } + + var largestIcon = result.first(where: { $0.url.lowercased().contains(".svg") }) + if largestIcon == nil { + largestIcon = result.first + for icon in result { + let maxSize = largestIcon?.dimensions?.width ?? 0 + if let width = icon.dimensions?.width, width > maxSize { + largestIcon = icon + } + } + } + + if let favicon = largestIcon { + self.faviconDisposable.set((fetchFavicon(context: self.context, url: favicon.url, size: CGSize(width: 20.0, height: 20.0)) + |> deliverOnMainQueue).startStrict(next: { [weak self] favicon in + guard let self else { + return + } + self.updateState { $0.withUpdatedFavicon(favicon) } + })) + } + }) } } diff --git a/submodules/BrowserUI/Sources/Favicon.swift b/submodules/BrowserUI/Sources/Favicon.swift new file mode 100644 index 0000000000..fe0e8ae84a --- /dev/null +++ b/submodules/BrowserUI/Sources/Favicon.swift @@ -0,0 +1,38 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import AccountContext +import Svg + +private var faviconCache: [String: UIImage] = [:] +func fetchFavicon(context: AccountContext, url: String, size: CGSize) -> Signal { + if let icon = faviconCache[url] { + return .single(icon) + } + return context.engine.resources.httpData(url: url) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { data in + if let data { + if let image = UIImage(data: data) { + return image + } else if url.lowercased().contains(".svg"), let preparedData = prepareSvgImage(data, false), let image = renderPreparedImage(preparedData, size, .clear, UIScreenScale, false) { + return image + } + return nil + } else { + return nil + } + } + |> beforeNext { image in + if let image { + Queue.mainQueue().async { + faviconCache[url] = image + } + } + } +} diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index c599f0ee6c..0ac1ac1ddd 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -9,7 +9,7 @@ public final class Button: Component { public let isEnabled: Bool public let isExclusive: Bool public let action: () -> Void - public let holdAction: (() -> Void)? + public let holdAction: ((UIView) -> Void)? public let highlightedAction: ActionSlot? convenience public init( @@ -39,7 +39,7 @@ public final class Button: Component { isEnabled: Bool = true, isExclusive: Bool = true, action: @escaping () -> Void, - holdAction: (() -> Void)?, + holdAction: ((UIView) -> Void)?, highlightedAction: ActionSlot? ) { self.content = content @@ -82,7 +82,7 @@ public final class Button: Component { } - public func withHoldAction(_ holdAction: (() -> Void)?) -> Button { + public func withHoldAction(_ holdAction: ((UIView) -> Void)?) -> Button { return Button( content: self.content, minSize: self.minSize, @@ -228,7 +228,7 @@ public final class Button: Component { return } strongSelf.holdActionTimer?.invalidate() - strongSelf.component?.holdAction?() + strongSelf.component?.holdAction?(strongSelf) strongSelf.beginExecuteHoldActionTimer() }) self.holdActionTimer = holdActionTimer @@ -246,7 +246,7 @@ public final class Button: Component { guard let strongSelf = self else { return } - strongSelf.component?.holdAction?() + strongSelf.component?.holdAction?(strongSelf) }) self.holdActionTimer = holdActionTimer RunLoop.main.add(holdActionTimer, forMode: .common) diff --git a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift index 7d4154d910..1e37b9b72a 100644 --- a/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift +++ b/submodules/Components/ReactionButtonListComponent/Sources/ReactionButtonListComponent.swift @@ -21,10 +21,12 @@ private let tagImage: UIImage? = { }() private final class StarsButtonEffectLayer: SimpleLayer { + let emitterLayer = CAEmitterLayer() + override init() { super.init() - self.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2).cgColor + self.addSublayer(self.emitterLayer) } override init(layer: Any) { @@ -35,7 +37,45 @@ private final class StarsButtonEffectLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } + private func setup() { + let color = UIColor(rgb: 0xffbe27) + + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 25.0 + emitter.lifetime = 2.0 + emitter.velocity = 12.0 + emitter.velocityRange = 3 + emitter.scale = 0.1 + emitter.scaleRange = 0.08 + emitter.alphaRange = 0.1 + emitter.emissionRange = .pi * 2.0 + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") + + let staticColors: [Any] = [ + color.withAlphaComponent(0.0).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + emitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + + self.emitterLayer.emitterCells = [emitter] + } + func update(size: CGSize) { + if self.emitterLayer.emitterCells == nil { + self.setup() + } + self.emitterLayer.emitterShape = .circle + self.emitterLayer.emitterSize = CGSize(width: size.width * 0.7, height: size.height * 0.7) + self.emitterLayer.emitterMode = .surface + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) } } diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 65dd3c5559..231e9a3bf8 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -569,7 +569,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis if !topPeers.isEmpty { var index: Int = 0 var sectionId: Int = 1 - for (title, peerIds) in sections { + for (title, peerIds, hasActions) in sections { var allSelected = true if let selectedPeerIndices = selectionState?.selectedPeerIndices, !selectedPeerIndices.isEmpty { for peerId in peerIds { @@ -617,7 +617,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis } let presence = presences[peer.id] - entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, true, true, nil, false)) + entries.append(.peer(index, .peer(peer: peer._asPeer(), isGlobal: false, participantCount: nil), presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, false, hasActions, true, nil, false)) index += 1 } @@ -629,7 +629,7 @@ private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactLis if !sections.isEmpty, let selectionState { var hasNonBirthdayPeers = false var allBirthdayPeerIds = Set() - for (_, peerIds) in sections { + for (_, peerIds, _) in sections { for peerId in peerIds { allBirthdayPeerIds.insert(peerId) } @@ -865,7 +865,7 @@ public enum ContactListPresentation { public enum TopPeers { case none case recent - case custom([(title: String, peerIds: [EnginePeer.Id])]) + case custom([(title: String, peerIds: [EnginePeer.Id], hasActions: Bool)]) } case orderedByPresence(options: [ContactListAdditionalOption]) @@ -1711,7 +1711,7 @@ public final class ContactListNode: ASDisplayNode { } case let .custom(sections): var peerIds: [EnginePeer.Id] = [] - for (_, sectionPeers) in sections { + for (_, sectionPeers, _) in sections { peerIds.append(contentsOf: sectionPeers) } topPeers = combineLatest( diff --git a/submodules/Display/Source/Navigation/MinimizedContainer.swift b/submodules/Display/Source/Navigation/MinimizedContainer.swift index a410d6d3ee..625f95a752 100644 --- a/submodules/Display/Source/Navigation/MinimizedContainer.swift +++ b/submodules/Display/Source/Navigation/MinimizedContainer.swift @@ -26,6 +26,7 @@ public protocol MinimizableController: ViewController { var isMinimized: Bool { get set } var isMinimizable: Bool { get } var minimizedIcon: UIImage? { get } + var minimizedProgress: Float? { get } func makeContentSnapshotView() -> UIView? func shouldDismissImmediately() -> Bool @@ -52,6 +53,10 @@ public extension MinimizableController { return nil } + var minimizedProgress: Float? { + return nil + } + func makeContentSnapshotView() -> UIView? { return self.displayNode.view.snapshotView(afterScreenUpdates: false) } diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 0ec54265fa..eec308359e 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -813,6 +813,16 @@ public extension CALayer { } } +public extension CAEmitterCell { + static func createEmitterBehavior(type: String) -> NSObject { + let selector = ["behaviorWith", "Type:"].joined(separator: "") + let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type + let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! + let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) + return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) + } +} + public extension CALayer { func snapshotContentTreeAsView(unhide: Bool = false) -> UIView? { let wasHidden = self.isHidden diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 908fc04883..e50f772e3a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -30,6 +30,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D return DrawingLocationEntityView(context: context, entity: entity) } else if let entity = entity as? DrawingLinkEntity { return DrawingLinkEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingWeatherEntity { + return DrawingWeatherEntityView(context: context, entity: entity) } else { return nil } @@ -59,6 +61,9 @@ private func prepareForRendering(entityView: DrawingEntityView) { if let entityView = entityView as? DrawingLinkEntityView { entityView.entity.renderImage = entityView.getRenderImage() } + if let entityView = entityView as? DrawingWeatherEntityView { + entityView.entity.renderImage = entityView.getRenderImage() + } } public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { @@ -397,6 +402,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { location.width = floor(self.size.width * 0.85) location.scale = zoomScale } + } else if let weather = entity as? DrawingWeatherEntity { + weather.position = center + if setup { + weather.rotation = rotation + weather.referenceDrawingSize = self.size + weather.width = floor(self.size.width * 0.85) + weather.scale = zoomScale + } } } diff --git a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift new file mode 100644 index 0000000000..b1fda8f725 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift @@ -0,0 +1,643 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import MediaEditor + +private func generateIcon(style: DrawingWeatherEntity.Style) -> UIImage? { + guard let image = UIImage(bundleImageName: "Chat/Attach Menu/Location") else { + return nil + } + return generateImage(image.size, contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let cgImage = image.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + if [.black, .white].contains(style) { + let green: UIColor + let blue: UIColor + + if case .black = style { + green = UIColor(rgb: 0x3EF588) + blue = UIColor(rgb: 0x4FAAFF) + } else { + green = UIColor(rgb: 0x1EBD5E) + blue = UIColor(rgb: 0x1C92FF) + } + + var locations: [CGFloat] = [0.0, 1.0] + let colorsArray = [green.cgColor, blue.cgColor] as NSArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: size.width, y: size.height), end: CGPoint(x: 0.0, y: 0.0), options: CGGradientDrawingOptions()) + } else { + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + } + }) +} + +public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelegate { + private var weatherEntity: DrawingWeatherEntity { + return self.entity as! DrawingWeatherEntity + } + + let backgroundView: UIView + let blurredBackgroundView: BlurredBackgroundView + + let textView: DrawingTextView + let iconView: UIImageView + private let imageNode: TransformImageNode + private var animationNode: AnimatedStickerNode? + + private var didSetUpAnimationNode = false + private let stickerFetchedDisposable = MetaDisposable() + private let cachedDisposable = MetaDisposable() + + init(context: AccountContext, entity: DrawingWeatherEntity) { + self.backgroundView = UIView() + self.backgroundView.clipsToBounds = true + + self.blurredBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.25), enableBlur: true) + self.blurredBackgroundView.clipsToBounds = true + + self.textView = DrawingTextView(frame: .zero) + self.textView.clipsToBounds = false + + self.textView.backgroundColor = .clear + self.textView.isEditable = false + self.textView.isSelectable = false + self.textView.contentInset = .zero + self.textView.showsHorizontalScrollIndicator = false + self.textView.showsVerticalScrollIndicator = false + self.textView.scrollsToTop = false + self.textView.isScrollEnabled = false + self.textView.textContainerInset = .zero + self.textView.minimumZoomScale = 1.0 + self.textView.maximumZoomScale = 1.0 + self.textView.keyboardAppearance = .dark + self.textView.autocorrectionType = .default + self.textView.spellCheckingType = .no + self.textView.textContainer.maximumNumberOfLines = 2 + self.textView.textContainer.lineBreakMode = .byTruncatingTail + + self.iconView = UIImageView() + self.imageNode = TransformImageNode() + + super.init(context: context, entity: entity) + + self.textView.delegate = self + self.addSubview(self.backgroundView) + self.addSubview(self.blurredBackgroundView) + self.addSubview(self.textView) + self.addSubview(self.iconView) + + self.update(animated: false) + + self.setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var textSize: CGSize = .zero + public override func sizeThatFits(_ size: CGSize) -> CGSize { + var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude)) + self.textSize = result + + let widthExtension: CGFloat + if self.weatherEntity.icon != nil { + widthExtension = result.height * 0.77 + } else { + widthExtension = result.height * 0.65 + } + result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension) + result.height = ceil(result.height * 1.2); + return result; + } + + public override func sizeToFit() { + let center = self.center + let transform = self.transform + self.transform = .identity + super.sizeToFit() + self.center = center + self.transform = transform + } + + public override func layoutSubviews() { + super.layoutSubviews() + + let iconSize: CGFloat + let iconOffset: CGFloat + if self.weatherEntity.icon != nil { + iconSize = min(80.0, floor(self.bounds.height * 0.7)) + iconOffset = 0.2 + } else { + iconSize = min(76.0, floor(self.bounds.height * 0.6)) + iconOffset = 0.3 + } + + self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) + self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0) + + let imageSize = CGSize(width: iconSize, height: iconSize) + self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() + + self.textView.frame = CGRect(origin: CGPoint(x: self.bounds.width - self.textSize.width - 6.0, y: floorToScreenPixels((self.bounds.height - self.textSize.height) / 2.0)), size: self.textSize) + self.backgroundView.frame = self.bounds + self.blurredBackgroundView.frame = self.bounds + self.blurredBackgroundView.update(size: self.bounds.size, transition: .immediate) + } + + override func selectedTapAction() -> Bool { + let values = [self.entity.scale, self.entity.scale * 0.93, self.entity.scale] + let keyTimes = [0.0, 0.33, 1.0] + self.layer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.3, keyPath: "transform.scale") + + let updatedStyle: DrawingWeatherEntity.Style + switch self.weatherEntity.style { + case .white: + updatedStyle = .black + case .black: + updatedStyle = .transparent + case .transparent: + if self.weatherEntity.hasCustomColor { + updatedStyle = .custom + } else { + updatedStyle = .white + } + case .custom: + updatedStyle = .white + case .blur: + updatedStyle = .white + } + self.weatherEntity.style = updatedStyle + + self.update() + + return true + } + + private var displayFontSize: CGFloat { + var textFontSize: CGFloat = 0.07 + let textLength = self.weatherEntity.temperature.count + if textLength > 10 { + textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0) + } + + let minFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.025) + let maxFontSize = max(10.0, max(self.weatherEntity.referenceDrawingSize.width, self.weatherEntity.referenceDrawingSize.height) * 0.25) + let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize + return fontSize + } + + private func updateText() { + let text = NSMutableAttributedString(string: self.weatherEntity.temperature.uppercased()) + let range = NSMakeRange(0, text.length) + let fontSize = self.displayFontSize + + self.textView.drawingLayoutManager.textContainers.first?.lineFragmentPadding = floor(fontSize * 0.24) + + let font = Font.with(size: fontSize, design: .camera, weight: .semibold) + text.addAttribute(.font, value: font, range: range) + text.addAttribute(.kern, value: -3.5 as NSNumber, range: range) + self.textView.font = font + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .left + text.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) + + let textColor: UIColor + switch self.weatherEntity.style { + case .white: + textColor = .black + case .black, .transparent, .blur: + textColor = .white + case .custom: + let color = self.weatherEntity.color.toUIColor() + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + } + + text.addAttribute(.foregroundColor, value: textColor, range: range) + + self.textView.attributedText = text + self.textView.visualText = text + } + + private var currentStyle: DrawingWeatherEntity.Style? + public override func update(animated: Bool = false) { + self.center = self.weatherEntity.position + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.weatherEntity.rotation), self.weatherEntity.scale, self.weatherEntity.scale) + + self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0) + switch self.weatherEntity.style { + case .white: + self.textView.textColor = .black + self.backgroundView.backgroundColor = .white + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .black: + self.textView.textColor = .white + self.backgroundView.backgroundColor = .black + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .transparent: + self.textView.textColor = .white + self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .custom: + let color = self.weatherEntity.color.toUIColor() + let textColor: UIColor + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + self.textView.textColor = textColor + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .blur: + self.textView.textColor = .white + self.backgroundView.isHidden = true + self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff) + self.blurredBackgroundView.isHidden = false + } + self.textView.textAlignment = .left + + self.updateText() + + self.sizeToFit() + + if self.currentStyle != self.weatherEntity.style { + self.currentStyle = self.weatherEntity.style + self.iconView.image = generateIcon(style: self.weatherEntity.style) + } + + self.backgroundView.layer.cornerRadius = self.textSize.height * 0.2 + self.blurredBackgroundView.layer.cornerRadius = self.backgroundView.layer.cornerRadius + if #available(iOS 13.0, *) { + self.backgroundView.layer.cornerCurve = .continuous + self.blurredBackgroundView.layer.cornerCurve = .continuous + } + + super.update(animated: animated) + } + + private func setup() { + if let file = self.weatherEntity.icon { + self.iconView.isHidden = true + self.addSubnode(self.imageNode) + if let dimensions = file.dimensions { + if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + if self.animationNode == nil { + let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = false + self.animationNode = animationNode + animationNode.started = { [weak self, weak animationNode] in + self?.imageNode.isHidden = true + + let _ = animationNode +// if let animationNode = animationNode { +// let _ = (animationNode.status +// |> take(1) +// |> deliverOnMainQueue).start(next: { [weak self] status in +// self?.started?(status.duration) +// }) +// } + } + self.addSubnode(animationNode) + + if file.isCustomTemplateEmoji { + animationNode.dynamicColor = UIColor(rgb: 0xffffff) + } + } + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) + } else { + if let animationNode = self.animationNode { + animationNode.visibility = false + self.animationNode = nil + animationNode.removeFromSupernode() + self.imageNode.isHidden = false + self.didSetUpAnimationNode = false + } + self.imageNode.setSignal(chatMessageSticker(account: self.context.account, userLocation: .other, file: file, small: false, synchronousLoad: false)) + self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: false)).start()) + } + self.setNeedsLayout() + } + } + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingWeatherEntitySelectionView else { + return + } + self.pushIdentityTransformForMeasurement() + + selectionView.transform = .identity + let bounds = self.selectionBounds + let center = bounds.center + + let scale = self.superview?.superview?.layer.value(forKeyPath: "transform.scale.x") as? CGFloat ?? 1.0 + selectionView.center = self.convert(center, to: selectionView.superview) + + selectionView.bounds = CGRect(origin: .zero, size: CGSize(width: (bounds.width * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.weatherEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.weatherEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView? { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingWeatherEntitySelectionView() + selectionView.entityView = self + return selectionView + } + + func getRenderImage() -> UIImage? { + let rect = self.bounds + UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0) + self.drawHierarchy(in: rect, afterScreenUpdates: true) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } + + func getRenderSubEntities() -> [DrawingEntity] { + return [] + } +} + +final class DrawingWeatherEntitySelectionView: DrawingEntitySelectionView { + private let border = SimpleShapeLayer() + private let leftHandle = SimpleShapeLayer() + private let rightHandle = SimpleShapeLayer() + + private var longPressGestureRecognizer: UILongPressGestureRecognizer? + + override init(frame: CGRect) { + let handleBounds = CGRect(origin: .zero, size: entitySelectionViewHandleSize) + let handles = [ + self.leftHandle, + self.rightHandle + ] + + super.init(frame: frame) + + self.backgroundColor = .clear + self.isOpaque = false + + self.border.lineCap = .round + self.border.fillColor = UIColor.clear.cgColor + self.border.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.75).cgColor + self.layer.addSublayer(self.border) + + for handle in handles { + handle.bounds = handleBounds + handle.fillColor = UIColor(rgb: 0x0a60ff).cgColor + handle.strokeColor = UIColor(rgb: 0xffffff).cgColor + handle.rasterizationScale = UIScreen.main.scale + handle.shouldRasterize = true + + self.layer.addSublayer(handle) + } + + self.snapTool.onSnapUpdated = { [weak self] type, snapped in + if let self, let entityView = self.entityView { + entityView.onSnapUpdated(type, snapped) + } + } + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + self.addGestureRecognizer(longPressGestureRecognizer) + self.longPressGestureRecognizer = longPressGestureRecognizer + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var scale: CGFloat = 1.0 { + didSet { + self.setNeedsLayout() + } + } + + override var selectionInset: CGFloat { + return 15.0 + } + + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + private let snapTool = DrawingEntitySnapTool() + + @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + if case .began = gestureRecognizer.state { + self.longPressed() + } + } + + private var currentHandle: CALayer? + override func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let entityView = self.entityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + let location = gestureRecognizer.location(in: self) + switch gestureRecognizer.state { + case .began: + self.tapGestureRecognizer?.isEnabled = false + self.tapGestureRecognizer?.isEnabled = true + + self.longPressGestureRecognizer?.isEnabled = false + self.longPressGestureRecognizer?.isEnabled = true + + self.snapTool.maybeSkipFromStart(entityView: entityView, position: entity.position) + + if let sublayers = self.layer.sublayers { + for layer in sublayers { + if layer.frame.contains(location) { + self.currentHandle = layer + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + return + } + } + } + self.currentHandle = self.layer + entityView.onInteractionUpdated(true) + case .changed: + if self.currentHandle == nil { + self.currentHandle = self.layer + } + + let delta = gestureRecognizer.translation(in: entityView.superview) + let parentLocation = gestureRecognizer.location(in: self.superview) + let velocity = gestureRecognizer.velocity(in: entityView.superview) + + var updatedScale = entity.scale + var updatedPosition = entity.position + var updatedRotation = entity.rotation + + if self.currentHandle === self.leftHandle || self.currentHandle === self.rightHandle { + if gestureRecognizer.numberOfTouches > 1 { + return + } + var deltaX = gestureRecognizer.translation(in: self).x + if self.currentHandle === self.leftHandle { + deltaX *= -1.0 + } + let scaleDelta = (self.bounds.size.width + deltaX * 2.0) / self.bounds.size.width + updatedScale = max(0.01, updatedScale * scaleDelta) + + let newAngle: CGFloat + if self.currentHandle === self.leftHandle { + newAngle = atan2(self.center.y - parentLocation.y, self.center.x - parentLocation.x) + } else { + newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) + } + var delta = newAngle - updatedRotation + if delta < -.pi { + delta = 2.0 * .pi + delta + } + let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0 + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0) + } else if self.currentHandle === self.layer { + updatedPosition.x += delta.x + updatedPosition.y += delta.y + + updatedPosition = self.snapTool.update(entityView: entityView, velocity: velocity, delta: delta, updatedPosition: updatedPosition, size: entityView.frame.size) + } + + entity.scale = updatedScale + entity.position = updatedPosition + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.setTranslation(.zero, in: entityView) + case .ended, .cancelled: + self.snapTool.reset() + if self.currentHandle != nil { + self.snapTool.rotationReset() + } + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + + switch gestureRecognizer.state { + case .began, .changed: + if case .began = gestureRecognizer.state { + entityView.onInteractionUpdated(true) + } + let scale = gestureRecognizer.scale + entity.scale = max(0.1, entity.scale * scale) + entityView.update() + + gestureRecognizer.scale = 1.0 + case .ended, .cancelled: + entityView.onInteractionUpdated(false) + default: + break + } + } + + override func handleRotate(_ gestureRecognizer: UIRotationGestureRecognizer) { + guard let entityView = self.entityView as? DrawingWeatherEntityView, let entity = entityView.entity as? DrawingWeatherEntity else { + return + } + + let velocity = gestureRecognizer.velocity + var updatedRotation = entity.rotation + var rotation: CGFloat = 0.0 + + switch gestureRecognizer.state { + case .began: + self.snapTool.maybeSkipFromStart(entityView: entityView, rotation: entity.rotation) + entityView.onInteractionUpdated(true) + case .changed: + rotation = gestureRecognizer.rotation + updatedRotation += rotation + + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocity, delta: rotation, updatedRotation: updatedRotation) + entity.rotation = updatedRotation + entityView.update() + + gestureRecognizer.rotation = 0.0 + case .ended, .cancelled: + self.snapTool.rotationReset() + entityView.onInteractionUpdated(false) + default: + break + } + + entityView.onPositionUpdated(entity.position) + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -22.0, dy: -22.0).contains(point) + } + + override func layoutSubviews() { + let inset = self.selectionInset - 10.0 + + let bounds = CGRect(origin: .zero, size: CGSize(width: entitySelectionViewHandleSize.width / self.scale, height: entitySelectionViewHandleSize.height / self.scale)) + let handleSize = CGSize(width: 9.0 / self.scale, height: 9.0 / self.scale) + let handlePath = CGPath(ellipseIn: CGRect(origin: CGPoint(x: (bounds.width - handleSize.width) / 2.0, y: (bounds.height - handleSize.height) / 2.0), size: handleSize), transform: nil) + let lineWidth = (1.0 + UIScreenPixel) / self.scale + + let handles = [ + self.leftHandle, + self.rightHandle + ] + + for handle in handles { + handle.path = handlePath + handle.bounds = bounds + handle.lineWidth = lineWidth + } + + self.leftHandle.position = CGPoint(x: inset, y: self.bounds.midY) + self.rightHandle.position = CGPoint(x: self.bounds.maxX - inset, y: self.bounds.midY) + + let width: CGFloat = self.bounds.width - inset * 2.0 + let height: CGFloat = self.bounds.height - inset * 2.0 + let cornerRadius: CGFloat = 12.0 - self.scale + + let perimeter: CGFloat = 2.0 * (width + height - cornerRadius * (4.0 - .pi)) + let count = 12 + let relativeDashLength: CGFloat = 0.25 + let dashLength = perimeter / CGFloat(count) + self.border.lineDashPattern = [dashLength * relativeDashLength, dashLength * relativeDashLength] as [NSNumber] + + self.border.lineWidth = 2.0 / self.scale + self.border.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: inset, y: inset), size: CGSize(width: width, height: height)), cornerRadius: cornerRadius).cgPath + } +} diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 40a29f45e8..a867977bac 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -632,6 +632,7 @@ private final class PendingInAppPurchaseState: Codable { case giftCode case giveaway case stars + case starsGift } case subscription @@ -641,6 +642,7 @@ private final class PendingInAppPurchaseState: Codable { case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?) case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32) case stars(count: Int64) + case starsGift(peerId: EnginePeer.Id, count: Int64) public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -674,7 +676,14 @@ private final class PendingInAppPurchaseState: Codable { untilDate: try container.decode(Int32.self, forKey: .untilDate) ) case .stars: - self = .stars(count: try container.decode(Int64.self, forKey: .stars)) + self = .stars( + count: try container.decode(Int64.self, forKey: .stars) + ) + case .starsGift: + self = .starsGift( + peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)), + count: try container.decode(Int64.self, forKey: .stars) + ) default: throw DecodingError.generic } @@ -710,6 +719,10 @@ private final class PendingInAppPurchaseState: Codable { case let .stars(count): try container.encode(PurposeType.stars.rawValue, forKey: .type) try container.encode(count, forKey: .stars) + case let .starsGift(peerId, count): + try container.encode(PurposeType.starsGift.rawValue, forKey: .type) + try container.encode(peerId.toInt64(), forKey: .peer) + try container.encode(count, forKey: .stars) } } @@ -729,6 +742,8 @@ private final class PendingInAppPurchaseState: Codable { self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate) case let .stars(count, _, _): self = .stars(count: count) + case let .starsGift(peerId, count, _, _): + self = .starsGift(peerId: peerId, count: count) } } @@ -749,6 +764,8 @@ private final class PendingInAppPurchaseState: Codable { return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount) case let .stars(count): return .stars(count: count, currency: currency, amount: amount) + case let .starsGift(peerId, count): + return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount) } } } diff --git a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift index a5bf188a88..c33f7e4da7 100644 --- a/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift +++ b/submodules/InstantPageUI/Sources/InstantPageGalleryController.swift @@ -198,7 +198,7 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable private let replaceRootController: (ViewController, Promise?) -> Void private let baseNavigationController: NavigationController? - var openUrl: ((InstantPageUrlItem) -> Void)? + public var openUrl: ((InstantPageUrlItem) -> Void)? private var innerOpenUrl: (InstantPageUrlItem) -> Void private var openUrlOptions: (InstantPageUrlItem) -> Void diff --git a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift index b4be0ba7ee..86bfdbbfda 100644 --- a/submodules/InstantPageUI/Sources/InstantPageImageItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPageImageItem.swift @@ -27,7 +27,7 @@ public final class InstantPageImageItem: InstantPageItem { return [self.media] } - let interactive: Bool + public let interactive: Bool let roundCorners: Bool let fit: Bool diff --git a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift index 11484db8b4..c45782b8e4 100644 --- a/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift +++ b/submodules/InstantPageUI/Sources/InstantPageMediaPlaylist.swift @@ -141,30 +141,30 @@ struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation { } } -final class InstantPageMediaPlaylist: SharedMediaPlaylist { +public final class InstantPageMediaPlaylist: SharedMediaPlaylist { private let webPage: TelegramMediaWebpage private let items: [InstantPageMedia] private let initialItemIndex: Int - var location: SharedMediaPlaylistLocation { + public var location: SharedMediaPlaylistLocation { return InstantPagePlaylistLocation(webpageId: self.webPage.webpageId) } - var currentItemDisappeared: (() -> Void)? + public var currentItemDisappeared: (() -> Void)? private var currentItem: InstantPageMedia? private var playedToEnd: Bool = false private var order: MusicPlaybackSettingsOrder = .regular - private(set) var looping: MusicPlaybackSettingsLooping = .none + public private(set) var looping: MusicPlaybackSettingsLooping = .none - let id: SharedMediaPlaylistId + public let id: SharedMediaPlaylistId private let stateValue = Promise() - var state: Signal { + public var state: Signal { return self.stateValue.get() } - init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) { + public init(webPage: TelegramMediaWebpage, items: [InstantPageMedia], initialItemIndex: Int) { assert(Queue.mainQueue().isCurrent()) self.id = InstantPageMediaPlaylistId(webpageId: webPage.webpageId) @@ -176,7 +176,7 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { self.control(.next) } - func control(_ action: SharedMediaPlaylistControlAction) { + public func control(_ action: SharedMediaPlaylistControlAction) { assert(Queue.mainQueue().isCurrent()) switch action { @@ -228,14 +228,14 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { } } - func setOrder(_ order: MusicPlaybackSettingsOrder) { + public func setOrder(_ order: MusicPlaybackSettingsOrder) { if self.order != order { self.order = order self.updateState() } } - func setLooping(_ looping: MusicPlaybackSettingsLooping) { + public func setLooping(_ looping: MusicPlaybackSettingsLooping) { if self.looping != looping { self.looping = looping self.updateState() @@ -246,6 +246,6 @@ final class InstantPageMediaPlaylist: SharedMediaPlaylist { self.stateValue.set(.single(SharedMediaPlaylistState(loading: false, playedToEnd: self.playedToEnd, item: self.currentItem.flatMap({ InstantPageMediaPlaylistItem(webPage: self.webPage, item: $0) }), nextItem: nil, previousItem: nil, order: self.order, looping: self.looping))) } - func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { + public func onItemPlaybackStarted(_ item: SharedMediaPlaylistItem) { } } diff --git a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift index efb02b214e..2d265e3e97 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePeerReferenceNode.swift @@ -46,7 +46,7 @@ private enum JoinState: Equatable { } } -final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { +public final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { private let context: AccountContext let safeInset: CGFloat private let transparent: Bool @@ -197,7 +197,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { self.joinDisposable.dispose() } - func update(strings: PresentationStrings, theme: InstantPageTheme) { + public func update(strings: PresentationStrings, theme: InstantPageTheme) { if self.strings !== strings || self.theme !== theme { let themeUpdated = self.theme !== theme self.strings = strings @@ -206,7 +206,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { } private func applyThemeAndStrings(themeUpdated: Bool) { @@ -263,7 +263,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - override func layout() { + public override func layout() { super.layout() let size = self.bounds.size @@ -290,14 +290,14 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode { } } - func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { + public func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return nil } - func updateHiddenMedia(media: InstantPageMedia?) { + public func updateHiddenMedia(media: InstantPageMedia?) { } - func updateIsVisible(_ isVisible: Bool) { + public func updateIsVisible(_ isVisible: Bool) { } @objc func buttonPressed() { diff --git a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift index 3470e9d9c6..778b703305 100644 --- a/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift +++ b/submodules/InstantPageUI/Sources/InstantPagePlayableVideoItem.swift @@ -16,7 +16,7 @@ public final class InstantPagePlayableVideoItem: InstantPageItem { return [self.media] } - let interactive: Bool + public let interactive: Bool public let wantsNode: Bool = true public let separatesTiles: Bool = false diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 6f32d2bd28..6a3e013311 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -327,7 +327,7 @@ extension ActionSheetControllerTheme { } } -extension ActionSheetController { +public extension ActionSheetController { convenience init(instantPageTheme: InstantPageTheme) { self.init(theme: ActionSheetControllerTheme(instantPageTheme: instantPageTheme), allowInputInset: false) } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index f522d8bb2d..0a88ade9d2 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -12,14 +12,6 @@ struct ArbitraryRandomNumberGenerator : RandomNumberGenerator { func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) } } -func createEmitterBehavior(type: String) -> NSObject { - let selector = ["behaviorWith", "Type:"].joined(separator: "") - let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type - let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! - let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) - return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) -} - func generateMaskImage(size originalSize: CGSize, position: CGPoint, inverse: Bool) -> UIImage? { var size = originalSize var position = position @@ -123,10 +115,10 @@ public class InvisibleInkDustView: UIView { emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") @@ -435,10 +427,10 @@ public class InvisibleInkDustNode: ASDisplayNode { emitter.setValue(2.0, forKey: "massRange") self.emitter = emitter - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index d6af2f9e1a..5e27921837 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -40,12 +40,12 @@ public class MediaDustLayer: CALayer { emitter.setValue(0.01, forKey: "massRange") self.emitter = emitter - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") - let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") scaleBehavior.setValue("scale", forKey: "keyPath") scaleBehavior.setValue([0.0, 0.5], forKey: "values") scaleBehavior.setValue([0.0, 0.05], forKey: "locations") @@ -154,31 +154,31 @@ public class MediaDustNode: ASDisplayNode { emitter.setValue(0.01, forKey: "massRange") self.emitter = emitter - let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + let alphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") alphaBehavior.setValue("color.alpha", forKey: "keyPath") alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") alphaBehavior.setValue(true, forKey: "additive") - let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + let scaleBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") scaleBehavior.setValue("scale", forKey: "keyPath") scaleBehavior.setValue([0.0, 0.5], forKey: "values") scaleBehavior.setValue([0.0, 0.05], forKey: "locations") - let randomAttractor0 = createEmitterBehavior(type: "simpleAttractor") + let randomAttractor0 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") randomAttractor0.setValue("randomAttractor0", forKey: "name") randomAttractor0.setValue(20, forKey: "falloff") randomAttractor0.setValue(35, forKey: "radius") randomAttractor0.setValue(5, forKey: "stiffness") randomAttractor0.setValue(NSValue(cgPoint: .zero), forKey: "position") - let randomAttractor1 = createEmitterBehavior(type: "simpleAttractor") + let randomAttractor1 = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") randomAttractor1.setValue("randomAttractor1", forKey: "name") randomAttractor1.setValue(20, forKey: "falloff") randomAttractor1.setValue(35, forKey: "radius") randomAttractor1.setValue(5, forKey: "stiffness") randomAttractor1.setValue(NSValue(cgPoint: .zero), forKey: "position") - let fingerAttractor = createEmitterBehavior(type: "simpleAttractor") + let fingerAttractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") fingerAttractor.setValue("fingerAttractor", forKey: "name") let behaviors = [randomAttractor0, randomAttractor1, fingerAttractor, alphaBehavior, scaleBehavior] diff --git a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift index 7382d2117b..509508353a 100644 --- a/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift +++ b/submodules/ItemListVenueItem/Sources/ItemListVenueItem.swift @@ -10,11 +10,17 @@ import LocationResources import ShimmerEffect public final class ItemListVenueItem: ListViewItem, ItemListItem { + public enum InfoIcon { + case info + case goTo + } + let presentationData: ItemListPresentationData let engine: TelegramEngine let venue: TelegramMediaMap? let title: String? let subtitle: String? + let icon: InfoIcon let style: ItemListStyle let action: (() -> Void)? let infoAction: (() -> Void)? @@ -22,12 +28,13 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem { public let sectionId: ItemListSectionId let header: ListViewItemHeader? - public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { + public init(presentationData: ItemListPresentationData, engine: TelegramEngine, venue: TelegramMediaMap?, title: String? = nil, subtitle: String? = nil, icon: ItemListVenueItem.InfoIcon = .info, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) { self.presentationData = presentationData self.engine = engine self.venue = venue self.title = title self.subtitle = subtitle + self.icon = icon self.sectionId = sectionId self.style = style self.action = action @@ -274,7 +281,15 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode { strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor strongSelf.backgroundNode.backgroundColor = itemBackgroundColor strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor - strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal) + + let iconName: String + switch item.icon { + case .info: + iconName = "Location/InfoIcon" + case .goTo: + iconName = "Location/GoTo" + } + strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: iconName), color: item.presentationData.theme.list.itemAccentColor), for: .normal) } let transition = ContainedViewLayoutTransition.immediate diff --git a/submodules/LocationUI/Sources/LocationMapNode.swift b/submodules/LocationUI/Sources/LocationMapNode.swift index e2aa9886b9..9025e079fd 100644 --- a/submodules/LocationUI/Sources/LocationMapNode.swift +++ b/submodules/LocationUI/Sources/LocationMapNode.swift @@ -189,6 +189,7 @@ public final class LocationMapNode: ASDisplayNode, MKMapViewDelegateTarget { public static let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016) public static let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008) + public static let globalMapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) class ProximityCircleRenderer: MKCircleRenderer { override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 5cefd8611b..16cb1f95dd 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -24,7 +24,7 @@ class LocationPickerInteraction { let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let goToUserLocation: () -> Void - let goToCoordinate: (CLLocationCoordinate2D) -> Void + let goToCoordinate: (CLLocationCoordinate2D, Bool) -> Void let openSearch: () -> Void let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void @@ -33,7 +33,7 @@ class LocationPickerInteraction { let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void - init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { + init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue @@ -231,14 +231,14 @@ public final class LocationPickerController: ViewController, AttachmentContainab return } strongSelf.controllerNode.goToUserLocation() - }, goToCoordinate: { [weak self] coordinate in + }, goToCoordinate: { [weak self] coordinate, zoomOut in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false - state.selectedLocation = .location(coordinate, nil) + state.selectedLocation = .location(coordinate, nil, zoomOut) state.searchingVenuesAround = false return state } diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 5e8ff2ca93..77df9cf0d4 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -219,7 +219,7 @@ private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEn enum LocationPickerLocation: Equatable { case none case selecting - case location(CLLocationCoordinate2D, String?) + case location(CLLocationCoordinate2D, String?, Bool) case venue(TelegramMediaMap, Int64?, String?) var isCustom: Bool { @@ -245,8 +245,8 @@ enum LocationPickerLocation: Equatable { } else { return false } - case let .location(lhsCoordinate, lhsAddress): - if case let .location(rhsCoordinate, rhsAddress) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress { + case let .location(lhsCoordinate, lhsAddress, lhsGlobal): + if case let .location(rhsCoordinate, rhsAddress, rhsGlobal) = rhs, locationCoordinatesAreEqual(lhsCoordinate, rhsCoordinate), lhsAddress == rhsAddress, lhsGlobal == rhsGlobal { return true } else { return false @@ -589,7 +589,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM var entries: [LocationPickerEntry] = [] switch state.selectedLocation { - case let .location(coordinate, address): + case let .location(coordinate, address, _): let title: String switch strongSelf.mode { case .share: @@ -722,12 +722,13 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM strongSelf.headerNode.mapNode.resetAnnotationSelection() case .selecting: strongSelf.headerNode.mapNode.resetAnnotationSelection() - case let .location(coordinate, address): + case let .location(coordinate, address, global): var updateMap = false + let span = global ? LocationMapNode.globalMapSpan : LocationMapNode.defaultMapSpan switch previousState.selectedLocation { case .none, .venue: updateMap = true - case let .location(previousCoordinate, _): + case let .location(previousCoordinate, _, _): if !locationCoordinatesAreEqual(previousCoordinate, coordinate) { updateMap = true } @@ -735,7 +736,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM break } if updateMap { - strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, hidePicker: false, animated: true) + strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: span, isUserLocation: false, hidePicker: false, animated: true) strongSelf.headerNode.mapNode.switchToPicking(animated: false) } @@ -849,11 +850,11 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM )) } - if case let .location(coordinate, address) = state.selectedLocation, address == nil { + if case let .location(coordinate, address, global) = state.selectedLocation, address == nil { setupGeocoding(coordinate, { [weak self] geoAddress, address, cityName, streetName, countryCode, isStreet in self?.updateState { state in var state = state - state.selectedLocation = .location(coordinate, address) + state.selectedLocation = .location(coordinate, address, global) state.geoAddress = geoAddress state.city = cityName state.street = streetName @@ -938,7 +939,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM strongSelf.updateState { state in var state = state if case .selecting = state.selectedLocation { - state.selectedLocation = .location(coordinate, nil) + state.selectedLocation = .location(coordinate, nil, false) state.searchingVenuesAround = false } return state @@ -1231,7 +1232,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } func requestPlacesAtSelectedLocation() { - if case let .location(coordinate, _) = self.state.selectedLocation { + if case let .location(coordinate, _, _) = self.state.selectedLocation { self.headerNode.mapNode.setMapCenter(coordinate: coordinate, animated: true) self.searchVenuesPromise.set(.single(coordinate)) self.updateState { state in diff --git a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift index 38f7848b76..0749c3fff0 100644 --- a/submodules/LocationUI/Sources/LocationSearchContainerNode.swift +++ b/submodules/LocationUI/Sources/LocationSearchContainerNode.swift @@ -23,6 +23,7 @@ private struct LocationSearchEntry: Identifiable, Comparable { let resultId: String? let title: String? let distance: Double + let story: Bool var stableId: String { return self.location.venue?.id ?? "" @@ -50,6 +51,9 @@ private struct LocationSearchEntry: Identifiable, Comparable { if lhs.distance != rhs.distance { return false } + if lhs.story != rhs.story { + return false + } return true } @@ -57,7 +61,7 @@ private struct LocationSearchEntry: Identifiable, Comparable { return lhs.index < rhs.index } - func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> ListViewItem { + func item(engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem { let venue = self.location let queryId = self.queryId let resultId = self.resultId @@ -71,9 +75,11 @@ private struct LocationSearchEntry: Identifiable, Comparable { header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings) subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).string } - return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, style: .plain, action: { + return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: self.location, title: self.title, subtitle: subtitle, icon: .goTo, style: .plain, action: { sendVenue(venue, queryId, resultId) - }, header: header) + }, infoAction: self.story && venue.venue == nil ? { + goToVenue(venue) + } : nil, header: header) } } @@ -86,12 +92,12 @@ struct LocationSearchContainerTransition { let isEmpty: Bool } -private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void) -> LocationSearchContainerTransition { +private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], query: String, isSearching: Bool, isEmpty: Bool, engine: TelegramEngine, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, goToVenue: @escaping (TelegramMediaMap) -> Void) -> LocationSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } - let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } - let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, sendVenue: sendVenue, goToVenue: goToVenue), directionHint: nil) } return LocationSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, query: query, isSearching: isSearching, isEmpty: isEmpty) } @@ -99,6 +105,7 @@ private func locationSearchContainerPreparedTransition(from fromEntries: [Locati final class LocationSearchContainerNode: ASDisplayNode { private let context: AccountContext private let interaction: LocationPickerInteraction + private let story: Bool private let dimNode: ASDisplayNode public let listNode: ListView @@ -122,6 +129,7 @@ final class LocationSearchContainerNode: ASDisplayNode { public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction, story: Bool) { self.context = context self.interaction = interaction + self.story = story let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -162,6 +170,8 @@ final class LocationSearchContainerNode: ASDisplayNode { let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let themeAndStringsPromise = self.themeAndStringsPromise + let locale = localeWithStrings(presentationData.strings) + let isSearching = self._isSearching let searchItems = self.searchQuery.get() |> mapToSignal { query -> Signal in @@ -178,7 +188,6 @@ final class LocationSearchContainerNode: ASDisplayNode { |> afterCompleted { isSearching.set(false) } - let locale = localeWithStrings(presentationData.strings) let foundPlacemarks = geocodeLocation(address: query, locale: locale) return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get()) |> delay(0.1, queue: Queue.concurrentDefaultQueue()) @@ -194,9 +203,13 @@ final class LocationSearchContainerNode: ASDisplayNode { guard let placemarkLocation = placemark.location else { continue } - let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + var address: MapGeoAddress? + if let countryCode = placemark.isoCountryCode, placemark.thoroughfare == nil { + address = MapGeoAddress(country: countryCode, state: placemark.administrativeArea, city: placemark.locality, street: nil) + } + let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: address, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) - entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation))) + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, queryId: nil, resultId: nil, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation), story: story)) index += 1 } @@ -207,7 +220,7 @@ final class LocationSearchContainerNode: ASDisplayNode { switch result.message { case let .mapLocation(mapMedia, _): if let _ = mapMedia.venue { - entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0)) + entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: mapMedia, queryId: contextResult.queryId, resultId: result.id, title: nil, distance: 0.0, story: story)) index += 1 } default: @@ -235,10 +248,21 @@ final class LocationSearchContainerNode: ASDisplayNode { self?.listNode.clearHighlightAnimated(true) if let _ = venue.venue { self?.interaction.sendVenue(venue, queryId, resultId) + } else if story, let address = venue.address { + let name: String + if let city = address.city { + name = city + } else { + name = displayCountryName(address.country, locale: locale) + } + self?.interaction.sendLocation(venue.coordinate, name, address) } else { - self?.interaction.goToCoordinate(venue.coordinate) + self?.interaction.goToCoordinate(venue.coordinate, false) self?.interaction.dismissSearch() } + }, goToVenue: { venue in + self?.interaction.goToCoordinate(venue.coordinate, true) + self?.interaction.dismissSearch() }) strongSelf.enqueueTransition(transition) } diff --git a/submodules/MediaPickerUI/BUILD b/submodules/MediaPickerUI/BUILD index 8a2730e157..af3769ac24 100644 --- a/submodules/MediaPickerUI/BUILD +++ b/submodules/MediaPickerUI/BUILD @@ -50,6 +50,7 @@ swift_library( "//submodules/ChatSendMessageActionUI", "//submodules/ComponentFlow", "//submodules/Components/ComponentDisplayAdapters", + "//submodules/AnimatedCountLabelNode", ], visibility = [ "//visibility:public", diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 5e53e62236..5e151f6597 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -26,6 +26,7 @@ import CameraScreen import MediaEditor import ImageObjectSeparation import ChatSendMessageActionUI +import AnimatedCountLabelNode final class MediaPickerInteraction { let downloadManager: AssetDownloadManager @@ -193,7 +194,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private let saveEditedPhotos: Bool private let titleView: MediaPickerTitleView + private let cancelButtonNode: WebAppCancelButtonNode private let moreButtonNode: MoreButtonNode + private let selectedButtonNode: SelectedButtonNode public weak var webSearchController: WebSearchController? @@ -227,6 +230,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var getCurrentSendMessageContextMediaPreview: (() -> ChatSendMessageContextScreenMediaPreview?)? = nil private let selectedCollection = Promise(nil) + private var selectedCollectionValue: PHAssetCollection? { + didSet { + self.selectedCollection.set(.single(self.selectedCollectionValue)) + } + } var dismissAll: () -> Void = { } @@ -935,8 +943,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - - var previousEntries = self.currentEntries if self.resetOnUpdate { @@ -992,7 +998,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate) } - private var currentDisplayMode: DisplayMode = .all + private(set) var currentDisplayMode: DisplayMode = .all { + didSet { + self.displayModeUpdated(self.currentDisplayMode) + } + } + var displayModeUpdated: (DisplayMode) -> Void = { _ in } + func updateDisplayMode(_ displayMode: DisplayMode, animated: Bool = true) { let updated = self.currentDisplayMode != displayMode self.currentDisplayMode = displayMode @@ -1803,9 +1815,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = presentationData.strings.Attachment_Gallery } + self.cancelButtonNode = WebAppCancelButtonNode(theme: self.presentationData.theme, strings: self.presentationData.strings) + self.moreButtonNode = MoreButtonNode(theme: self.presentationData.theme) self.moreButtonNode.iconNode.enqueueState(.more, animated: false) + self.selectedButtonNode = SelectedButtonNode(theme: self.presentationData.theme) + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) self.statusBar.statusBarStyle = .Ignore @@ -1906,7 +1922,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { if case let .assets(collection, _) = self.subject, collection != nil { self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed)) } else { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(customDisplayNode: self.cancelButtonNode) + self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed) + self.navigationItem.leftBarButtonItem?.target = self + +// self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } if self.bannedSendPhotos != nil && self.bannedSendVideos != nil { @@ -1923,6 +1943,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + self.selectedButtonNode.addTarget(self, action: #selector(self.selectedPressed), forControlEvents: .touchUpInside) + self.scrollToTop = { [weak self] in if let strongSelf = self { if let webSearchController = strongSelf.webSearchController { @@ -2050,6 +2072,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self._ready.set(self.controllerNode.ready.get()) + self.controllerNode.displayModeUpdated = { [weak self] _ in + guard let self else { + return + } + let count = Int32(self.interaction?.selectionState?.count() ?? 0) + self.updateSelectionState(count: count) + } if case .media = self.subject { self.controllerNode.updateDisplayMode(.selected, animated: false) } @@ -2086,10 +2115,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } self.controllerNode.resetOnUpdate = true if collection.assetCollectionSubtype == .smartAlbumUserLibrary { - self.selectedCollection.set(.single(nil)) + self.selectedCollectionValue = nil self.titleView.title = self.presentationData.strings.MediaPicker_Recents } else { - self.selectedCollection.set(.single(collection)) + self.selectedCollectionValue = collection self.titleView.title = collection.localizedTitle ?? "" } self.scrollToTop?() @@ -2211,6 +2240,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { fileprivate func updateSelectionState(count: Int32) { self.selectionCount = count + let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) var moreIsVisible = false if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) { moreIsVisible = true @@ -2220,25 +2250,32 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { moreIsVisible = true // self.moreButtonNode.iconNode.enqueueState(.more, animated: false) } else { - if count > 0 { - self.titleView.segments = [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(count)] - self.titleView.segmentsHidden = false - moreIsVisible = true -// self.moreButtonNode.iconNode.enqueueState(.more, animated: true) + let title: String + let isEnabled: Bool + if self.controllerNode.currentDisplayMode == .selected { + title = self.presentationData.strings.Attachment_SelectedMedia(count) + isEnabled = false } else { - self.titleView.segmentsHidden = true - moreIsVisible = false -// self.moreButtonNode.iconNode.enqueueState(.search, animated: true) - - if self.titleView.index != 0 { - Queue.mainQueue().after(0.3) { - self.titleView.index = 0 - } - } + title = self.selectedCollectionValue?.localizedTitle ?? self.presentationData.strings.MediaPicker_Recents + isEnabled = true } + self.titleView.updateTitle(title: title, isEnabled: isEnabled, animated: true) + self.cancelButtonNode.setState(isEnabled ? .cancel : .back, animated: true) + + let isSelectionButtonVisible = count > 0 && self.controllerNode.currentDisplayMode == .all + transition.updateAlpha(node: self.selectedButtonNode, alpha: isSelectionButtonVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.selectedButtonNode, scale: isSelectionButtonVisible ? 1.0 : 0.01) + + let selectedSize = self.selectedButtonNode.update(count: count) + if self.selectedButtonNode.supernode == nil { + self.navigationBar?.addSubnode(self.selectedButtonNode) + } + self.selectedButtonNode.frame = CGRect(origin: CGPoint(x: self.view.bounds.width - 54.0 - selectedSize.width, y: 18.0 + UIScreenPixel), size: selectedSize) + + self.titleView.segmentsHidden = true + moreIsVisible = count > 0 } - let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut) transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) } @@ -2246,7 +2283,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private func updateThemeAndStrings() { self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) self.titleView.theme = self.presentationData.theme + self.cancelButtonNode.theme = self.presentationData.theme self.moreButtonNode.theme = self.presentationData.theme + self.selectedButtonNode.theme = self.presentationData.theme self.controllerNode.updatePresentationData(self.presentationData) } @@ -2304,13 +2343,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { return true } } - - @objc private func cancelPressed() { - self.dismissAllTooltips() - self.dismiss() - } - public override func dismiss(completion: (() -> Void)? = nil) { self.controllerNode.cancelAssetDownloads() @@ -2408,6 +2441,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.groupsController = groupsController } + @objc private func cancelPressed() { + self.dismissAllTooltips() + if case .back = self.cancelButtonNode.state { + self.controllerNode.updateDisplayMode(.all) + } else { + self.dismiss() + } + } + + @objc private func selectedPressed() { + self.controllerNode.updateDisplayMode(.selected, animated: true) + } + @objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { guard self.moreButtonNode.iconNode.alpha > 0.0 else { return @@ -3132,3 +3178,65 @@ public func stickerMediaPickerController( controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller } + +private class SelectedButtonNode: HighlightableButtonNode { + private let background = ASImageNode() + private let icon = ASImageNode() + private let label = ImmediateAnimatedCountLabelNode() + + var theme: PresentationTheme { + didSet { + self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor) + let _ = self.update(count: self.count) + } + } + + private var count: Int32 = 0 + + init(theme: PresentationTheme) { + self.theme = theme + + super.init() + + self.background.displaysAsynchronously = false + self.icon.displaysAsynchronously = false + self.label.displaysAsynchronously = false + + self.icon.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: .white) + self.background.image = generateStretchableFilledCircleImage(radius: 21.0 / 2.0, color: self.theme.list.itemCheckColors.fillColor) + + self.addSubnode(self.background) + self.addSubnode(self.icon) + self.addSubnode(self.label) + } + + func update(count: Int32) -> CGSize { + self.count = count + + let diameter: CGFloat = 21.0 + let font = Font.with(size: 15.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]) + + let stringValue = "\(max(1, count))" + var segments: [AnimatedCountLabelNode.Segment] = [] + for char in stringValue { + if let intValue = Int(String(char)) { + segments.append(.number(intValue, NSAttributedString(string: String(char), font: font, textColor: self.theme.list.itemCheckColors.foregroundColor))) + } + } + self.label.segments = segments + + let textSize = self.label.updateLayout(size: CGSize(width: 100.0, height: diameter), animated: true) + let size = CGSize(width: textSize.width + 28.0, height: diameter) + + if let _ = self.icon.image { + let iconSize = CGSize(width: 22.0, height: 22.0) + let iconFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.icon.frame = iconFrame + } + + self.label.frame = CGRect(origin: CGPoint(x: 21.0, y: floor((size.height - textSize.height) / 2.0) - UIScreenPixel), size: textSize) + self.background.frame = CGRect(origin: .zero, size: size) + + return size + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index 2cae7588f2..bd44af71bb 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -36,6 +36,35 @@ final class MediaPickerTitleView: UIView { } } + public func updateTitle(title: String, isEnabled: Bool, animated: Bool) { + if animated { + if self.title != title { + if let snapshotView = self.titleNode.view.snapshotContentTree() { + snapshotView.frame = self.titleNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.titleNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + if self.isEnabled != isEnabled { + if let snapshotView = self.arrowNode.view.snapshotContentTree() { + snapshotView.frame = self.arrowNode.frame + self.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in + snapshotView.removeFromSuperview() + }) + self.arrowNode.view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + self.title = title + self.isEnabled = isEnabled + } + public var isHighlighted: Bool = false { didSet { self.alpha = self.isHighlighted ? 0.5 : 1.0 @@ -45,7 +74,7 @@ final class MediaPickerTitleView: UIView { public var segmentsHidden = true { didSet { if self.segmentsHidden != oldValue { - let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut) + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0) diff --git a/submodules/PremiumUI/Resources/gift b/submodules/PremiumUI/Resources/gift deleted file mode 100644 index 87a9c2a5e8..0000000000 Binary files a/submodules/PremiumUI/Resources/gift and /dev/null differ diff --git a/submodules/PremiumUI/Resources/gift2.scn b/submodules/PremiumUI/Resources/gift2.scn new file mode 100644 index 0000000000..ffbaec38e3 Binary files /dev/null and b/submodules/PremiumUI/Resources/gift2.scn differ diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star deleted file mode 100644 index 3b27e0220e..0000000000 Binary files a/submodules/PremiumUI/Resources/star and /dev/null differ diff --git a/submodules/PremiumUI/Resources/star2 b/submodules/PremiumUI/Resources/star2 new file mode 100644 index 0000000000..2ab0606554 Binary files /dev/null and b/submodules/PremiumUI/Resources/star2 differ diff --git a/submodules/PremiumUI/Resources/star2.scn b/submodules/PremiumUI/Resources/star2.scn deleted file mode 100644 index 2ed1379f05..0000000000 Binary files a/submodules/PremiumUI/Resources/star2.scn and /dev/null differ diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 900a4d0cd6..453ee956af 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -32,6 +32,8 @@ extension PremiumGiftSource { return "attach" case .settings: return "settings" + case .stars: + return "" case .chatList: return "chats" case .channelBoost: @@ -241,7 +243,6 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { } names.append("**\(context.component.peers[i].compactDisplayTitle)**") } - descriptionString = strings.Premium_Gift_MultipleDescription(names, "").string } else { for i in 0 ..< min(3, context.component.peers.count) { if i == 0 { diff --git a/submodules/StatisticsUI/Sources/ChannelStatsController.swift b/submodules/StatisticsUI/Sources/ChannelStatsController.swift index 8c84f7fb8f..bd2c810e98 100644 --- a/submodules/StatisticsUI/Sources/ChannelStatsController.swift +++ b/submodules/StatisticsUI/Sources/ChannelStatsController.swift @@ -2038,9 +2038,10 @@ public func channelStatsController(context: AccountContext, updatedPresentationD peer.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) let peerData = context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), - TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId), - TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) + TelegramEngine.EngineData.Item.Peer.CanViewStats(id: peerId), + TelegramEngine.EngineData.Item.Peer.AdsRestricted(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId), + TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) ) let longLoadingSignal: Signal = .single(false) |> then(.single(true) |> delay(2.0, queue: Queue.mainQueue())) @@ -2066,7 +2067,7 @@ public func channelStatsController(context: AccountContext, updatedPresentationD ) |> deliverOnMainQueue |> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in - let (adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData + let (canViewStats, adsRestricted, canViewRevenue, canViewStarsRevenue) = peerData var isGroup = false if let peer, case let .channel(channel) = peer, case .group = channel.info { @@ -2149,7 +2150,9 @@ public func channelStatsController(context: AccountContext, updatedPresentationD index = 2 } var tabs: [String] = [] - tabs.append(presentationData.strings.Stats_Statistics) + if canViewStats { + tabs.append(presentationData.strings.Stats_Statistics) + } tabs.append(presentationData.strings.Stats_Boosts) if canViewRevenue || canViewStarsRevenue { tabs.append(presentationData.strings.Stats_Monetization) diff --git a/submodules/Svg/PublicHeaders/Svg/Svg.h b/submodules/Svg/PublicHeaders/Svg/Svg.h index 51e30030be..5df838a162 100755 --- a/submodules/Svg/PublicHeaders/Svg/Svg.h +++ b/submodules/Svg/PublicHeaders/Svg/Svg.h @@ -4,7 +4,7 @@ #import #import -NSData * _Nullable prepareSvgImage(NSData * _Nonnull data); +NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool pattern); UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit); UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, bool opaque); diff --git a/submodules/Svg/Sources/Svg.m b/submodules/Svg/Sources/Svg.m index e86b96599c..bb242235c5 100755 --- a/submodules/Svg/Sources/Svg.m +++ b/submodules/Svg/Sources/Svg.m @@ -361,14 +361,26 @@ UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *b [_data appendBytes:&command length:sizeof(command)]; } +- (void)setFillColor:(uint32_t)color opacity:(CGFloat)opacity { + uint8_t command = 11; + [_data appendBytes:&command length:sizeof(command)]; + + color = ((uint32_t)(opacity * 255.0) << 24) | color; + [_data appendBytes:&color length:sizeof(color)]; +} + @end +UIColor *colorWithBGRA(uint32_t bgra) +{ + return [[UIColor alloc] initWithRed:(((bgra) & 0xff) / 255.0f) green:(((bgra >> 8) & 0xff) / 255.0f) blue:(((bgra >> 16) & 0xff) / 255.0f) alpha:(((bgra >> 24) & 0xff) / 255.0f)]; +} + UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, CGFloat scale, bool fit) { NSDate *startTime = [NSDate date]; UIColor *foregroundColor = [UIColor whiteColor]; - int32_t ptr = 0; int32_t width; int32_t height; @@ -544,7 +556,15 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC CGContextStrokePath(context); } break; + case 11: + { + uint32_t bgra; + [data getBytes:&bgra range:NSMakeRange(ptr, sizeof(bgra))]; + ptr += sizeof(bgra); + CGContextSetFillColorWithColor(context, colorWithBGRA(bgra).CGColor); + CGContextStrokePath(context); + } default: break; } @@ -559,7 +579,7 @@ UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIC return resultImage; } -NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) { +NSData * _Nullable prepareSvgImage(NSData * _Nonnull data, bool template) { NSDate *startTime = [NSDate date]; NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]; @@ -600,8 +620,12 @@ NSData * _Nullable prepareSvgImage(NSData * _Nonnull data) { } if (shape->fill.type != NSVG_PAINT_NONE) { - [context setFillColorWithOpacity:shape->opacity]; - + if (template) { + [context setFillColorWithOpacity:shape->opacity]; + } else { + [context setFillColor:shape->fill.color opacity:shape->opacity]; + } + bool isFirst = true; bool hasStartPoint = false; CGPoint startPoint; diff --git a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift index 31edbae2b2..784164b543 100644 --- a/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/RevenueStatistics.swift @@ -4,11 +4,54 @@ import Postbox import TelegramApi import MtProtoKit -public struct RevenueStats: Equatable { - public struct Balances: Equatable { +public struct RevenueStats: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case topHoursGraph + case revenueGraph + case balances + case usdRate + } + + static func key(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8 + 4) + key.setInt64(0, value: peerId.toInt64()) + return key + } + + public struct Balances: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case currentBalance + case availableBalance + case overallRevenue + } + public let currentBalance: Int64 public let availableBalance: Int64 public let overallRevenue: Int64 + + init( + currentBalance: Int64, + availableBalance: Int64, + overallRevenue: Int64 + ) { + self.currentBalance = currentBalance + self.availableBalance = availableBalance + self.overallRevenue = overallRevenue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.currentBalance = try container.decode(Int64.self, forKey: .currentBalance) + self.availableBalance = try container.decode(Int64.self, forKey: .availableBalance) + self.overallRevenue = try container.decode(Int64.self, forKey: .overallRevenue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.currentBalance, forKey: .currentBalance) + try container.encode(self.availableBalance, forKey: .availableBalance) + try container.encode(self.overallRevenue, forKey: .overallRevenue) + } } public let topHoursGraph: StatsGraph @@ -23,6 +66,22 @@ public struct RevenueStats: Equatable { self.usdRate = usdRate } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.topHoursGraph = try container.decode(StatsGraph.self, forKey: .topHoursGraph) + self.revenueGraph = try container.decode(StatsGraph.self, forKey: .revenueGraph) + self.balances = try container.decode(Balances.self, forKey: .balances) + self.usdRate = try container.decode(Double.self, forKey: .usdRate) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.topHoursGraph, forKey: .topHoursGraph) + try container.encode(self.revenueGraph, forKey: .revenueGraph) + try container.encode(self.balances, forKey: .balances) + try container.encode(self.usdRate, forKey: .usdRate) + } + public static func == (lhs: RevenueStats, rhs: RevenueStats) -> Bool { if lhs.topHoursGraph != rhs.topHoursGraph { return false @@ -124,6 +183,17 @@ private final class RevenueStatsContextImpl { self._statePromise.set(.single(self._state)) self.load() + + let _ = (account.postbox.transaction { transaction -> RevenueStats? in + return transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)))?.get(RevenueStats.self) + } + |> deliverOnMainQueue).start(next: { [weak self] cachedResult in + guard let self, let cachedResult else { + return + } + self._state = RevenueStatsContextState(stats: cachedResult) + self._statePromise.set(.single(self._state)) + }) } deinit { @@ -155,9 +225,17 @@ private final class RevenueStatsContextImpl { self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] stats in - if let strongSelf = self { - strongSelf._state = RevenueStatsContextState(stats: stats) - strongSelf._statePromise.set(.single(strongSelf._state)) + if let self { + self._state = RevenueStatsContextState(stats: stats) + self._statePromise.set(.single(self._state)) + + if let stats { + let _ = (self.account.postbox.transaction { transaction in + if let entry = CodableEntry(stats) { + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedRevenueStats, key: StarsRevenueStats.key(peerId: peerId)), entry: entry) + } + }).start() + } } })) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 66e2542d89..e66a33337f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -127,6 +127,7 @@ public struct Namespaces { public static let applicationIcons: Int8 = 36 public static let availableMessageEffects: Int8 = 37 public static let cachedStarsRevenueStats: Int8 = 38 + public static let cachedRevenueStats: Int8 = 39 } public struct UnorderedItemList { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift index 6a49e26224..2eef259c90 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesSettings.swift @@ -82,6 +82,7 @@ public struct PresentationResourcesSettings { public static let business = renderIcon(name: "Settings/Menu/Business", backgroundColors: [UIColor(rgb: 0xA95CE3), UIColor(rgb: 0xF16B80)]) public static let myProfile = renderIcon(name: "Settings/Menu/Profile") public static let reactions = renderIcon(name: "Settings/Menu/Reactions") + public static let balance = renderIcon(name: "Settings/Menu/Balance", scaleFactor: 0.97, backgroundColors: [UIColor(rgb: 0x34c759)]) public static let premium = generateImage(CGSize(width: 29.0, height: 29.0), contextGenerator: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index cf7fd66196..2deee3a50a 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -745,6 +745,23 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributes[1] = boldAttributes attributedString = addAttributesToStringWithRanges(strings.Notification_PremiumGift_Sent(compactAuthorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } + case let .giftStars(currency, amount, count, _, _, _): + let _ = count + let price = formatCurrencyAmount(amount, currency: currency) + if message.author?.id == accountPeerId { + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_SentYou(price)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + //TODO:localize + var authorName = compactAuthorName + var peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] + if message.id.peerId.namespace == Namespaces.Peer.CloudUser && message.id.peerId.id._internalGetInt64Value() == 777000 { + authorName = "Unknown user" + peerIds = [] + } + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } case let .topicCreated(title, iconColor, iconFileId): if forForumOverview { let maybeFileId = iconFileId ?? 0 @@ -992,6 +1009,39 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) } } + case let .paymentRefunded(peerId, currency, totalAmount, _, _): + //TODO:localize + let patternString: String + if peerId == message.id.peerId { + patternString = "You received a refund of {amount}" + } else { + patternString = "You received a refund of {amount} from {name}" + } + + let mutableString = NSMutableAttributedString() + mutableString.append(NSAttributedString(string: patternString, font: titleFont, textColor: primaryTextColor)) + + var range = NSRange(location: NSNotFound, length: 0) + range = (mutableString.string as NSString).range(of: "{amount}") + if range.location != NSNotFound { + if currency == "XTR" { + let amountAttributedString = NSMutableAttributedString(string: "#\(totalAmount)", font: titleBoldFont, textColor: primaryTextColor) + if let range = amountAttributedString.string.range(of: "#") { + amountAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: amountAttributedString.string)) + amountAttributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: amountAttributedString.string)) + } + mutableString.replaceCharacters(in: range, with: amountAttributedString) + } else { + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: formatCurrencyAmount(totalAmount, currency: currency), font: titleBoldFont, textColor: primaryTextColor)) + } + } + range = (mutableString.string as NSString).range(of: "{name}") + if range.location != NSNotFound { + let peerName = message.peers[peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + mutableString.replaceCharacters(in: range, with: NSAttributedString(string: peerName, font: titleBoldFont, textColor: primaryTextColor)) + mutableString.addAttribute(NSAttributedString.Key(TelegramTextAttributes.PeerMention), value: TelegramPeerMention(peerId: peerId, mention: ""), range: NSMakeRange(range.location, (peerName as NSString).length)) + } + attributedString = mutableString case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD index 1cab69556f..1b071b8cfd 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/UndoUI", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/NavigationStackComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift index 17359fffd5..49dccb428e 100644 --- a/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift +++ b/submodules/TelegramUI/Components/Ads/AdsReportScreen/Sources/AdsReportScreen.swift @@ -13,6 +13,7 @@ import BalancedTextComponent import MultilineTextComponent import ListSectionComponent import ListActionItemComponent +import NavigationStackComponent import ItemListUI import UndoUI import AccountContext @@ -655,297 +656,3 @@ public final class AdsReportScreen: ViewControllerComponentContainer { } } } - - - -private final class NavigationContainer: UIView, UIGestureRecognizerDelegate { - var requestUpdate: ((ComponentTransition) -> Void)? - var requestPop: (() -> Void)? - var transitionFraction: CGFloat = 0.0 - - private var panRecognizer: InteractiveTransitionGestureRecognizer? - - var isNavigationEnabled: Bool = false { - didSet { - self.panRecognizer?.isEnabled = self.isNavigationEnabled - } - } - - init() { - super.init(frame: .zero) - - let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in - guard let strongSelf = self else { - return [] - } - let _ = strongSelf - return [.right] - }) - panRecognizer.delegate = self - self.addGestureRecognizer(panRecognizer) - self.panRecognizer = panRecognizer - } - - required public init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { - return false - } - if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { - return true - } - return false - } - - @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { - switch recognizer.state { - case .began: - self.transitionFraction = 0.0 - case .changed: - let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width - let transitionFraction = max(0.0, min(1.0, distanceFactor)) - if self.transitionFraction != transitionFraction { - self.transitionFraction = transitionFraction - self.requestUpdate?(.immediate) - } - case .ended, .cancelled: - let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width - let transitionFraction = max(0.0, min(1.0, distanceFactor)) - if transitionFraction > 0.2 { - self.transitionFraction = 0.0 - self.requestPop?() - } else { - self.transitionFraction = 0.0 - self.requestUpdate?(.spring(duration: 0.45)) - } - default: - break - } - } -} - -final class NavigationStackComponent: Component { - public let items: [AnyComponentWithIdentity] - public let requestPop: () -> Void - - public init( - items: [AnyComponentWithIdentity], - requestPop: @escaping () -> Void - ) { - self.items = items - self.requestPop = requestPop - } - - public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool { - if lhs.items != rhs.items { - return false - } - return true - } - - private final class ItemView: UIView { - let contents = ComponentView() - let dimView = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - - self.dimView.alpha = 0.0 - self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) - self.dimView.isUserInteractionEnabled = false - self.addSubview(self.dimView) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - } - - private struct ReadyItem { - var index: Int - var itemId: AnyHashable - var itemView: ItemView - var itemTransition: ComponentTransition - var itemSize: CGSize - - init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) { - self.index = index - self.itemId = itemId - self.itemView = itemView - self.itemTransition = itemTransition - self.itemSize = itemSize - } - } - - public final class View: UIView { - private var itemViews: [AnyHashable: ItemView] = [:] - private let navigationContainer = NavigationContainer() - - private var component: NavigationStackComponent? - private var state: EmptyComponentState? - - public override init(frame: CGRect) { - super.init(frame: CGRect()) - - self.addSubview(self.navigationContainer) - - self.navigationContainer.requestUpdate = { [weak self] transition in - guard let self else { - return - } - self.state?.updated(transition: transition) - } - - self.navigationContainer.requestPop = { [weak self] in - guard let self else { - return - } - self.component?.requestPop() - } - } - - required public init?(coder: NSCoder) { - preconditionFailure() - } - - func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - self.state = state - - let navigationTransitionFraction = self.navigationContainer.transitionFraction - self.navigationContainer.isNavigationEnabled = component.items.count > 1 - - var validItemIds: [AnyHashable] = [] - - - var readyItems: [ReadyItem] = [] - for i in 0 ..< component.items.count { - let item = component.items[i] - let itemId = item.id - validItemIds.append(itemId) - - let itemView: ItemView - var itemTransition = transition - if let current = self.itemViews[itemId] { - itemView = current - } else { - itemTransition = itemTransition.withAnimation(.none) - itemView = ItemView() - self.itemViews[itemId] = itemView - itemView.contents.parentState = state - } - - let itemSize = itemView.contents.update( - transition: itemTransition, - component: item.component, - environment: { environment[ChildEnvironment.self] }, - containerSize: CGSize(width: availableSize.width, height: availableSize.height) - ) - - readyItems.append(ReadyItem( - index: i, - itemId: itemId, - itemView: itemView, - itemTransition: itemTransition, - itemSize: itemSize - )) - } - - let sortedItems = readyItems.sorted(by: { $0.index < $1.index }) - for readyItem in sortedItems { - let transitionFraction: CGFloat - let alphaTransitionFraction: CGFloat - if readyItem.index == readyItems.count - 1 { - transitionFraction = navigationTransitionFraction - alphaTransitionFraction = 1.0 - } else if readyItem.index == readyItems.count - 2 { - transitionFraction = navigationTransitionFraction - 1.0 - alphaTransitionFraction = navigationTransitionFraction - } else { - transitionFraction = 0.0 - alphaTransitionFraction = 0.0 - } - - let transitionOffset: CGFloat - if readyItem.index == readyItems.count - 1 { - transitionOffset = readyItem.itemSize.width * transitionFraction - } else { - transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction - } - - let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize) - - let itemBounds = CGRect(origin: .zero, size: itemFrame.size) - if let itemComponentView = readyItem.itemView.contents.view { - var isAdded = false - if itemComponentView.superview == nil { - isAdded = true - - readyItem.itemView.insertSubview(itemComponentView, at: 0) - self.navigationContainer.addSubview(readyItem.itemView) - } - readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) - readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds) - readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize)) - readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction) - - if readyItem.index > 0 && isAdded { - transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil) - } - } - } - - let lastHeight = sortedItems.last?.itemSize.height ?? 0.0 - let previousHeight: CGFloat - if sortedItems.count > 1 { - previousHeight = sortedItems[sortedItems.count - 2].itemSize.height - } else { - previousHeight = lastHeight - } - let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction - - var removedItemIds: [AnyHashable] = [] - for (id, _) in self.itemViews { - if !validItemIds.contains(id) { - removedItemIds.append(id) - } - } - for id in removedItemIds { - guard let itemView = self.itemViews[id] else { - continue - } - if let itemComponeentView = itemView.contents.view { - var position = itemComponeentView.center - position.x += itemComponeentView.bounds.width - transition.setPosition(view: itemComponeentView, position: position, completion: { _ in - itemView.removeFromSuperview() - self.itemViews.removeValue(forKey: id) - }) - } else { - itemView.removeFromSuperview() - self.itemViews.removeValue(forKey: id) - } - } - - let contentSize = CGSize(width: availableSize.width, height: contentHeight) - self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize) - - return contentSize - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index a261f2b647..b78c2bcf09 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -203,6 +203,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .giftPremium = action.action { result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .giftStars = action.action { + result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedProfilePhoto = action.action { result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .setChatWallpaper = action.action { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 252c593418..152625ba23 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -235,6 +235,8 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in var giftSize = CGSize(width: 220.0, height: 240.0) + let incoming = item.message.effectivelyIncoming(item.context.account.peerId) + let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: EngineMessage(item.message), accountPeerId: item.context.account.peerId) let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText @@ -252,6 +254,14 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { case let .giftPremium(_, _, monthsValue, _, _): months = monthsValue text = item.presentationData.strings.Notification_PremiumGift_Subtitle(item.presentationData.strings.Notification_PremiumGift_Months(months)).string + case let .giftStars(_, _, count, _, _, _): + months = 6 + var peerName = "" + if let peer = item.message.peers[item.message.id.peerId] { + peerName = EnginePeer(peer).compactDisplayTitle + } + title = item.presentationData.strings.Notification_StarsGift_Title(Int32(count)) + text = incoming ? item.presentationData.strings.Notification_StarsGift_Subtitle : item.presentationData.strings.Notification_StarsGift_SubtitleYou(peerName).string case let .giftCode(_, fromGiveaway, unclaimed, channelId, monthsValue, _, _, _, _): if channelId == nil { months = monthsValue diff --git a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift index d6556c800c..66cb841f9c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatSendStarsScreen/Sources/ChatSendStarsScreen.swift @@ -137,15 +137,22 @@ private final class BalanceComponent: CombinedComponent { } private final class BadgeComponent: Component { + enum Direction { + case left + case right + } let theme: PresentationTheme let title: String + let inertiaDirection: Direction? init( theme: PresentationTheme, - title: String + title: String, + inertiaDirection: Direction? ) { self.theme = theme self.title = title + self.inertiaDirection = inertiaDirection } static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool { @@ -155,6 +162,9 @@ private final class BadgeComponent: Component { if lhs.title != rhs.title { return false } + if lhs.inertiaDirection != rhs.inertiaDirection { + return false + } return true } @@ -174,6 +184,7 @@ private final class BadgeComponent: Component { private var component: BadgeComponent? private var previousAvailableSize: CGSize? + private var previousInertiaDirection: BadgeComponent.Direction? override init(frame: CGRect) { self.badgeView = UIView() @@ -225,9 +236,8 @@ private final class BadgeComponent: Component { required init(coder: NSCoder) { preconditionFailure() } - + func update(component: BadgeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - if self.component == nil { self.badgeIcon.image = UIImage(bundleImageName: "Premium/SendStarsStarSliderIcon")?.withRenderingMode(.alwaysTemplate) } @@ -237,23 +247,8 @@ private final class BadgeComponent: Component { self.badgeLabel.color = .white - let countWidth: CGFloat - switch component.title.count { - case 1: - countWidth = 20.0 - case 2: - countWidth = 35.0 - case 3: - countWidth = 51.0 - case 4: - countWidth = 60.0 - case 5: - countWidth = 74.0 - case 6: - countWidth = 88.0 - default: - countWidth = 51.0 - } + let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) + let countWidth: CGFloat = badgeLabelSize.width + 3.0 let badgeWidth: CGFloat = countWidth + 54.0 let badgeSize = CGSize(width: badgeWidth, height: 48.0) @@ -265,6 +260,25 @@ private final class BadgeComponent: Component { transition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0)) + if component.inertiaDirection != self.previousInertiaDirection { + self.previousInertiaDirection = component.inertiaDirection + + var angle: CGFloat = 0.0 + let transition: ContainedViewLayoutTransition + if let inertiaDirection = component.inertiaDirection { + switch inertiaDirection { + case .left: + angle = 0.22 + case .right: + angle = -0.22 + } + transition = .animated(duration: 0.45, curve: .spring) + } else { + transition = .animated(duration: 0.45, curve: .customSpring(damping: 65.0, initialVelocity: 0.0)) + } + transition.updateTransformRotation(view: self.badgeView, angle: angle) + } + self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0) @@ -276,8 +290,6 @@ private final class BadgeComponent: Component { self.badgeView.alpha = 1.0 let size = badgeSize - - let badgeLabelSize = self.badgeLabel.update(value: component.title, transition: .easeInOut(duration: 0.12)) transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: 14.0 + floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: 5.0), size: badgeLabelSize)) if self.previousAvailableSize != availableSize { @@ -651,9 +663,11 @@ private final class ChatSendStarsScreenComponent: Component { private let title = ComponentView() private let descriptionText = ComponentView() + private let badgeStars = BadgeStarsView() private let slider = ComponentView() private let sliderBackground = UIView() private let sliderForeground = UIView() + private let sliderStars = SliderStarsView() private let badge = ComponentView() private var topPeersLeftSeparator: SimpleLayer? @@ -703,9 +717,7 @@ private final class ChatSendStarsScreenComponent: Component { self.addSubview(self.dimView) self.layer.addSublayer(self.backgroundLayer) - - self.addSubview(self.navigationBarContainer) - + self.scrollView.delaysContentTouches = true self.scrollView.canCancelContentTouches = true self.scrollView.clipsToBounds = false @@ -728,6 +740,11 @@ private final class ChatSendStarsScreenComponent: Component { self.scrollView.addSubview(self.scrollContentView) + self.sliderForeground.clipsToBounds = true + self.sliderForeground.addSubview(self.sliderStars) + + self.addSubview(self.navigationBarContainer) + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } @@ -830,6 +847,10 @@ private final class ChatSendStarsScreenComponent: Component { } } + private var previousSliderValue: Float = 0.0 + private var previousTimestamp: Double? + private var inertiaDirection: BadgeComponent.Direction? + func update(component: ChatSendStarsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let environment = environment[ViewControllerComponentContainer.Environment.self].value let themeUpdated = self.environment?.theme !== environment.theme @@ -881,6 +902,53 @@ private final class ChatSendStarsScreenComponent: Component { } self.amount = 1 + Int64(value) self.state?.updated(transition: .immediate) + + let sliderValue = Float(value) / 1000.0 + let currentTimestamp = CACurrentMediaTime() + + if let previousTimestamp { + let deltaTime = currentTimestamp - previousTimestamp + let delta = sliderValue - self.previousSliderValue + let deltaValue = abs(sliderValue - self.previousSliderValue) + + let speed = deltaValue / Float(deltaTime) + let newSpeed = max(0, min(65.0, speed * 70.0)) + + var inertiaDirection: BadgeComponent.Direction? + if newSpeed >= 1.0 { + if delta > 0.0 { + inertiaDirection = .right + } else { + inertiaDirection = .left + } + } + if inertiaDirection != self.inertiaDirection { + self.inertiaDirection = inertiaDirection + self.state?.updated(transition: .immediate) + } + + if newSpeed < 0.01 && deltaValue < 0.001 { + + } else { + self.badgeStars.update(speed: newSpeed, delta: delta) + } + } + + self.previousSliderValue = sliderValue + self.previousTimestamp = currentTimestamp + }, + isTrackingUpdated: { [weak self] isTracking in + guard let self else { + return + } + if !isTracking { + self.previousTimestamp = nil + self.badgeStars.update(speed: 0.0) + } + if self.inertiaDirection != nil { + self.inertiaDirection = nil + self.state?.updated(transition: .immediate) + } } )), environment: {}, @@ -889,6 +957,7 @@ private final class ChatSendStarsScreenComponent: Component { let sliderFrame = CGRect(origin: CGPoint(x: sliderInset, y: contentHeight + 127.0), size: sliderSize) if let sliderView = self.slider.view { if sliderView.superview == nil { + self.scrollContentView.addSubview(self.badgeStars) self.scrollContentView.addSubview(self.sliderBackground) self.scrollContentView.addSubview(self.sliderForeground) self.scrollContentView.addSubview(sliderView) @@ -910,20 +979,30 @@ private final class ChatSendStarsScreenComponent: Component { self.sliderBackground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 self.sliderForeground.layer.cornerRadius = sliderBackgroundFrame.height * 0.5 + self.sliderStars.frame = CGRect(origin: .zero, size: sliderBackgroundFrame.size) + self.sliderStars.update(size: sliderBackgroundFrame.size, value: progressFraction) + self.sliderForeground.isHidden = sliderForegroundFrame.width <= sliderMinWidth + var effectiveInertiaDirection = self.inertiaDirection + if progressFraction <= 0.03 || progressFraction >= 0.97 { + effectiveInertiaDirection = nil + } + let badgeSize = self.badge.update( transition: transition, component: AnyComponent(BadgeComponent( - theme: environment.theme, title: "\(self.amount)") - ), + theme: environment.theme, + title: "\(self.amount)", + inertiaDirection: effectiveInertiaDirection + )), environment: {}, containerSize: CGSize(width: 200.0, height: 200.0) ) var badgeFrame = CGRect(origin: CGPoint(x: sliderForegroundFrame.minX + sliderForegroundFrame.width - floorToScreenPixels(sliderMinWidth * 0.5), y: sliderForegroundFrame.minY - 8.0), size: badgeSize) if let badgeView = self.badge.view as? BadgeComponent.View { if badgeView.superview == nil { - self.scrollContentView.addSubview(badgeView) + self.scrollContentView.insertSubview(badgeView, belowSubview: self.badgeStars) } let badgeSideInset = sideInset + 15.0 @@ -943,6 +1022,10 @@ private final class ChatSendStarsScreenComponent: Component { badgeView.adjustTail(size: badgeSize, overflowWidth: -badgeOverflowWidth) } + + let starsRect = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: sliderForegroundFrame.midY)) + self.badgeStars.frame = starsRect + self.badgeStars.update(size: starsRect.size, emitterPosition: CGPoint(x: badgeFrame.minX, y: badgeFrame.midY - 64.0)) } contentHeight += 123.0 @@ -1437,3 +1520,198 @@ private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: context.strokePath() }) } + +private final class BadgeStarsView: UIView { + private let staticEmitterLayer = CAEmitterLayer() + private let dynamicEmitterLayer = CAEmitterLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.staticEmitterLayer) + self.layer.addSublayer(self.dynamicEmitterLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + private func setupEmitter() { + let color = UIColor(rgb: 0xffbe27) + + self.staticEmitterLayer.emitterShape = .circle + self.staticEmitterLayer.emitterSize = CGSize(width: 10.0, height: 5.0) + self.staticEmitterLayer.emitterMode = .outline + self.layer.addSublayer(self.staticEmitterLayer) + + self.dynamicEmitterLayer.birthRate = 0.0 + self.dynamicEmitterLayer.emitterShape = .circle + self.dynamicEmitterLayer.emitterSize = CGSize(width: 10.0, height: 55.0) + self.dynamicEmitterLayer.emitterMode = .surface + self.layer.addSublayer(self.dynamicEmitterLayer) + + let staticEmitter = CAEmitterCell() + staticEmitter.name = "emitter" + staticEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + staticEmitter.birthRate = 20.0 + staticEmitter.lifetime = 2.7 + staticEmitter.velocity = 30.0 + staticEmitter.velocityRange = 3 + staticEmitter.scale = 0.15 + staticEmitter.scaleRange = 0.08 + staticEmitter.emissionRange = .pi * 2.0 + staticEmitter.setValue(3.0, forKey: "mass") + staticEmitter.setValue(2.0, forKey: "massRange") + + let dynamicEmitter = CAEmitterCell() + dynamicEmitter.name = "emitter" + dynamicEmitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + dynamicEmitter.birthRate = 0.0 + dynamicEmitter.lifetime = 2.7 + dynamicEmitter.velocity = 30.0 + dynamicEmitter.velocityRange = 3 + dynamicEmitter.scale = 0.15 + dynamicEmitter.scaleRange = 0.08 + dynamicEmitter.emissionRange = .pi / 3.0 + dynamicEmitter.setValue(3.0, forKey: "mass") + dynamicEmitter.setValue(2.0, forKey: "massRange") + + let staticColors: [Any] = [ + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.35).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let staticColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + staticColorBehavior.setValue(staticColors, forKey: "colors") + staticEmitter.setValue([staticColorBehavior], forKey: "emitterBehaviors") + + let dynamicColors: [Any] = [ + UIColor.white.withAlphaComponent(0.35).cgColor, + color.withAlphaComponent(0.85).cgColor, + color.cgColor, + color.cgColor, + color.withAlphaComponent(0.0).cgColor + ] + let dynamicColorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + dynamicColorBehavior.setValue(dynamicColors, forKey: "colors") + dynamicEmitter.setValue([dynamicColorBehavior], forKey: "emitterBehaviors") + + let attractor = CAEmitterCell.createEmitterBehavior(type: "simpleAttractor") + attractor.setValue("attractor", forKey: "name") + attractor.setValue(20, forKey: "falloff") + attractor.setValue(35, forKey: "radius") + self.staticEmitterLayer.setValue([attractor], forKey: "emitterBehaviors") + self.staticEmitterLayer.setValue(4.0, forKeyPath: "emitterBehaviors.attractor.stiffness") + self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") + + self.staticEmitterLayer.emitterCells = [staticEmitter] + self.dynamicEmitterLayer.emitterCells = [dynamicEmitter] + } + + func update(speed: Float, delta: Float? = nil) { + if speed > 0.0 { + if self.dynamicEmitterLayer.birthRate.isZero { + self.dynamicEmitterLayer.beginTime = CACurrentMediaTime() + } + + self.dynamicEmitterLayer.setValue(Float(20.0 + speed * 1.4), forKeyPath: "emitterCells.emitter.birthRate") + self.dynamicEmitterLayer.setValue(2.7 - min(1.1, 1.5 * speed / 120.0), forKeyPath: "emitterCells.emitter.lifetime") + self.dynamicEmitterLayer.setValue(30.0 + CGFloat(speed / 80.0), forKeyPath: "emitterCells.emitter.velocity") + + if let delta, speed > 15.0 { + self.dynamicEmitterLayer.setValue(delta > 0 ? .pi : 0, forKeyPath: "emitterCells.emitter.emissionLongitude") + self.dynamicEmitterLayer.setValue(.pi / 2.0, forKeyPath: "emitterCells.emitter.emissionRange") + } else { + self.dynamicEmitterLayer.setValue(0.0, forKeyPath: "emitterCells.emitter.emissionLongitude") + self.dynamicEmitterLayer.setValue(.pi * 2.0, forKeyPath: "emitterCells.emitter.emissionRange") + } + self.staticEmitterLayer.setValue(true, forKeyPath: "emitterBehaviors.attractor.enabled") + + self.dynamicEmitterLayer.birthRate = 1.0 + self.staticEmitterLayer.birthRate = 0.0 + } else { + self.dynamicEmitterLayer.birthRate = 0.0 + + if let staticEmitter = self.staticEmitterLayer.emitterCells?.first { + staticEmitter.beginTime = CACurrentMediaTime() + } + self.staticEmitterLayer.birthRate = 1.0 + self.staticEmitterLayer.setValue(false, forKeyPath: "emitterBehaviors.attractor.enabled") + } + } + + func update(size: CGSize, emitterPosition: CGPoint) { + if self.staticEmitterLayer.emitterCells == nil { + self.setupEmitter() + } + + self.staticEmitterLayer.frame = CGRect(origin: .zero, size: size) + self.staticEmitterLayer.emitterPosition = emitterPosition + + self.dynamicEmitterLayer.frame = CGRect(origin: .zero, size: size) + self.dynamicEmitterLayer.emitterPosition = emitterPosition + self.staticEmitterLayer.setValue(emitterPosition, forKeyPath: "emitterBehaviors.attractor.position") + } +} + +private final class SliderStarsView: UIView { + private let emitterLayer = CAEmitterLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.emitterLayer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + private func setupEmitter() { + self.emitterLayer.emitterShape = .rectangle + self.emitterLayer.emitterMode = .surface + self.layer.addSublayer(self.emitterLayer) + + let emitter = CAEmitterCell() + emitter.name = "emitter" + emitter.contents = UIImage(bundleImageName: "Premium/Stars/Particle")?.cgImage + emitter.birthRate = 20.0 + emitter.lifetime = 2.0 + emitter.velocity = 15.0 + emitter.velocityRange = 10 + emitter.scale = 0.15 + emitter.scaleRange = 0.08 + emitter.emissionRange = .pi / 4.0 + emitter.setValue(3.0, forKey: "mass") + emitter.setValue(2.0, forKey: "massRange") + self.emitterLayer.emitterCells = [emitter] + + let colors: [Any] = [ + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.0).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.38).cgColor, + UIColor.white.withAlphaComponent(0.0).cgColor + ] + let colorBehavior = CAEmitterCell.createEmitterBehavior(type: "colorOverLife") + colorBehavior.setValue(colors, forKey: "colors") + emitter.setValue([colorBehavior], forKey: "emitterBehaviors") + } + + func update(size: CGSize, value: CGFloat) { + if self.emitterLayer.emitterCells == nil { + self.setupEmitter() + } + + self.emitterLayer.setValue(20.0 + Float(value * 40.0), forKeyPath: "emitterCells.emitter.birthRate") + self.emitterLayer.setValue(15.0 + value * 75.0, forKeyPath: "emitterCells.emitter.velocity") + + self.emitterLayer.frame = CGRect(origin: .zero, size: size) + self.emitterLayer.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.emitterLayer.emitterSize = size + } +} diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 803ff11124..aa7488a964 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -438,6 +438,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { if tinted { self.updateTintColor() } + case .ton: + self.updateTon() } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) @@ -623,6 +625,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } + private func updateTon() { + self.contents = tonImage?.cgImage + } + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { guard let arguments = self.arguments else { return @@ -899,7 +905,17 @@ private let starImage: UIImage? = { context.clear(CGRect(origin: .zero, size: size)) if let image = UIImage(bundleImageName: "Premium/Stars/StarLarge"), let cgImage = image.cgImage { - context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 2.0, dy: 2.0), byTiling: false) + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) + } + })?.withRenderingMode(.alwaysTemplate) +}() + +private let tonImage: UIImage? = { + generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + if let image = generateTintedImage(image: UIImage(bundleImageName: "Ads/TonBig"), color: UIColor(rgb: 0x007aff)), let cgImage = image.cgImage { + context.draw(cgImage, in: CGRect(origin: .zero, size: size).insetBy(dx: 4.0, dy: 4.0), byTiling: false) } })?.withRenderingMode(.alwaysTemplate) }() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index f88507b953..ee7a06c1e0 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -692,7 +692,7 @@ public final class EntityKeyboardComponent: Component { deleteBackwards?() AudioServicesPlaySystemSound(1155) } - ).withHoldAction({ + ).withHoldAction({ _ in deleteBackwards?() AudioServicesPlaySystemSound(1155) }).minSize(CGSize(width: 38.0, height: 38.0))))) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index ad6000d2cc..8830b70ed6 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -24,6 +24,7 @@ public enum CodableDrawingEntity: Equatable { case vector(DrawingVectorEntity) case location(DrawingLocationEntity) case link(DrawingLinkEntity) + case weather(DrawingWeatherEntity) public init?(entity: DrawingEntity) { if let entity = entity as? DrawingStickerEntity { @@ -40,6 +41,8 @@ public enum CodableDrawingEntity: Equatable { self = .location(entity) } else if let entity = entity as? DrawingLinkEntity { self = .link(entity) + } else if let entity = entity as? DrawingWeatherEntity { + self = .weather(entity) } else { return nil } @@ -61,6 +64,8 @@ public enum CodableDrawingEntity: Equatable { return entity case let .link(entity): return entity + case let .weather(entity): + return entity } } @@ -109,6 +114,14 @@ public enum CodableDrawingEntity: Equatable { size = entitySize } } + case let .weather(entity): + position = entity.position + size = entity.renderImage?.size + rotation = entity.rotation + scale = entity.scale + if let size { + cornerRadius = 10.0 / (size.width * entity.scale) + } default: return nil } @@ -198,6 +211,7 @@ extension CodableDrawingEntity: Codable { case vector case location case link + case weather } public init(from decoder: Decoder) throws { @@ -218,6 +232,8 @@ extension CodableDrawingEntity: Codable { self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity)) case .link: self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity)) + case .weather: + self = .weather(try container.decode(DrawingWeatherEntity.self, forKey: .entity)) } } @@ -245,6 +261,9 @@ extension CodableDrawingEntity: Codable { case let .link(payload): try container.encode(EntityType.link, forKey: .type) try container.encode(payload, forKey: .entity) + case let .weather(payload): + try container.encode(EntityType.weather, forKey: .type) + try container.encode(payload, forKey: .entity) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift new file mode 100644 index 0000000000..67fb6eebc5 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift @@ -0,0 +1,182 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TextFormat +import Postbox +import TelegramCore + +public final class DrawingWeatherEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case style + case color + case hasCustomColor + case temperature + case icon + case referenceDrawingSize + case position + case width + case scale + case rotation + case renderImage + } + + public enum Style: Codable, Equatable { + case white + case black + case transparent + case custom + case blur + } + + public var uuid: UUID + public var isAnimated: Bool { + return false + } + + + public var style: Style + public var temperature: String + public var icon: TelegramMediaFile? + public var color: DrawingColor = DrawingColor(color: .white) { + didSet { + if self.color.toUIColor().argb == UIColor.white.argb { + self.style = .white + self.hasCustomColor = false + } else { + self.style = .custom + self.hasCustomColor = true + } + } + } + public var hasCustomColor = false + public var lineWidth: CGFloat = 0.0 + + public var referenceDrawingSize: CGSize + public var position: CGPoint + public var width: CGFloat + public var scale: CGFloat { + didSet { + self.scale = min(2.5, self.scale) + } + } + public var rotation: CGFloat + + public var center: CGPoint { + return self.position + } + + public var renderImage: UIImage? + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init(temperature: String, style: Style, icon: TelegramMediaFile?) { + self.uuid = UUID() + + self.temperature = temperature + self.style = style + self.icon = icon + + self.referenceDrawingSize = .zero + self.position = .zero + self.width = 100.0 + self.scale = 1.0 + self.rotation = 0.0 + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.uuid = try container.decode(UUID.self, forKey: .uuid) + self.temperature = try container.decode(String.self, forKey: .temperature) + self.style = try container.decode(Style.self, forKey: .style) + self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white) + self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false + + if let iconData = try container.decodeIfPresent(Data.self, forKey: .icon) { + self.icon = PostboxDecoder(buffer: MemoryBuffer(data: iconData)).decodeRootObject() as? TelegramMediaFile + } + + self.referenceDrawingSize = try container.decode(CGSize.self, forKey: .referenceDrawingSize) + self.position = try container.decode(CGPoint.self, forKey: .position) + self.width = try container.decode(CGFloat.self, forKey: .width) + self.scale = try container.decode(CGFloat.self, forKey: .scale) + self.rotation = try container.decode(CGFloat.self, forKey: .rotation) + if let renderImageData = try? container.decodeIfPresent(Data.self, forKey: .renderImage) { + self.renderImage = UIImage(data: renderImageData) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.uuid, forKey: .uuid) + try container.encode(self.temperature, forKey: .temperature) + try container.encode(self.style, forKey: .style) + try container.encode(self.color, forKey: .color) + try container.encode(self.hasCustomColor, forKey: .hasCustomColor) + + var encoder = PostboxEncoder() + if let icon = self.icon { + encoder = PostboxEncoder() + encoder.encodeRootObject(icon) + let iconData = encoder.makeData() + try container.encode(iconData, forKey: .icon) + } + + try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) + try container.encode(self.position, forKey: .position) + try container.encode(self.width, forKey: .width) + try container.encode(self.scale, forKey: .scale) + try container.encode(self.rotation, forKey: .rotation) + if let renderImage, let data = renderImage.pngData() { + try container.encode(data, forKey: .renderImage) + } + } + + public func duplicate(copy: Bool) -> DrawingEntity { + let newEntity = DrawingWeatherEntity(temperature: self.temperature, style: self.style, icon: self.icon) + if copy { + newEntity.uuid = self.uuid + } + newEntity.referenceDrawingSize = self.referenceDrawingSize + newEntity.position = self.position + newEntity.width = self.width + newEntity.scale = self.scale + newEntity.rotation = self.rotation + return newEntity + } + + public func isEqual(to other: DrawingEntity) -> Bool { + guard let other = other as? DrawingWeatherEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.temperature != other.temperature { + return false + } + if self.style != other.style { + return false + } + if self.referenceDrawingSize != other.referenceDrawingSize { + return false + } + if self.position != other.position { + return false + } + if self.width != other.width { + return false + } + if self.scale != other.scale { + return false + } + if self.rotation != other.rotation { + return false + } + return true + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index c6758ebf51..495bf3f24f 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -4155,6 +4155,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } if !self.didSetupStaticEmojiPack { + self.didSetupStaticEmojiPack = true self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) } @@ -4212,7 +4213,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate emojiFile = .single(nil) } - let _ = emojiFile.start(next: { [weak self] emojiFile in + let _ = (emojiFile + |> deliverOnMainQueue).start(next: { [weak self] emojiFile in guard let self else { return } @@ -4570,6 +4572,63 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.mediaEditor?.play() } + func addWeather() { + if !self.didSetupStaticEmojiPack { + self.didSetupStaticEmojiPack = true + self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) + } + + let emojiFile: Signal + let emoji = "☀️".strippedEmoji + + emojiFile = self.context.animatedEmojiStickers + |> take(1) + |> map { result -> TelegramMediaFile? in + if let file = result[emoji]?.first { + return file.file + } else { + return nil + } + +// if case let .result(_, items, _) = result, let match = items.first(where: { item in +// var displayText: String? +// for attribute in item.file.attributes { +// if case let .Sticker(alt, _, _) = attribute { +// displayText = alt +// break +// } +// } +// if let displayText, displayText.hasPrefix(emoji) { +// return true +// } else { +// return false +// } +// }) { +// return match.file +// } else { +// return nil +// } + } + + + let _ = (emojiFile + |> deliverOnMainQueue).start(next: { [weak self] emojiFile in + guard let self else { + return + } + let scale = 1.0 + self.interaction?.insertEntity( + DrawingWeatherEntity( + temperature: "35°C", + style: .white, + icon: emojiFile + ), + scale: scale, + position: nil + ) + }) + } + func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { return @@ -4824,6 +4883,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate controller?.dismiss(animated: true) } } + controller.addWeather = { [weak self, weak controller] in + if let self { + self.addWeather() + + self.stickerScreen = nil + controller?.dismiss(animated: true) + } + } controller.pushController = { [weak self] c in self?.controller?.push(c) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift index c0f5a74ee8..bd96828384 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StickerCutoutOutlineView.swift @@ -4,14 +4,6 @@ import Display import CoreImage import MediaEditor -func createEmitterBehavior(type: String) -> NSObject { - let selector = ["behaviorWith", "Type:"].joined(separator: "") - let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type - let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))! - let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) - return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type) -} - private var previousBeginTime: Int = 3 final class StickerCutoutOutlineView: UIView { @@ -81,7 +73,7 @@ final class StickerCutoutOutlineView: UIView { let lineEmitterCell = CAEmitterCell() lineEmitterCell.beginTime = CACurrentMediaTime() - let lineAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + let lineAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath") lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values") lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors") @@ -107,7 +99,7 @@ final class StickerCutoutOutlineView: UIView { let glowEmitterCell = CAEmitterCell() glowEmitterCell.beginTime = CACurrentMediaTime() - let glowAlphaBehavior = createEmitterBehavior(type: "valueOverLife") + let glowAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife") glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath") glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values") glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors") diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift index c03c065d7c..41b968e987 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedContainer.swift @@ -241,7 +241,10 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll if let snapshotView = self.snapshotView { var snapshotFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - snapshotView.bounds.size.width) / 2.0), y: 0.0), size: snapshotView.bounds.size) - + if self.item.controller.minimizedTopEdgeOffset == nil && isExpanded { + snapshotFrame = snapshotFrame.offsetBy(dx: 0.0, dy: -12.0) + } + var requiresBlur = false var blurFrame = snapshotFrame if snapshotView.frame.width * 1.1 < size.width { @@ -1018,6 +1021,14 @@ public class MinimizedContainerImpl: ASDisplayNode, MinimizedContainer, ASScroll transition.updateBounds(node: itemNode, bounds: CGRect(origin: .zero, size: layout.size)) } transition.updateTransform(node: itemNode, transform: CATransform3DIdentity) + + if let _ = itemNode.snapshotView { + if itemNode.item.controller.minimizedTopEdgeOffset == nil, let snapshotView = itemNode.snapshotView, snapshotView.frame.origin.y == -12.0 { + let snapshotFrame = snapshotView.frame.offsetBy(dx: 0.0, dy: 12.0) + transition.updateFrame(view: snapshotView, frame: snapshotFrame) + } + } + transition.updatePosition(node: itemNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0 + topInset + self.scrollView.contentOffset.y), completion: { _ in self.isApplyingTransition = false if self.currentTransition == currentTransition { diff --git a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift index 74e47dd5e4..dfbb7ab256 100644 --- a/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift +++ b/submodules/TelegramUI/Components/MinimizedContainer/Sources/MinimizedHeaderNode.swift @@ -25,11 +25,14 @@ final class MinimizedHeaderNode: ASDisplayNode { var theme: NavigationControllerTheme { didSet { self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor + self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06) + self.iconView.tintColor = self.theme.navigationBar.primaryTextColor } } let strings: PresentationStrings private let backgroundView = UIView() + private let progressView = UIView() private var iconView = UIImageView() private let titleLabel = ComponentView() private let closeButton = ComponentView() @@ -48,6 +51,12 @@ final class MinimizedHeaderNode: ASDisplayNode { self.icon = nil } + if self.controllers.count == 1, let progress = self.controllers.first?.minimizedProgress { + self.progress = progress + } else { + self.progress = nil + } + if newValue.count != self.controllers.count { self._controllers = newValue.map { WeakController($0) } @@ -93,6 +102,14 @@ final class MinimizedHeaderNode: ASDisplayNode { } } + var progress: Float? { + didSet { + if let (size, insets, isExpanded) = self.validLayout { + self.update(size: size, insets: insets, isExpanded: isExpanded, transition: .immediate) + } + } + } + var title: String? { didSet { if let (size, insets, isExpanded) = self.validLayout { @@ -111,20 +128,25 @@ final class MinimizedHeaderNode: ASDisplayNode { self.strings = strings self.backgroundView.clipsToBounds = true - self.backgroundView.backgroundColor = theme.navigationBar.opaqueBackgroundColor + self.backgroundView.backgroundColor = self.theme.navigationBar.opaqueBackgroundColor self.backgroundView.layer.cornerRadius = 10.0 if #available(iOS 11.0, *) { self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } + self.progressView.backgroundColor = self.theme.navigationBar.primaryTextColor.withAlphaComponent(0.06) + + self.iconView.contentMode = .scaleAspectFit self.iconView.clipsToBounds = true self.iconView.layer.cornerRadius = 2.5 + self.iconView.tintColor = self.theme.navigationBar.primaryTextColor super.init() self.clipsToBounds = true self.view.addSubview(self.backgroundView) + self.backgroundView.addSubview(self.progressView) self.backgroundView.addSubview(self.iconView) applySmoothRoundedCorners(self.backgroundView.layer) @@ -149,9 +171,9 @@ final class MinimizedHeaderNode: ASDisplayNode { func update(size: CGSize, insets: UIEdgeInsets, isExpanded: Bool, transition: ContainedViewLayoutTransition) { self.validLayout = (size, insets, isExpanded) - + let headerHeight: CGFloat = 44.0 - let titleSpacing: CGFloat = 4.0 + let titleSpacing: CGFloat = 6.0 var titleSideInset: CGFloat = 56.0 if !isExpanded { titleSideInset += insets.left @@ -177,7 +199,7 @@ final class MinimizedHeaderNode: ASDisplayNode { } let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0), y: floorToScreenPixels((headerHeight - iconSize.height) / 2.0)), size: iconSize) - transition.updateFrame(view: self.iconView, frame: iconFrame) + self.iconView.frame = iconFrame let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalWidth) / 2.0) + totalWidth - titleSize.width, y: floorToScreenPixels((headerHeight - titleSize.height) / 2.0)), size: titleSize) if let view = self.titleLabel.view { @@ -220,5 +242,10 @@ final class MinimizedHeaderNode: ASDisplayNode { } transition.updateFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: size.width, height: 243.0))) + + transition.updateAlpha(layer: self.progressView.layer, alpha: isExpanded && self.progress != nil ? 1.0 : 0.0) + if let progress = self.progress { + self.progressView.frame = CGRect(origin: .zero, size: CGSize(width: size.width * CGFloat(progress), height: 243.0)) + } } } diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/BUILD b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD new file mode 100644 index 0000000000..775f9ca233 --- /dev/null +++ b/submodules/TelegramUI/Components/NavigationStackComponent/BUILD @@ -0,0 +1,21 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "NavigationStackComponent", + module_name = "NavigationStackComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/Components/BundleIconComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift new file mode 100644 index 0000000000..0a0a12c4a4 --- /dev/null +++ b/submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift @@ -0,0 +1,297 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AppBundle +import BundleIconComponent + +private final class NavigationContainer: UIView, UIGestureRecognizerDelegate { + var requestUpdate: ((ComponentTransition) -> Void)? + var requestPop: (() -> Void)? + var transitionFraction: CGFloat = 0.0 + + private var panRecognizer: InteractiveTransitionGestureRecognizer? + + var isNavigationEnabled: Bool = false { + didSet { + self.panRecognizer?.isEnabled = self.isNavigationEnabled + } + } + + init() { + super.init(frame: .zero) + + let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in + guard let strongSelf = self else { + return [] + } + let _ = strongSelf + return [.right] + }) + panRecognizer.delegate = self + self.addGestureRecognizer(panRecognizer) + self.panRecognizer = panRecognizer + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer { + return false + } + if let _ = otherGestureRecognizer as? UIPanGestureRecognizer { + return true + } + return false + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.transitionFraction = 0.0 + case .changed: + let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if self.transitionFraction != transitionFraction { + self.transitionFraction = transitionFraction + self.requestUpdate?(.immediate) + } + case .ended, .cancelled: + let distanceFactor: CGFloat = recognizer.translation(in: self).x / self.bounds.width + let transitionFraction = max(0.0, min(1.0, distanceFactor)) + if transitionFraction > 0.2 { + self.transitionFraction = 0.0 + self.requestPop?() + } else { + self.transitionFraction = 0.0 + self.requestUpdate?(.spring(duration: 0.45)) + } + default: + break + } + } +} + +public final class NavigationStackComponent: Component { + public let items: [AnyComponentWithIdentity] + public let requestPop: () -> Void + + public init( + items: [AnyComponentWithIdentity], + requestPop: @escaping () -> Void + ) { + self.items = items + self.requestPop = requestPop + } + + public static func ==(lhs: NavigationStackComponent, rhs: NavigationStackComponent) -> Bool { + if lhs.items != rhs.items { + return false + } + return true + } + + private final class ItemView: UIView { + let contents = ComponentView() + let dimView = UIView() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.dimView.alpha = 0.0 + self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2) + self.dimView.isUserInteractionEnabled = false + self.addSubview(self.dimView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private struct ReadyItem { + var index: Int + var itemId: AnyHashable + var itemView: ItemView + var itemTransition: ComponentTransition + var itemSize: CGSize + + init(index: Int, itemId: AnyHashable, itemView: ItemView, itemTransition: ComponentTransition, itemSize: CGSize) { + self.index = index + self.itemId = itemId + self.itemView = itemView + self.itemTransition = itemTransition + self.itemSize = itemSize + } + } + + public final class View: UIView { + private var itemViews: [AnyHashable: ItemView] = [:] + private let navigationContainer = NavigationContainer() + + private var component: NavigationStackComponent? + private var state: EmptyComponentState? + + public override init(frame: CGRect) { + super.init(frame: CGRect()) + + self.addSubview(self.navigationContainer) + + self.navigationContainer.requestUpdate = { [weak self] transition in + guard let self else { + return + } + self.state?.updated(transition: transition) + } + + self.navigationContainer.requestPop = { [weak self] in + guard let self else { + return + } + self.component?.requestPop() + } + } + + required public init?(coder: NSCoder) { + preconditionFailure() + } + + func update(component: NavigationStackComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + let navigationTransitionFraction = self.navigationContainer.transitionFraction + self.navigationContainer.isNavigationEnabled = component.items.count > 1 + + var validItemIds: [AnyHashable] = [] + + var readyItems: [ReadyItem] = [] + for i in 0 ..< component.items.count { + let item = component.items[i] + let itemId = item.id + validItemIds.append(itemId) + + let itemView: ItemView + var itemTransition = transition + if let current = self.itemViews[itemId] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ItemView() + self.itemViews[itemId] = itemView + itemView.contents.parentState = state + } + + let itemSize = itemView.contents.update( + transition: itemTransition, + component: item.component, + environment: { environment[ChildEnvironment.self] }, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + + readyItems.append(ReadyItem( + index: i, + itemId: itemId, + itemView: itemView, + itemTransition: itemTransition, + itemSize: itemSize + )) + } + + let sortedItems = readyItems.sorted(by: { $0.index < $1.index }) + for readyItem in sortedItems { + let transitionFraction: CGFloat + let alphaTransitionFraction: CGFloat + if readyItem.index == readyItems.count - 1 { + transitionFraction = navigationTransitionFraction + alphaTransitionFraction = 1.0 + } else if readyItem.index == readyItems.count - 2 { + transitionFraction = navigationTransitionFraction - 1.0 + alphaTransitionFraction = navigationTransitionFraction + } else { + transitionFraction = 0.0 + alphaTransitionFraction = 0.0 + } + + let transitionOffset: CGFloat + if readyItem.index == readyItems.count - 1 { + transitionOffset = readyItem.itemSize.width * transitionFraction + } else { + transitionOffset = readyItem.itemSize.width / 3.0 * transitionFraction + } + + let itemFrame = CGRect(origin: CGPoint(x: transitionOffset, y: 0.0), size: readyItem.itemSize) + + let itemBounds = CGRect(origin: .zero, size: itemFrame.size) + if let itemComponentView = readyItem.itemView.contents.view { + var isAdded = false + if itemComponentView.superview == nil { + isAdded = true + + readyItem.itemView.insertSubview(itemComponentView, at: 0) + self.navigationContainer.addSubview(readyItem.itemView) + } + readyItem.itemTransition.setFrame(view: readyItem.itemView, frame: itemFrame) + readyItem.itemTransition.setFrame(view: itemComponentView, frame: itemBounds) + readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize)) + readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction) + + if readyItem.index > 0 && isAdded { + transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil) + } + } + } + + let lastHeight = sortedItems.last?.itemSize.height ?? 0.0 + let previousHeight: CGFloat + if sortedItems.count > 1 { + previousHeight = sortedItems[sortedItems.count - 2].itemSize.height + } else { + previousHeight = lastHeight + } + let contentHeight = lastHeight * (1.0 - navigationTransitionFraction) + previousHeight * navigationTransitionFraction + + var removedItemIds: [AnyHashable] = [] + for (id, _) in self.itemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + } + } + for id in removedItemIds { + guard let itemView = self.itemViews[id] else { + continue + } + if let itemComponeentView = itemView.contents.view { + var position = itemComponeentView.center + position.x += itemComponeentView.bounds.width + transition.setPosition(view: itemComponeentView, position: position, completion: { _ in + itemView.removeFromSuperview() + self.itemViews.removeValue(forKey: id) + }) + } else { + itemView.removeFromSuperview() + self.itemViews.removeValue(forKey: id) + } + } + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize) + + return contentSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift index eca6988d5e..31c385a7fc 100644 --- a/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift +++ b/submodules/TelegramUI/Components/PeerAllowedReactionsScreen/Sources/EmojiSelectionComponent.swift @@ -194,7 +194,7 @@ public final class EmojiSelectionComponent: Component { component.backspace?() AudioServicesPlaySystemSound(1155) } - ).withHoldAction({ [weak self] in + ).withHoldAction({ [weak self] _ in guard let self, let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index d00acf4c33..42d19efc37 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -146,6 +146,7 @@ swift_library( "//submodules/ConfettiEffect", "//submodules/ContactsPeerItem", "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift index 828a840b8e..4114c252b0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenActionItem.swift @@ -3,6 +3,7 @@ import Display import SwiftSignalKit import TelegramPresentationData import AvatarNode +import AccountContext enum PeerInfoScreenActionColor { case accent @@ -89,7 +90,7 @@ private final class PeerInfoScreenActionItemNode: PeerInfoScreenItemNode { self.iconDisposable.dispose() } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenActionItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift index 3dc8c14283..3461f6182b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenAddressItem.swift @@ -170,7 +170,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenAddressItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift index 883ceb5d28..381d450fa4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBirthdatePickerItem.swift @@ -52,7 +52,7 @@ private final class PeerInfoScreenBirthdatePickerItemNode: PeerInfoScreenItemNod self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenBirthdatePickerItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift index c0b3d9671a..9f18816812 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenBusinessHoursItem.swift @@ -12,6 +12,7 @@ import ComponentFlow import MultilineTextComponent import BundleIconComponent import PlainButtonComponent +import AccountContext func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String { var text = "" @@ -279,7 +280,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenBusinessHoursItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift index 40adb5bbea..1f20c7b4b9 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCallListItem.swift @@ -57,7 +57,7 @@ private final class PeerInfoScreenCallListItemNode: PeerInfoScreenItemNode { self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenCallListItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift index 59374afed5..ec69eaef71 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenCommentItem.swift @@ -3,6 +3,7 @@ import Display import TelegramPresentationData import TextFormat import Markdown +import AccountContext final class PeerInfoScreenCommentItem: PeerInfoScreenItem { enum LinkAction { @@ -63,7 +64,7 @@ private final class PeerInfoScreenCommentItemNode: PeerInfoScreenItemNode { self.view.addGestureRecognizer(recognizer) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenCommentItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift index 0144179eb7..1d6c028f04 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenContactInfoItem.swift @@ -237,7 +237,7 @@ private final class PeerInfoScreenContactInfoItemNode: PeerInfoScreenItemNode { return nil } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenContactInfoItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift index d05a1a25fc..87436a17f4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureEncryptionKeyItem.swift @@ -3,6 +3,7 @@ import Display import TelegramPresentationData import EncryptionKeyVisualization import TelegramCore +import AccountContext final class PeerInfoScreenDisclosureEncryptionKeyItem: PeerInfoScreenItem { let id: AnyHashable @@ -71,7 +72,7 @@ private final class PeerInfoScreenDisclosureEncryptionKeyItemNode: PeerInfoScree self.addSubnode(self.maskNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenDisclosureEncryptionKeyItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift index d9d6ad497c..c8e725e618 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenDisclosureItem.swift @@ -2,6 +2,8 @@ import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData +import TextNodeWithEntities +import AccountContext final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { enum Label { @@ -12,6 +14,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { case none case text(String) + case attributedText(NSAttributedString) case coloredText(String, LabelColor) case badge(String, UIColor) case semitransparentBadge(String, UIColor) @@ -22,6 +25,8 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { switch self { case .none, .image: return "" + case let .attributedText(text): + return text.string case let .text(text), let .coloredText(text, _), let .badge(text, _), let .semitransparentBadge(text, _), let .titleBadge(text, _): return text } @@ -29,7 +34,7 @@ final class PeerInfoScreenDisclosureItem: PeerInfoScreenItem { var badgeColor: UIColor? { switch self { - case .none, .text, .coloredText, .image: + case .none, .text, .coloredText, .image, .attributedText: return nil case let .badge(_, color), let .semitransparentBadge(_, color), let .titleBadge(_, color): return color @@ -69,7 +74,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { private let maskNode: ASImageNode private let iconNode: ASImageNode private let labelBadgeNode: ASImageNode - private let labelNode: ImmediateTextNode + private let labelNode: ImmediateTextNodeWithEntities private var additionalLabelNode: ImmediateTextNode? private var additionalLabelBadgeNode: ASImageNode? private let textNode: ImmediateTextNode @@ -97,7 +102,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.labelBadgeNode.displaysAsynchronously = false self.labelBadgeNode.isLayerBacked = true - self.labelNode = ImmediateTextNode() + self.labelNode = ImmediateTextNodeWithEntities() self.labelNode.displaysAsynchronously = false self.labelNode.isUserInteractionEnabled = false @@ -135,7 +140,7 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { self.iconDisposable.dispose() } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenDisclosureItem else { return 10.0 } @@ -177,8 +182,20 @@ private final class PeerInfoScreenDisclosureItemNode: PeerInfoScreenItemNode { labelColorValue = presentationData.theme.list.itemSecondaryTextColor labelFont = titleFont } - self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue) + self.labelNode.arguments = TextNodeWithEntities.Arguments( + context: context, + cache: context.animationCache, + renderer: context.animationRenderer, + placeholderColor: .clear, + attemptSynchronous: true + ) + + if case let .attributedText(text) = item.label { + self.labelNode.attributedText = text + } else { + self.labelNode.attributedText = NSAttributedString(string: item.label.text, font: labelFont, textColor: labelColorValue) + } self.textNode.maximumNumberOfLines = 1 self.textNode.attributedText = NSAttributedString(string: item.text, font: titleFont, textColor: textColorValue) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift index 05f39366a2..eb69f67ad2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenHeaderItem.swift @@ -1,6 +1,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData +import AccountContext final class PeerInfoScreenHeaderItem: PeerInfoScreenItem { let id: AnyHashable @@ -44,7 +45,7 @@ private final class PeerInfoScreenHeaderItemNode: PeerInfoScreenItemNode { self.addSubnode(self.activateArea) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenHeaderItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift index 23707c5a9e..71cb89c1e1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenInfoItem.swift @@ -55,7 +55,7 @@ private final class PeerInfoScreenInfoItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenInfoItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift index 9f5865fc1e..71058bb91e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenLabeledValueItem.swift @@ -121,6 +121,8 @@ private func generateExpandBackground(size: CGSize, color: UIColor) -> UIImage? } private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { + private weak var context: AccountContext? + private let containerNode: ContextControllerSourceNode private let contextSourceNode: ContextExtractedContentContainingNode @@ -383,8 +385,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { 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) + if let validLayout = self.validLayout, let context = self.context { + let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) } } }) @@ -412,8 +414,8 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { 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) + if let validLayout = self.validLayout, let context = self.context { + let _ = self.update(context: context, width: validLayout.width, safeInsets: validLayout.safeInsets, presentationData: validLayout.presentationData, item: validLayout.item, topItem: validLayout.topItem, bottomItem: validLayout.bottomItem, hasCorners: validLayout.hasCorners, transition: .immediate) } } }) @@ -430,11 +432,12 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenLabeledValueItem else { return 10.0 } + self.context = context self.validLayout = (width, safeInsets, presentationData, item, topItem, bottomItem, hasCorners) self.item = item diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift index 436a667ed9..33af7c50da 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift @@ -118,7 +118,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenMemberItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift index f269e27aec..abc95fbf4a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenPersonalChannelItem.swift @@ -416,7 +416,7 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenPersonalChannelItem else { return 50.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift index d62ca44354..cc01acfe54 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenSwitchItem.swift @@ -2,6 +2,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import AppBundle +import AccountContext final class PeerInfoScreenSwitchItem: PeerInfoScreenItem { let id: AnyHashable @@ -89,7 +90,7 @@ private final class PeerInfoScreenSwitchItemNode: PeerInfoScreenItemNode { } } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenSwitchItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index ab53c67806..992dc9582f 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -351,6 +351,7 @@ final class PeerInfoScreenData { let starsState: StarsContext.State? let starsRevenueStatsState: StarsRevenueStats? let starsRevenueStatsContext: StarsRevenueStatsContext? + let revenueStatsState: RevenueStats? let _isContact: Bool var forceIsContact: Bool = false @@ -393,7 +394,8 @@ final class PeerInfoScreenData { personalChannel: PeerInfoPersonalChannelData?, starsState: StarsContext.State?, starsRevenueStatsState: StarsRevenueStats?, - starsRevenueStatsContext: StarsRevenueStatsContext? + starsRevenueStatsContext: StarsRevenueStatsContext?, + revenueStatsState: RevenueStats? ) { self.peer = peer self.chatPeer = chatPeer @@ -425,6 +427,7 @@ final class PeerInfoScreenData { self.starsState = starsState self.starsRevenueStatsState = starsRevenueStatsState self.starsRevenueStatsContext = starsRevenueStatsContext + self.revenueStatsState = revenueStatsState } } @@ -920,7 +923,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, personalChannel: personalChannel, starsState: starsState, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil ) } } @@ -962,7 +966,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen personalChannel: nil, starsState: nil, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1304,7 +1309,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen personalChannel: personalChannel, starsState: nil, starsRevenueStatsState: starsRevenueContextAndState.1, - starsRevenueStatsContext: starsRevenueContextAndState.0 + starsRevenueStatsContext: starsRevenueContextAndState.0, + revenueStatsState: nil ) } case .channel: @@ -1380,6 +1386,36 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen let isPremiumRequiredForStoryPosting: Signal = isPremiumRequiredForStoryPosting(context: context) + let starsRevenueContextAndState = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId) + ) + |> distinctUntilChanged + |> mapToSignal { canViewStarsRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in + guard canViewStarsRevenue else { + return .single((nil, nil)) + } + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) + return starsRevenueStatsContext.state + |> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in + return (starsRevenueStatsContext, state.stats) + } + } + + let revenueContextAndState = context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.CanViewRevenue(id: peerId) + ) + |> distinctUntilChanged + |> mapToSignal { canViewRevenue -> Signal<(RevenueStatsContext?, RevenueStats?), NoError> in + guard canViewRevenue else { + return .single((nil, nil)) + } + let revenueStatsContext = RevenueStatsContext(account: context.account, peerId: peerId) + return revenueStatsContext.state + |> map { state -> (RevenueStatsContext?, RevenueStats?) in + return (revenueStatsContext, state.stats) + } + } + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: false, chatLocationContextHolder: chatLocationContextHolder), @@ -1395,9 +1431,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, - isPremiumRequiredForStoryPosting + isPremiumRequiredForStoryPosting, + starsRevenueContextAndState, + revenueContextAndState ) - |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, status, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, accountIsPremium, recommendedChannels, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState, revenueContextAndState -> PeerInfoScreenData in var availablePanes = availablePanes if let hasStories { if hasStories { @@ -1447,7 +1485,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen requestsStatePromise.set(requestsContext.state |> map(Optional.init)) } } - + return PeerInfoScreenData( peer: peerView.peers[peerId], chatPeer: peerView.peers[peerId], @@ -1477,8 +1515,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, starsState: nil, - starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsState: starsRevenueContextAndState.1, + starsRevenueStatsContext: starsRevenueContextAndState.0, + revenueStatsState: revenueContextAndState.1 ) } case let .group(groupId): @@ -1775,7 +1814,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen personalChannel: nil, starsState: nil, starsRevenueStatsState: nil, - starsRevenueStatsContext: nil + starsRevenueStatsContext: nil, + revenueStatsState: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 975d7e23b3..73b73692d6 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -126,7 +126,7 @@ protocol PeerInfoScreenItem: AnyObject { class PeerInfoScreenItemNode: ASDisplayNode, AccessibilityFocusableNode { var bringToFrontForHighlight: (() -> Void)? - func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { preconditionFailure() } @@ -165,7 +165,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { self.addSubnode(self.bottomSeparatorNode) } - func update(width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { + func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, hasCorners: Bool, presentationData: PresentationData, items: [PeerInfoScreenItem], transition: ContainedViewLayoutTransition) -> CGFloat { self.backgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.topSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor @@ -217,7 +217,7 @@ private final class PeerInfoScreenItemSectionContainerNode: ASDisplayNode { bottomItem = items[i + 1] } - let itemHeight = itemNode.update(width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition) + let itemHeight = itemNode.update(context: context, width: width, safeInsets: safeInsets, presentationData: presentationData, item: item, topItem: topItem, bottomItem: bottomItem, hasCorners: hasCorners, transition: itemTransition) let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)) itemTransition.updateFrame(node: itemNode, frame: itemFrame) if wasAdded { @@ -561,7 +561,7 @@ private final class PeerInfoInteraction { let editingToggleMessageSignatures: (Bool) -> Void let openParticipantsSection: (PeerInfoParticipantsSection) -> Void let openRecentActions: () -> Void - let openStats: (Bool) -> Void + let openStats: (ChannelStatsSection) -> Void let editingOpenPreHistorySetup: () -> Void let editingOpenAutoremoveMesages: () -> Void let openPermissions: () -> Void @@ -629,7 +629,7 @@ private final class PeerInfoInteraction { editingToggleMessageSignatures: @escaping (Bool) -> Void, openParticipantsSection: @escaping (PeerInfoParticipantsSection) -> Void, openRecentActions: @escaping () -> Void, - openStats: @escaping (Bool) -> Void, + openStats: @escaping (ChannelStatsSection) -> Void, editingOpenPreHistorySetup: @escaping () -> Void, editingOpenAutoremoveMesages: @escaping () -> Void, openPermissions: @escaping () -> Void, @@ -1444,6 +1444,31 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) items[.peerInfo]!.append(PeerInfoScreenCommentItem(id: 8, text: presentationData.strings.Bot_AddToChatInfo)) } + + if let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + let starsBalance = data.starsRevenueStatsState?.balances.availableBalance ?? 0 + let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 + + if overallStarsBalance > 0 { + var string = "" + if overallStarsBalance > 0 { + string.append("*\(starsBalance)") + } + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: 9, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { + interaction.editingOpenStars() + })) + } + + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: 10, label: .none, text: presentationData.strings.Bot_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { + interaction.openEditing() + })) + } } } } else if let channel = data.peer as? TelegramChannel { @@ -1455,7 +1480,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese let ItemAdmins = 6 let ItemMembers = 7 let ItemMemberRequests = 8 - let ItemEdit = 9 + let ItemBalance = 9 + let ItemEdit = 10 if let _ = data.threadData { let mainUsername: String @@ -1609,6 +1635,40 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese })) } + if cachedData.flags.contains(.canViewRevenue) || cachedData.flags.contains(.canViewStarsRevenue) { + let revenueBalance = data.revenueStatsState?.balances.availableBalance ?? 0 + let starsBalance = data.starsRevenueStatsState?.balances.availableBalance ?? 0 + + let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0 + let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? 0 + + if overallRevenueBalance > 0 || overallStarsBalance > 0 { + var string = "" + if overallRevenueBalance > 0 { + string.append("#\(revenueBalance)") + } + if overallStarsBalance > 0 { + if !string.isEmpty { + string.append(" ") + } + string.append("*\(starsBalance)") + } + let attributedString = NSMutableAttributedString(string: string, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: presentationData.theme.list.itemSecondaryTextColor) + if let range = attributedString.string.range(of: "#") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + if let range = attributedString.string.range(of: "*") { + attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string)) + attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string)) + } + + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemBalance, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: { + interaction.openStats(.monetization) + })) + } + } + items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: { interaction.openEditing() })) @@ -1721,7 +1781,6 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemInfo = 3 let ItemDelete = 4 let ItemUsername = 5 - let ItemStars = 6 let ItemIntro = 7 let ItemCommands = 8 @@ -1732,13 +1791,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemUsername, label: .text("@\(user.addressName ?? "")"), text: presentationData.strings.PeerInfo_Bot_Username, icon: PresentationResourcesSettings.bot, action: { interaction.editingOpenPublicLinkSetup() })) - - if let starsRevenueStats = data.starsRevenueStatsState, starsRevenueStats.balances.overallRevenue > 0 { - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(starsRevenueStats.balances.currentBalance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: { - interaction.editingOpenStars() - })) - } - + items[.peerSettings]!.append(PeerInfoScreenActionItem(id: ItemIntro, text: presentationData.strings.PeerInfo_Bot_EditIntro, icon: UIImage(bundleImageName: "Peer Info/BotIntro"), action: { interaction.openPeerMention("botfather", .withBotStartPayload(ChatControllerInitialBotStart(payload: "\(user.addressName ?? "")-intro", behavior: .interactive))) })) @@ -1959,7 +2012,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL if let cachedData = data.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) { items[.peerAdditionalSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStats, label: .none, text: presentationData.strings.Channel_Info_Stats, icon: UIImage(bundleImageName: "Chat/Info/StatsIcon"), action: { - interaction.openStats(false) + interaction.openStats(.stats) })) } @@ -2649,8 +2702,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro openRecentActions: { [weak self] in self?.openRecentActions() }, - openStats: { [weak self] boosts in - self?.openStats(boosts: boosts) + openStats: { [weak self] section in + self?.openStats(section: section) }, editingOpenPreHistorySetup: { [weak self] in self?.editingOpenPreHistorySetup() @@ -6132,7 +6185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, action: { [weak self] _, f in f(.dismissWithoutContent) - self?.openStats() + self?.openStats(section: .stats) }))) } if cachedData.flags.contains(.translationHidden) { @@ -7820,7 +7873,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.controller?.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.peerId, scope: .archive)) } - private func openStats(boosts: Bool = false, boostStatus: ChannelBoostStatus? = nil) { + private func openStats(section: ChannelStatsSection, boostStatus: ChannelBoostStatus? = nil) { guard let controller = self.controller, let data = self.data, let peer = data.peer else { return } @@ -7830,7 +7883,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro if let channel = peer as? TelegramChannel, case .group = channel.info { statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id) } else { - statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: boosts ? .boosts : .stats, boostStatus: boostStatus) + statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, boostStatus: boostStatus) } controller.push(statsController) } @@ -9732,7 +9785,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let controller = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: peer.id, subject: .stories, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in if let self { - self.openStats(boosts: true, boostStatus: boostStatus) + self.openStats(section: .boosts, boostStatus: boostStatus) } }) navigationController.pushViewController(controller) @@ -11132,7 +11185,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro contentHeight -= 16.0 } } - let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight)) if additive { transition.updateFrameAdditive(node: sectionNode, frame: sectionFrame) @@ -11191,9 +11244,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self.editingSections[sectionId] = sectionNode self.scrollNode.addSubnode(sectionNode) } - + let sectionWidth = layout.size.width - insets.left - insets.right - let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) + let sectionHeight = sectionNode.update(context: self.context, width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight)) if wasAdded { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift index 0e5affd07c..9a639d6e13 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenMultilineInputtem.swift @@ -2,6 +2,7 @@ import AsyncDisplayKit import Display import TelegramPresentationData import ItemListUI +import AccountContext final class PeerInfoScreenMultilineInputItem: PeerInfoScreenItem { let id: AnyHashable @@ -53,7 +54,7 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode { self.addSubnode(self.bottomSeparatorNode) } - override func update(width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { + override func update(context: AccountContext, width: CGFloat, safeInsets: UIEdgeInsets, presentationData: PresentationData, item: PeerInfoScreenItem, topItem: PeerInfoScreenItem?, bottomItem: PeerInfoScreenItem?, hasCorners: Bool, transition: ContainedViewLayoutTransition) -> CGFloat { guard let item = item as? PeerInfoScreenMultilineInputItem else { return 10.0 } diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index 094d6f7c8f..91fbf41205 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -22,42 +22,42 @@ public final class GiftAvatarComponent: Component { let theme: PresentationTheme let peers: [EnginePeer] let photo: TelegramMediaWebFile? - let starsPeer: StarsContext.State.Transaction.Peer? let isVisible: Bool let hasIdleAnimations: Bool let hasScaleAnimation: Bool let avatarSize: CGFloat let color: UIColor? let offset: CGFloat? + var hasLargeParticles: Bool public init( context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], photo: TelegramMediaWebFile? = nil, - starsPeer: StarsContext.State.Transaction.Peer? = nil, isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, avatarSize: CGFloat = 100.0, color: UIColor? = nil, - offset: CGFloat? = nil + offset: CGFloat? = nil, + hasLargeParticles: Bool = false ) { self.context = context self.theme = theme self.peers = peers self.photo = photo - self.starsPeer = starsPeer self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations self.hasScaleAnimation = hasScaleAnimation self.avatarSize = avatarSize self.color = color self.offset = offset + self.hasLargeParticles = hasLargeParticles } public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset + return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset && lhs.hasLargeParticles == rhs.hasLargeParticles } public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -142,7 +142,7 @@ public final class GiftAvatarComponent: Component { private var didSetup = false private func setup() { - guard let scene = loadCompressedScene(name: "gift", version: sceneVersion), !self.didSetup else { + guard let scene = loadCompressedScene(name: "gift2", version: sceneVersion), !self.didSetup else { return } @@ -152,6 +152,21 @@ public final class GiftAvatarComponent: Component { self.sceneView.delegate = self if let color = self.component?.color { +// let names: [String] = [ +// "particles_left", +// "particles_right", +// "particles_left_bottom", +// "particles_right_bottom", +// "particles_center" +// ] +// +// for name in names { +// if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { +// particleSystem.particleColor = color +// particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0) +// } +// } + let names: [String] = [ "particles_left", "particles_right", @@ -160,10 +175,59 @@ public final class GiftAvatarComponent: Component { "particles_center" ] + let starNames: [String] = [ + "coins_left", + "coins_right" + ] + + let particleColor = color + for name in starNames { + if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { + particleSystem.particleIntensity = 1.0 + particleSystem.particleIntensityVariation = 0.05 + particleSystem.particleColor = particleColor + particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0) + node.isHidden = false + + if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] { + let animation = CAKeyframeAnimation() + if let existing = colorController.animation as? CAKeyframeAnimation { + animation.keyTimes = existing.keyTimes + animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? [] + } else { + animation.values = [ 0.0, 1.0, 1.0, 0.0 ] + } + let opacityController = SCNParticlePropertyController(animation: animation) + particleSystem.propertyControllers = [ + .size: sizeController, + .opacity: opacityController + ] + } + } + } + for name in names { if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first { - particleSystem.particleColor = color - particleSystem.particleColorVariation = SCNVector4Make(0, 0, 0, 0) + particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity) + particleSystem.particleIntensityVariation = 0.05 + particleSystem.particleColor = particleColor + particleSystem.particleColorVariation = SCNVector4Make(0.1, 0.0, 0.12, 0.0) + + + if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] { + let animation = CAKeyframeAnimation() + if let existing = colorController.animation as? CAKeyframeAnimation { + animation.keyTimes = existing.keyTimes + animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? [] + } else { + animation.values = [ 0.0, 1.0, 1.0, 0.0 ] + } + let opacityController = SCNParticlePropertyController(animation: animation) + particleSystem.propertyControllers = [ + .size: sizeController, + .opacity: opacityController + ] + } } } @@ -187,9 +251,7 @@ public final class GiftAvatarComponent: Component { } } - private func onReady() { - self.setupScaleAnimation() - + private func onReady() { self.playAppearanceAnimation(explode: true) self.previousInteractionTimestamp = CACurrentMediaTime() @@ -203,23 +265,7 @@ public final class GiftAvatarComponent: Component { }, queue: Queue.mainQueue()) self.timer?.start() } - - private func setupScaleAnimation() { - guard self.component?.hasScaleAnimation == true else { - return - } - - let animation = CABasicAnimation(keyPath: "transform.scale") - animation.duration = 2.0 - animation.fromValue = 1.0 - animation.toValue = 1.15 - animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - animation.autoreverses = true - animation.repeatCount = .infinity - - self.avatarNode.view.layer.add(animation, forKey: "scale") - } - + private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) { guard let scene = self.sceneView.scene else { return @@ -319,6 +365,10 @@ public final class GiftAvatarComponent: Component { self.hasIdleAnimations = component.hasIdleAnimations + if let _ = component.color { + self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor + } + if let photo = component.photo { let imageNode: TransformImageNode if let current = self.imageNode { @@ -339,86 +389,6 @@ public final class GiftAvatarComponent: Component { imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() self.avatarNode.isHidden = true - } else if let starsPeer = component.starsPeer { - let iconBackgroundView: UIImageView - let iconView: UIImageView - if let currentBackground = self.iconBackgroundView, let current = self.iconView { - iconBackgroundView = currentBackground - iconView = current - } else { - iconBackgroundView = UIImageView() - iconView = UIImageView() - - self.addSubview(iconBackgroundView) - self.addSubview(iconView) - - self.iconBackgroundView = iconBackgroundView - self.iconView = iconView - - let size = CGSize(width: component.avatarSize, height: component.avatarSize) - var iconInset: CGFloat = 9.0 - var iconOffset: CGFloat = 0.0 - - switch starsPeer { - case .appStore: - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x2a9ef1).cgColor, - UIColor(rgb: 0x72d5fd).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") - case .playMarket: - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x54cb68).cgColor, - UIColor(rgb: 0xa0de7e).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") - case .fragment: - iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") - iconOffset = 5.0 - case .ads: - iconBackgroundView.image = generateFilledCircleImage(diameter: size.width, color: UIColor(rgb: 0x1b1f24)) - iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") - iconOffset = 5.0 - case .premiumBot: - iconInset = 15.0 - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x6b93ff).cgColor, - UIColor(rgb: 0x8d77ff).cgColor, - UIColor(rgb: 0xb56eec).cgColor, - UIColor(rgb: 0xb56eec).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - case .peer, .unsupported: - iconInset = 15.0 - iconBackgroundView.image = generateGradientFilledCircleImage( - diameter: size.width, - colors: [ - UIColor(rgb: 0xb1b1b1).cgColor, - UIColor(rgb: 0xcdcdcd).cgColor - ], - direction: .mirroredDiagonal - ) - iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) - } - - let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - size.width) / 2.0), y: 113.0 - size.height / 2.0), size: size) - iconBackgroundView.frame = imageFrame - iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) - } } else if component.peers.count > 1 { let avatarSize = CGSize(width: 60.0, height: 60.0) diff --git a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift index 081731cbb3..5c526ae4b7 100644 --- a/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift +++ b/submodules/TelegramUI/Components/SliderComponent/Sources/SliderComponent.swift @@ -88,15 +88,17 @@ public final class SliderComponent: Component { if let isTrackingUpdated = component.isTrackingUpdated { internalIsTrackingUpdated = { [weak self] isTracking in if let self { - if isTracking { - self.sliderView?.bordered = true - } else { - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in - self?.sliderView?.bordered = false - }) + if !"".isEmpty { + if isTracking { + self.sliderView?.bordered = true + } else { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + self?.sliderView?.bordered = false + }) + } } - isTrackingUpdated(isTracking) } + isTrackingUpdated(isTracking) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift index 545bfc8409..679bb8df66 100644 --- a/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsAvatarComponent/Sources/StarsAvatarComponent.swift @@ -65,7 +65,7 @@ public final class StarsAvatarComponent: Component { private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 16.0)) + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 20.0)) super.init(frame: frame) diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD index 99fda0c389..b71d4ed3c0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD @@ -22,6 +22,8 @@ swift_library( "//submodules/AvatarNode", "//submodules/AccountContext", "//submodules/InvisibleInkDustNode", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 44aa3890ba..a4335b74e8 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -11,6 +11,8 @@ import PhotoResources import AvatarNode import AccountContext import InvisibleInkDustNode +import AnimatedStickerNode +import TelegramAnimatedStickerNode final class StarsParticlesView: UIView { private struct Particle { @@ -251,6 +253,7 @@ public final class StarsImageComponent: Component { case media([AnyMediaReference]) case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) + case gift(Int64) public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { switch lhs { @@ -284,6 +287,12 @@ public final class StarsImageComponent: Component { } else { return false } + case let .gift(lhsCount): + if case let .gift = rhs(rhsCount) { + return true + } else { + return false + } } } } @@ -347,6 +356,8 @@ public final class StarsImageComponent: Component { private var dustNode: MediaDustNode? private var button: UIControl? + private var animationNode: AnimatedStickerNode? + private var lockView: UIImageView? private var countView = ComponentView() @@ -776,6 +787,31 @@ public final class StarsImageComponent: Component { iconBackgroundView.frame = imageFrame iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) } + case let .gift(count): + let animationNode: AnimatedStickerNode + if let current = self.animationNode { + animationNode = current + } else { + let stickerName: String + if count <= 1000 { + stickerName = "Gift3" + } else if count < 2500 { + stickerName = "Gift6" + } else { + stickerName = "Gift12" + } + animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.autoplay = true + animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: stickerName), width: 384, height: 384, playbackMode: .still(.end), mode: .direct(cachePathPrefix: nil)) + animationNode.visibility = true + containerNode.view.addSubview(animationNode.view) + self.animationNode = animationNode + + animationNode.playOnce() + } + let animationFrame = imageFrame.insetBy(dx: -imageFrame.width * 0.19, dy: -imageFrame.height * 0.19).offsetBy(dx: 0.0, dy: -14.0) + animationNode.frame = animationFrame + animationNode.updateLayout(size: animationFrame.size) } if let _ = component.action { diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index 30da96ee36..843c567bcc 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -27,9 +27,32 @@ import BundleIconComponent import ConfettiEffect private struct StarsProduct: Equatable { - let option: StarsTopUpOption + enum Option: Equatable { + case topUp(StarsTopUpOption) + case gift(StarsGiftOption) + } + + let option: Option let storeProduct: InAppPurchaseManager.Product + var count: Int64 { + switch self.option { + case let .topUp(option): + return option.count + case let .gift(option): + return option.count + } + } + + var isExtended: Bool { + switch self.option { + case let .topUp(option): + return option.isExtended + case let .gift(option): + return option.isExtended + } + } + var id: String { return self.storeProduct.id } @@ -54,13 +77,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let externalState: ExternalState let containerSize: CGSize let balance: Int64? - let options: [StarsTopUpOption] - let peerId: EnginePeer.Id? - let requiredStars: Int64? + let options: [Any] + let purpose: StarsPurchasePurpose let selectedProductId: String? let forceDark: Bool let products: [StarsProduct]? let expanded: Bool + let peers: [EnginePeer.Id: EnginePeer] let stateUpdated: (ComponentTransition) -> Void let buy: (StarsProduct) -> Void @@ -69,13 +92,13 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { externalState: ExternalState, containerSize: CGSize, balance: Int64?, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, + options: [Any], + purpose: StarsPurchasePurpose, selectedProductId: String?, forceDark: Bool, products: [StarsProduct]?, expanded: Bool, + peers: [EnginePeer.Id: EnginePeer], stateUpdated: @escaping (ComponentTransition) -> Void, buy: @escaping (StarsProduct) -> Void ) { @@ -84,12 +107,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { self.containerSize = containerSize self.balance = balance self.options = options - self.peerId = peerId - self.requiredStars = requiredStars + self.purpose = purpose self.selectedProductId = selectedProductId self.forceDark = forceDark self.products = products self.expanded = expanded + self.peers = peers self.stateUpdated = stateUpdated self.buy = buy } @@ -101,13 +124,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.containerSize != rhs.containerSize { return false } - if lhs.options != rhs.options { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.requiredStars != rhs.requiredStars { + if lhs.purpose != rhs.purpose { return false } if lhs.selectedProductId != rhs.selectedProductId { @@ -122,6 +139,9 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.expanded != rhs.expanded { return false } + if lhs.peers != rhs.peers { + return false + } return true } @@ -129,31 +149,18 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { private let context: AccountContext var products: [StarsProduct]? - var peer: EnginePeer? private var disposable: Disposable? - + + var cachedChevronImage: (UIImage, PresentationTheme)? + init( context: AccountContext, - peerId: EnginePeer.Id? + purpose: StarsPurchasePurpose ) { self.context = context super.init() - - if let peerId { - self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in - if let self, let peer { - self.peer = peer - self.updated(transition: .immediate) - } - }) - } - - let _ = updatePremiumPromoConfigurationOnce(account: context.account).start() } deinit { @@ -162,63 +169,32 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, peerId: self.peerId) + return State(context: self.context, purpose: self.purpose) } static var body: Body { -// let overscroll = Child(Rectangle.self) -// let fade = Child(RoundedRectangle.self) let text = Child(BalancedTextComponent.self) let list = Child(VStack.self) let termsText = Child(BalancedTextComponent.self) return { context in let sideInset: CGFloat = 16.0 - + + let component = context.component let scrollEnvironment = context.environment[ScrollChildEnvironment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state - state.products = context.component.products + + state.products = component.products let theme = environment.theme let strings = environment.strings - let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let availableWidth = context.availableSize.width let sideInsets = sideInset * 2.0 + environment.safeInsets.left + environment.safeInsets.right var size = CGSize(width: context.availableSize.width, height: 0.0) - -// var topBackgroundColor = theme.list.plainBackgroundColor -// let bottomBackgroundColor = theme.list.blocksBackgroundColor -// if theme.overallDarkAppearance { -// topBackgroundColor = bottomBackgroundColor -// } -// -// let overscroll = overscroll.update( -// component: Rectangle(color: topBackgroundColor), -// availableSize: CGSize(width: context.availableSize.width, height: 1000), -// transition: context.transition -// ) -// context.add(overscroll -// .position(CGPoint(x: overscroll.size.width / 2.0, y: -overscroll.size.height / 2.0)) -// ) -// -// let fade = fade.update( -// component: RoundedRectangle( -// colors: [ -// topBackgroundColor, -// bottomBackgroundColor -// ], -// cornerRadius: 0.0, -// gradientDirection: .vertical -// ), -// availableSize: CGSize(width: availableWidth, height: 300), -// transition: context.transition -// ) -// context.add(fade -// .position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0)) -// ) - + size.height += 183.0 + 10.0 + environment.navigationHeight - 56.0 let textColor = theme.list.itemPrimaryTextColor @@ -228,22 +204,36 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let boldTextFont = Font.semibold(15.0) let textString: String - if let _ = context.component.requiredStars { - textString = state.peer == nil ? strings.Stars_Purchase_StarsNeededUnlockInfo : strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string - } else { + switch context.component.purpose { + case .generic: textString = strings.Stars_Purchase_GetStarsInfo + case .gift: + textString = strings.Stars_Purchase_GiftInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case .transfer: + textString = strings.Stars_Purchase_StarsNeededInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case let .subscription(_, _, renew): + textString = renew ? strings.Stars_Purchase_SubscriptionRenewInfo(component.peers.first?.value.compactDisplayTitle ?? "").string : strings.Stars_Purchase_SubscriptionInfo(component.peers.first?.value.compactDisplayTitle ?? "").string + case .unlockMedia: + textString = strings.Stars_Purchase_StarsNeededUnlockInfo } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: accentColor)!, theme) + } + + let titleAttributedString = parseMarkdownIntoAttributedString(textString, attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + + if let range = titleAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + titleAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: titleAttributedString.string)) + } + let text = text.update( component: BalancedTextComponent( - text: .markdown( - text: textString, - attributes: markdownAttributes - ), + text: .plain(titleAttributedString), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, @@ -271,16 +261,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { size.height += 21.0 context.component.externalState.descriptionHeight = text.size.height - - let initialValues: [Int64] = [ - 15, - 75, - 250, - 500, - 1000, - 2500 - ] - + let stars: [Int64: Int] = [ 15: 1, 75: 2, @@ -312,21 +293,21 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if let products = state.products, let balance = context.component.balance { var minimumCount: Int64? - if let requiredStars = context.component.requiredStars { + if let requiredStars = context.component.purpose.requiredStars { minimumCount = requiredStars - balance } for product in products { - if let minimumCount, minimumCount > product.option.count && !(items.isEmpty && product.id == products.last?.id) { + if let minimumCount, minimumCount > product.count && !(items.isEmpty && product.id == products.last?.id) { continue } if let _ = minimumCount, items.isEmpty { - } else if !context.component.expanded && !initialValues.contains(product.option.count) { + } else if !context.component.expanded && product.isExtended { continue } - let title = strings.Stars_Purchase_Stars(Int32(product.option.count)) + let title = strings.Stars_Purchase_Stars(Int32(product.count)) let price = product.price let titleComponent = AnyComponent(MultilineTextComponent( @@ -360,7 +341,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { title: titleComponent, contentInsets: UIEdgeInsets(top: 12.0, left: -6.0, bottom: 12.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsIconComponent( - count: stars[product.option.count] ?? 1 + count: stars[product.count] ?? 1 ))), true), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -445,7 +426,6 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { }) let textSideInset: CGFloat = 16.0 - let component = context.component let termsText = termsText.update( component: BalancedTextComponent( text: .markdown(text: strings.Stars_Purchase_Info, attributes: termsMarkdownAttributes), @@ -490,9 +470,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let context: AccountContext let starsContext: StarsContext - let options: [StarsTopUpOption] - let peerId: EnginePeer.Id? - let requiredStars: Int64? + let options: [Any] + let purpose: StarsPurchasePurpose let forceDark: Bool let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void @@ -501,9 +480,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { init( context: AccountContext, starsContext: StarsContext, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, + options: [Any], + purpose: StarsPurchasePurpose, forceDark: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, @@ -512,8 +490,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.context = context self.starsContext = starsContext self.options = options - self.peerId = peerId - self.requiredStars = requiredStars + self.purpose = purpose self.forceDark = forceDark self.updateInProgress = updateInProgress self.present = present @@ -527,13 +504,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { if lhs.starsContext !== rhs.starsContext { return false } - if lhs.options != rhs.options { - return false - } - if lhs.peerId != rhs.peerId { - return false - } - if lhs.requiredStars != rhs.requiredStars { + if lhs.purpose != rhs.purpose { return false } if lhs.forceDark != rhs.forceDark { @@ -544,6 +515,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext + private let purpose: StarsPurchasePurpose + private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void private let completion: (Int64) -> Void @@ -554,11 +527,11 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { var hasIdleAnimations = true var progressProduct: StarsProduct? - - private(set) var promoConfiguration: PremiumPromoConfiguration? - + private(set) var products: [StarsProduct]? private(set) var starsState: StarsContext.State? + + var peers: [EnginePeer.Id: EnginePeer] = [:] let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer @@ -569,12 +542,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { init( context: AccountContext, starsContext: StarsContext, - initialOptions: [StarsTopUpOption], + purpose: StarsPurchasePurpose, + initialOptions: [Any], updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, completion: @escaping (Int64) -> Void ) { self.context = context + self.purpose = purpose self.updateInProgress = updateInProgress self.present = present self.completion = completion @@ -590,32 +565,65 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } else { availableProducts = .single([]) } - - let options: Signal<[StarsTopUpOption], NoError> - if !initialOptions.isEmpty { - options = .single(initialOptions) - } else { - options = .single([]) |> then(context.engine.payments.starsTopUpOptions()) + + let products: Signal<[StarsProduct], NoError> + switch purpose { + case .gift: + let options: Signal<[StarsGiftOption], NoError> + if !initialOptions.isEmpty, let initialGiftOptions = initialOptions as? [StarsGiftOption] { + options = .single(initialGiftOptions) + } else { + options = .single([]) |> then(context.engine.payments.starsGiftOptions(peerId: nil)) + } + products = combineLatest(availableProducts, options) + |> map { availableProducts, options in + var products: [StarsProduct] = [] + for option in options { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { + products.append(StarsProduct(option: .gift(option), storeProduct: product)) + } + } + return products + } + default: + let options: Signal<[StarsTopUpOption], NoError> + if !initialOptions.isEmpty, let initialTopUpOptions = initialOptions as? [StarsTopUpOption] { + options = .single(initialTopUpOptions) + } else { + options = .single([]) |> then(context.engine.payments.starsTopUpOptions()) + } + products = combineLatest(availableProducts, options) + |> map { availableProducts, options in + var products: [StarsProduct] = [] + for option in options { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { + products.append(StarsProduct(option: .topUp(option), storeProduct: product)) + } + } + return products + } } - + + let peerIds = purpose.peerIds self.disposable = combineLatest( queue: Queue.mainQueue(), - availableProducts, - options, - starsContext.state - ).start(next: { [weak self] availableProducts, options, starsState in + products, + starsContext.state, + context.engine.data.get(EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))) + ).start(next: { [weak self] products, starsState, result in guard let self else { return } - var products: [StarsProduct] = [] - for option in options { - if let product = availableProducts.first(where: { $0.id == option.storeProductId }) { - products.append(StarsProduct(option: option, storeProduct: product)) + self.products = products.sorted(by: { $0.count < $1.count }) + self.starsState = starsState + + var peers: [EnginePeer.Id: EnginePeer] = [:] + for peerId in peerIds { + if let maybePeer = result[peerId], let peer = maybePeer { + peers[peerId] = peer } } - - self.products = products.sorted(by: { $0.option.count < $1.option.count }) - self.starsState = starsState + self.peers = peers self.updated(transition: .immediate) }) @@ -636,7 +644,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.updated(transition: .easeInOut(duration: 0.2)) let (currency, amount) = product.storeProduct.priceCurrencyAndAmount - let purpose: AppStoreTransactionPurpose = .stars(count: product.option.count, currency: currency, amount: amount) + let purpose: AppStoreTransactionPurpose + switch self.purpose { + case let .gift(peerId): + purpose = .starsGift(peerId: peerId, count: product.count, currency: currency, amount: amount) + default: + purpose = .stars(count: product.count, currency: currency, amount: amount) + } let _ = (self.context.engine.payments.canPurchasePremium(purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] available in @@ -649,7 +663,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { self.updateInProgress(false) self.updated(transition: .easeInOut(duration: 0.2)) - self.completion(product.option.count) + self.completion(product.count) } }, error: { [weak self] error in if let strongSelf = self { @@ -699,13 +713,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } func makeState() -> State { - return State(context: self.context, starsContext: self.starsContext, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) + return State(context: self.context, starsContext: self.starsContext, purpose: self.purpose, initialOptions: self.options, updateInProgress: self.updateInProgress, present: self.present, completion: self.completion) } static var body: Body { let background = Child(Rectangle.self) let scrollContent = Child(ScrollComponent.self) let star = Child(PremiumStarComponent.self) + let avatar = Child(GiftAvatarComponent.self) let topPanel = Child(BlurredBackgroundComponent.self) let topSeparator = Child(Rectangle.self) let title = Child(MultilineTextComponent.self) @@ -730,23 +745,44 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { starIsVisible = false } - let header = star.update( - component: PremiumStarComponent( - theme: environment.theme, - isIntro: true, - isVisible: starIsVisible, - hasIdleAnimations: state.hasIdleAnimations, - colors: [ - UIColor(rgb: 0xe57d02), - UIColor(rgb: 0xf09903), - UIColor(rgb: 0xf9b004), - UIColor(rgb: 0xfdd219) - ], - particleColor: UIColor(rgb: 0xf9b004) - ), - availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), - transition: context.transition - ) + let header: _UpdatedChildComponent + if case let .gift(peerId) = context.component.purpose { + var peers: [EnginePeer] = [] + if let peer = state.peers[peerId] { + peers.append(peer) + } + header = avatar.update( + component: GiftAvatarComponent( + context: context.component.context, + theme: environment.theme, + peers: peers, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations, + color: UIColor(rgb: 0xf9b004), + hasLargeParticles: true + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + } else { + header = star.update( + component: PremiumStarComponent( + theme: environment.theme, + isIntro: true, + isVisible: starIsVisible, + hasIdleAnimations: state.hasIdleAnimations, + colors: [ + UIColor(rgb: 0xe57d02), + UIColor(rgb: 0xf09903), + UIColor(rgb: 0xf9b004), + UIColor(rgb: 0xfdd219) + ], + particleColor: UIColor(rgb: 0xf9b004) + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + } let topPanel = topPanel.update( component: BlurredBackgroundComponent( @@ -765,10 +801,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { ) let titleText: String - if let requiredStars = context.component.requiredStars { - titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) - } else { + switch context.component.purpose { + case .generic: titleText = strings.Stars_Purchase_GetStars + case .gift: + titleText = strings.Stars_Purchase_GiftStars + case let .transfer(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars): + titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } let title = title.update( @@ -820,12 +859,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { containerSize: context.availableSize, balance: state.starsState?.balance, options: context.component.options, - peerId: context.component.peerId, - requiredStars: context.component.requiredStars, + purpose: context.component.purpose, selectedProductId: state.progressProduct?.storeProduct.id, forceDark: context.component.forceDark, products: state.products, expanded: state.isExpanded, + peers: state.peers, stateUpdated: { [weak state] transition in scrollAction.invoke(CGPoint(x: 0.0, y: 150.0 + contentExternalState.descriptionHeight)) state?.isExpanded = true @@ -929,7 +968,6 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { public final class StarsPurchaseScreen: ViewControllerComponentContainer { fileprivate let context: AccountContext fileprivate let starsContext: StarsContext - fileprivate let options: [StarsTopUpOption] private var didSetReady = false private let _ready = Promise() @@ -940,16 +978,12 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { public init( context: AccountContext, starsContext: StarsContext, - options: [StarsTopUpOption], - peerId: EnginePeer.Id?, - requiredStars: Int64?, - modal: Bool = true, - forceDark: Bool = false, + options: [Any] = [], + purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void = { _ in } ) { self.context = context self.starsContext = starsContext - self.options = options var updateInProgressImpl: ((Bool) -> Void)? var presentImpl: ((ViewController) -> Void)? @@ -958,9 +992,8 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { context: context, starsContext: starsContext, options: options, - peerId: peerId, - requiredStars: requiredStars, - forceDark: forceDark, + purpose: purpose, + forceDark: false, updateInProgress: { inProgress in updateInProgressImpl?(inProgress) }, @@ -970,17 +1003,13 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { completion: { stars in completionImpl?(stars) } - ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) + ), navigationBarAppearance: .transparent, presentationMode: .modal, theme: .default) let presentationData = context.sharedContext.currentPresentationData.with { $0 } - if modal { - let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) - self.navigationItem.setLeftBarButton(cancelItem, animated: false) - self.navigationPresentation = .modal - } else { - self.navigationPresentation = .modalInLargeLayout - } + let cancelItem = UIBarButtonItem(title: presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed)) + self.navigationItem.setLeftBarButton(cancelItem, animated: false) + self.navigationPresentation = .modal updateInProgressImpl = { [weak self] inProgress in if let strongSelf = self { @@ -1043,6 +1072,9 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { if let view = self.node.hostView.findTaggedView(tag: PremiumStarComponent.View.Tag()) as? PremiumStarComponent.View { self.didSetReady = true self._ready.set(view.ready) + } else if let view = self.node.hostView.findTaggedView(tag: GiftAvatarComponent.View.Tag()) as? GiftAvatarComponent.View { + self.didSetReady = true + self._ready.set(view.ready) } } } @@ -1141,3 +1173,31 @@ final class StarsIconComponent: CombinedComponent { } } } + +private extension StarsPurchasePurpose { + var peerIds: [EnginePeer.Id] { + switch self { + case let .gift(peerId): + return [peerId] + case let .transfer(peerId, _): + return [peerId] + case let .subscription(peerId, _, _): + return [peerId] + default: + return [] + } + } + + var requiredStars: Int64? { + switch self { + case let .transfer(_, requiredStars): + return requiredStars + case let .subscription(_, requiredStars, _): + return requiredStars + case let .unlockMedia(requiredStars): + return requiredStars + default: + return nil + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD index f392b1ca5e..66f7f31423 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/Components/SolidRoundedButtonComponent", "//submodules/AvatarNode", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/TelegramUI/Components/Stars/StarsAvatarComponent", "//submodules/GalleryUI", ], visibility = [ diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index eb7299b249..0f5ddc38bd 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -22,6 +22,7 @@ import TelegramStringFormatting import UndoUI import StarsImageComponent import GalleryUI +import StarsAvatarComponent private final class StarsTransactionSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -73,6 +74,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { var peerMap: [EnginePeer.Id: EnginePeer] = [:] var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? var inProgress = false @@ -89,6 +91,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } case let .receipt(receipt): peerIds.append(receipt.botPaymentId) + case let .gift(message): + peerIds.append(message.id.peerId) } self.disposable = (context.engine.data.get( @@ -186,87 +190,110 @@ private final class StarsTransactionSheetContent: CombinedComponent { let media: [AnyMediaReference] let photo: TelegramMediaWebFile? let isRefund: Bool + let isGift: Bool var delayedCloseOnOpenPeer = true switch subject { case let .transaction(transaction, parentPeer): - switch transaction.peer { - case let .peer(peer): + if transaction.flags.contains(.isGift) { + titleText = "Received Gift" + descriptionText = "Use Stars to unlock content and services on Telegram. [See Examples >]()" + count = transaction.count + transactionId = transaction.id + via = nil + messageId = nil + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } else { + toPeer = nil + } + transactionPeer = transaction.peer + media = [] + photo = nil + isRefund = false + isGift = true + delayedCloseOnOpenPeer = false + } else { + switch transaction.peer { + case let .peer(peer): + if !transaction.media.isEmpty { + titleText = strings.Stars_Transaction_MediaPurchase + } else { + titleText = transaction.title ?? peer.compactDisplayTitle + } + via = nil + case .appStore: + titleText = strings.Stars_Transaction_AppleTopUp_Title + via = strings.Stars_Transaction_AppleTopUp_Subtitle + case .playMarket: + titleText = strings.Stars_Transaction_GoogleTopUp_Title + via = strings.Stars_Transaction_GoogleTopUp_Subtitle + case .premiumBot: + titleText = strings.Stars_Transaction_PremiumBotTopUp_Title + via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle + case .fragment: + if parentPeer.id == component.context.account.peerId { + titleText = strings.Stars_Transaction_FragmentTopUp_Title + via = strings.Stars_Transaction_FragmentTopUp_Subtitle + } else { + titleText = strings.Stars_Transaction_FragmentWithdrawal_Title + via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle + } + case .ads: + titleText = strings.Stars_Transaction_TelegramAds_Title + via = strings.Stars_Transaction_TelegramAds_Subtitle + case .unsupported: + titleText = strings.Stars_Transaction_Unsupported_Title + via = nil + } if !transaction.media.isEmpty { - titleText = strings.Stars_Transaction_MediaPurchase + var description: String = "" + var photoCount: Int32 = 0 + var videoCount: Int32 = 0 + for media in transaction.media { + if let _ = media as? TelegramMediaFile { + videoCount += 1 + } else { + photoCount += 1 + } + } + if photoCount > 0 && videoCount > 0 { + description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string + } else if photoCount > 0 { + if photoCount > 1 { + description += strings.Stars_Transaction_Photos(photoCount) + } else { + description += strings.Stars_Transaction_SinglePhoto + } + } else if videoCount > 0 { + if videoCount > 1 { + description += strings.Stars_Transaction_Videos(videoCount) + } else { + description += strings.Stars_Transaction_SingleVideo + } + } + descriptionText = description } else { - titleText = transaction.title ?? peer.compactDisplayTitle + descriptionText = transaction.description ?? "" } - via = nil - case .appStore: - titleText = strings.Stars_Transaction_AppleTopUp_Title - via = strings.Stars_Transaction_AppleTopUp_Subtitle - case .playMarket: - titleText = strings.Stars_Transaction_GoogleTopUp_Title - via = strings.Stars_Transaction_GoogleTopUp_Subtitle - case .premiumBot: - titleText = strings.Stars_Transaction_PremiumBotTopUp_Title - via = strings.Stars_Transaction_PremiumBotTopUp_Subtitle - case .fragment: - if parentPeer.id == component.context.account.peerId { - titleText = strings.Stars_Transaction_FragmentTopUp_Title - via = strings.Stars_Transaction_FragmentTopUp_Subtitle + + messageId = transaction.paidMessageId + + count = transaction.count + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer } else { - titleText = strings.Stars_Transaction_FragmentWithdrawal_Title - via = strings.Stars_Transaction_FragmentWithdrawal_Subtitle + toPeer = nil } - case .ads: - titleText = strings.Stars_Transaction_TelegramAds_Title - via = strings.Stars_Transaction_TelegramAds_Subtitle - case .unsupported: - titleText = strings.Stars_Transaction_Unsupported_Title - via = nil + transactionPeer = transaction.peer + media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } + photo = transaction.photo + isGift = false + isRefund = transaction.flags.contains(.isRefund) } - if !transaction.media.isEmpty { - var description: String = "" - var photoCount: Int32 = 0 - var videoCount: Int32 = 0 - for media in transaction.media { - if let _ = media as? TelegramMediaFile { - videoCount += 1 - } else { - photoCount += 1 - } - } - if photoCount > 0 && videoCount > 0 { - description += strings.Stars_Transaction_MediaAnd(strings.Stars_Transaction_Photos(photoCount), strings.Stars_Transaction_Videos(videoCount)).string - } else if photoCount > 0 { - if photoCount > 1 { - description += strings.Stars_Transaction_Photos(photoCount) - } else { - description += strings.Stars_Transaction_SinglePhoto - } - } else if videoCount > 0 { - if videoCount > 1 { - description += strings.Stars_Transaction_Videos(videoCount) - } else { - description += strings.Stars_Transaction_SingleVideo - } - } - descriptionText = description - } else { - descriptionText = transaction.description ?? "" - } - - messageId = transaction.paidMessageId - - count = transaction.count - transactionId = transaction.id - date = transaction.date - if case let .peer(peer) = transaction.peer { - toPeer = peer - } else { - toPeer = nil - } - transactionPeer = transaction.peer - media = transaction.media.map { AnyMediaReference.starsTransaction(transaction: StarsTransactionReference(peerId: parentPeer.id, id: transaction.id, isRefund: transaction.flags.contains(.isRefund)), media: $0) } - photo = transaction.photo - isRefund = transaction.flags.contains(.isRefund) case let .receipt(receipt): titleText = receipt.invoiceMedia.title descriptionText = receipt.invoiceMedia.description @@ -284,6 +311,28 @@ private final class StarsTransactionSheetContent: CombinedComponent { media = [] photo = receipt.invoiceMedia.photo isRefund = false + isGift = false + delayedCloseOnOpenPeer = false + case let .gift(message): + let incoming = message.flags.contains(.Incoming) + titleText = incoming ? "Received Gift" : "Sent Gift" + let peerName = state.peerMap[message.id.peerId]?.compactDisplayTitle ?? "" + descriptionText = incoming ? "Use Stars to unlock content and services on Telegram. [See Examples >]()" : "With Stars, \(peerName) will be able to unlock content and services on Telegram.\n[See Examples >]()" + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .giftStars(_, _, countValue, _, _, _) = action.action { + count = !incoming ? -countValue : countValue + transactionId = nil + } else { + fatalError() + } + via = nil + messageId = nil + date = message.timestamp + toPeer = state.peerMap[message.id.peerId] + transactionPeer = nil + media = [] + photo = nil + isRefund = false + isGift = true delayedCloseOnOpenPeer = false } @@ -312,7 +361,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) let imageSubject: StarsImageComponent.Subject - if !media.isEmpty { + if isGift { + imageSubject = .gift + } else if !media.isEmpty { imageSubject = .media(media) } else if let photo { imageSubject = .photo(photo) @@ -373,12 +424,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { content: AnyComponent( PeerCellComponent( context: component.context, - textColor: tableLinkColor, + theme: theme, peer: toPeer ) ), action: { - if delayedCloseOnOpenPeer { + if toPeer.id.namespace == Namespaces.Peer.CloudUser && toPeer.id.id._internalGetInt64Value() == 777000 { + + } else if delayedCloseOnOpenPeer { component.openPeer(toPeer) Queue.mainQueue().after(1.0, { component.cancel(false) @@ -539,14 +592,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { originY += star.size.height - 23.0 if !descriptionText.isEmpty { + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) + } + + let textFont = Font.regular(15.0) + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString + if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string)) + } let description = description.update( component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: descriptionText, - font: Font.regular(15.0), - textColor: theme.actionSheet.primaryTextColor, - paragraphAlignment: .center - )), + text: .plain(attributedString), horizontalAlignment: .center, maximumNumberOfLines: 3 ), @@ -768,6 +828,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case transaction(StarsContext.State.Transaction, EnginePeer) case receipt(BotPaymentReceipt) + case gift(EngineMessage) } private let context: AccountContext @@ -1166,12 +1227,12 @@ private final class TableComponent: CombinedComponent { private final class PeerCellComponent: Component { let context: AccountContext - let textColor: UIColor - let peer: EnginePeer? + let theme: PresentationTheme + let peer: EnginePeer - init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { + init(context: AccountContext, theme: PresentationTheme, peer: EnginePeer) { self.context = context - self.textColor = textColor + self.theme = theme self.peer = peer } @@ -1179,7 +1240,7 @@ private final class PeerCellComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.textColor !== rhs.textColor { + if lhs.theme !== rhs.theme { return false } if lhs.peer != rhs.peer { @@ -1189,18 +1250,14 @@ private final class PeerCellComponent: Component { } final class View: UIView { - private let avatarNode: AvatarNode + private let avatar = ComponentView() private let text = ComponentView() private var component: PeerCellComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) - super.init(frame: frame) - - self.addSubnode(self.avatarNode) } required init?(coder: NSCoder) { @@ -1211,21 +1268,33 @@ private final class PeerCellComponent: Component { self.component = component self.state = state - self.avatarNode.setPeer( - context: component.context, - theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, - peer: component.peer, - synchronousLoad: true - ) - let avatarSize = CGSize(width: 22.0, height: 22.0) let spacing: CGFloat = 6.0 + let peerName: String + let peer: StarsContext.State.Transaction.Peer + if component.peer.id.namespace == Namespaces.Peer.CloudUser && component.peer.id.id._internalGetInt64Value() == 777000 { + peerName = "Unknown User" + peer = .fragment + } else { + peerName = component.peer.compactDisplayTitle + peer = .peer(component.peer) + } + + let avatarNaturalSize = self.avatar.update( + transition: .immediate, + component: AnyComponent( + StarsAvatarComponent(context: component.context, theme: component.theme, peer: peer, photo: nil, media: [], backgroundColor: .clear) + ), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let textSize = self.text.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) + text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.theme.list.itemAccentColor, paragraphAlignment: .left)) ) ), environment: {}, @@ -1235,7 +1304,15 @@ private final class PeerCellComponent: Component { let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) - self.avatarNode.frame = avatarFrame + + if let view = self.avatar.view { + if view.superview == nil { + self.addSubview(view) + } + let scale = avatarSize.width / avatarNaturalSize.width + view.transform = CGAffineTransform(scaleX: scale, y: scale) + view.frame = avatarFrame + } if let view = self.text.view { if view.superview == nil { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 243e28cd2a..4316971319 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -23,6 +23,7 @@ final class StarsBalanceComponent: Component { let actionCooldownUntilTimestamp: Int32? let action: () -> Void let buyAds: (() -> Void)? + let additionalAction: AnyComponent? init( theme: PresentationTheme, @@ -35,7 +36,8 @@ final class StarsBalanceComponent: Component { actionIsEnabled: Bool, actionCooldownUntilTimestamp: Int32? = nil, action: @escaping () -> Void, - buyAds: (() -> Void)? + buyAds: (() -> Void)?, + additionalAction: AnyComponent? = nil ) { self.theme = theme self.strings = strings @@ -48,6 +50,7 @@ final class StarsBalanceComponent: Component { self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp self.action = action self.buyAds = buyAds + self.additionalAction = additionalAction } static func ==(lhs: StarsBalanceComponent, rhs: StarsBalanceComponent) -> Bool { @@ -88,6 +91,8 @@ final class StarsBalanceComponent: Component { private var button = ComponentView() private var buyAdsButton = ComponentView() + private var additionalButton = ComponentView() + private var component: StarsBalanceComponent? private weak var state: EmptyComponentState? @@ -275,9 +280,29 @@ final class StarsBalanceComponent: Component { } } - contentHeight += buttonSize.height } + + if let additionalAction = component.additionalAction { + contentHeight += 18.0 + + let buttonSize = self.additionalButton.update( + transition: transition, + component: additionalAction, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 50.0) + ) + if let buttonView = self.additionalButton.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) / 2.0), y: contentHeight), size: buttonSize) + buttonView.frame = buttonFrame + } + contentHeight += buttonSize.height + contentHeight += 2.0 + } + contentHeight += sideInset return CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index 2e8c2b6441..bdbdf00eb5 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -203,12 +203,13 @@ final class StarsTransactionsListPanelComponent: Component { let fontBaseDisplaySize = 17.0 - let itemTitle: String + var itemTitle: String let itemSubtitle: String? var itemDate: String + var itemPeer = item.peer switch item.peer { case let .peer(peer): - if !item.media.isEmpty { + if !item.media.isEmpty { itemTitle = environment.strings.Stars_Intro_Transaction_MediaPurchase itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else if let title = item.title { @@ -216,7 +217,16 @@ final class StarsTransactionsListPanelComponent: Component { itemSubtitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = nil + if item.flags.contains(.isGift) { + //TODO:localize + itemSubtitle = "Received Gift" + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id.id._internalGetInt64Value() == 777000 { + itemTitle = "Unknown User" + itemPeer = .fragment + } + } else { + itemSubtitle = nil + } } case .appStore: itemTitle = environment.strings.Stars_Intro_Transaction_AppleTopUp_Title @@ -298,7 +308,7 @@ final class StarsTransactionsListPanelComponent: Component { theme: environment.theme, title: AnyComponent(VStack(titleComponents, alignment: .left, spacing: 2.0)), contentInsets: UIEdgeInsets(top: 9.0, left: environment.containerInsets.left, bottom: 8.0, right: environment.containerInsets.right), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: item.peer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(StarsAvatarComponent(context: component.context, theme: environment.theme, peer: itemPeer, photo: item.photo, media: item.media, backgroundColor: environment.theme.list.plainBackgroundColor))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(StarsLabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index cd66fc1390..f7ebfb81c6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -26,17 +26,20 @@ final class StarsTransactionsScreenComponent: Component { let starsContext: StarsContext let openTransaction: (StarsContext.State.Transaction) -> Void let buy: () -> Void + let gift: () -> Void init( context: AccountContext, starsContext: StarsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, - buy: @escaping () -> Void + buy: @escaping () -> Void, + gift: @escaping () -> Void ) { self.context = context self.starsContext = starsContext self.openTransaction = openTransaction self.buy = buy + self.gift = gift } static func ==(lhs: StarsTransactionsScreenComponent, rhs: StarsTransactionsScreenComponent) -> Bool { @@ -89,6 +92,8 @@ final class StarsTransactionsScreenComponent: Component { private let balanceView = ComponentView() + private let subscriptionsView = ComponentView() + private let topBalanceTitleView = ComponentView() private let topBalanceValueView = ComponentView() private let topBalanceIconView = ComponentView() @@ -282,6 +287,7 @@ final class StarsTransactionsScreenComponent: Component { } let environment = environment[ViewControllerComponentContainer.Environment.self].value + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if self.stateDisposable == nil { self.stateDisposable = (component.starsContext.state @@ -531,7 +537,27 @@ final class StarsTransactionsScreenComponent: Component { } component.buy() }, - buyAds: nil + buyAds: nil, + additionalAction: AnyComponent( + Button( + content: AnyComponent( + HStack([ + AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Premium/Stars/Gift", tintColor: environment.theme.list.itemAccentColor)) + ), + AnyComponentWithIdentity( + id: "label", + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Gift Stars to Friends", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)))) + ) + ], + spacing: 6.0) + ), + action: { + component.gift() + } + ) + ) ) ))] )), @@ -545,10 +571,42 @@ final class StarsTransactionsScreenComponent: Component { } starTransition.setFrame(view: balanceView, frame: balanceFrame) } - contentHeight += balanceSize.height contentHeight += 44.0 + let subscriptionsItems: [AnyComponentWithIdentity] = [] + + if !subscriptionsItems.isEmpty { + //TODO:localize + let subscriptionsSize = self.subscriptionsView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "My Subscriptions".uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: subscriptionsItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let subscriptionsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subscriptionsSize.width) / 2.0), y: contentHeight), size: subscriptionsSize) + if let subscriptionsView = self.subscriptionsView.view { + if subscriptionsView.superview == nil { + self.scrollView.addSubview(subscriptionsView) + } + starTransition.setFrame(view: subscriptionsView, frame: subscriptionsFrame) + } + contentHeight += subscriptionsSize.height + contentHeight += 44.0 + } + let initialTransactions = self.starsState?.transactions ?? [] var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] if !initialTransactions.isEmpty { @@ -704,6 +762,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { self.starsContext = starsContext var buyImpl: (() -> Void)? + var giftImpl: (() -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? super.init(context: context, component: StarsTransactionsScreenComponent( context: context, @@ -713,6 +772,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }, buy: { buyImpl?() + }, + gift: { + giftImpl?() } ), navigationBarAppearance: .transparent) @@ -744,7 +806,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil, completion: { [weak self] stars in + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: .generic, completion: { [weak self] stars in guard let self else { return } @@ -768,6 +830,36 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer { }) } + giftImpl = { [weak self] in + guard let self else { + return + } + let _ = combineLatest(queue: Queue.mainQueue(), + self.options.get() |> take(1), + self.context.account.stateManager.contactBirthdays |> take(1) + ).start(next: { [weak self] options, birthdays in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .stars(birthdays), completion: { [weak self] peerIds in + guard let self, let peerId = peerIds.first else { + return + } + let purchaseController = self.context.sharedContext.makeStarsPurchaseScreen( + context: self.context, + starsContext: starsContext, + options: options, + purpose: .gift(peerId: peerId), + completion: { stars in + + } + ) + self.push(purchaseController) + }) + self.push(controller) + }) + } + self.starsContext.load(force: false) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 5e2223bd68..71b0c643d1 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -478,12 +478,19 @@ private final class SheetContent: CombinedComponent { state?.buy(requestTopUp: { [weak controller] completion in let premiumConfiguration = PremiumConfiguration.with(appConfiguration: accountContext.currentAppConfiguration.with { $0 }) if !premiumConfiguration.isPremiumDisabled { + let purpose: StarsPurchasePurpose + if isMedia { + purpose = .unlockMedia(requiredStars: invoice.totalAmount) + } else if let peerId = state?.botPeer?.id { + purpose = .transfer(peerId: peerId, requiredStars: invoice.totalAmount) + } else { + purpose = .generic + } let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( context: accountContext, starsContext: starsContext, options: state?.options ?? [], - peerId: isMedia ? nil : state?.botPeer?.id, - requiredStars: invoice.totalAmount, + purpose: purpose, completion: { [weak starsContext] stars in starsContext?.add(balance: stars) Queue.mainQueue().after(0.1) { diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 80ee4b5559..969429e816 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -552,6 +552,9 @@ public class StickerPickerScreen: ViewController { self.presentLinkPremiumSuggestion() } } + self.storyStickersContentView?.weatherAction = { [weak self] in + self?.controller?.addWeather() + } } let gifItems: Signal @@ -2063,6 +2066,7 @@ public class StickerPickerScreen: ViewController { public var presentAudioPicker: () -> Void = { } public var addReaction: () -> Void = { } public var addLink: () -> Void = { } + public var addWeather: () -> Void = { } public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { self.context = context @@ -2204,16 +2208,30 @@ private final class InteractiveStickerButtonContent: Component { func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor - let iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent( - name: component.iconName, - tintColor: .white, - maxSize: CGSize(width: 20.0, height: 20.0) - )), - environment: {}, - containerSize: availableSize - ) + let iconSize: CGSize + if component.iconName == "Sun" { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(Text( + text: "☀️", + font: Font.with(size: 23.0, design: .camera), + color: .white + )), + environment: {}, + containerSize: availableSize + ) + } else { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: component.iconName, + tintColor: .white, + maxSize: CGSize(width: 20.0, height: 20.0) + )), + environment: {}, + containerSize: availableSize + ) + } let titleSize = self.title.update( transition: .immediate, component: AnyComponent(Text( @@ -2473,7 +2491,7 @@ final class ItemStack: CombinedComponent { let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0 let spacing = remainingWidth / CGFloat(rowItemsCount - 1) - if spacing < context.component.minSpacing || currentGroup.count == 2 { + if spacing < context.component.minSpacing || currentGroup.count == 3 { groups.append(currentGroup) currentGroup = [] } @@ -2537,6 +2555,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { var audioAction: () -> Void = {} var reactionAction: () -> Void = {} var linkAction: () -> Void = {} + var weatherAction: () -> Void = {} init(isPremium: Bool) { self.isPremium = isPremium @@ -2601,6 +2620,29 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { }) ) ), + AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + InteractiveStickerButtonContent( + theme: theme, + title: "35°C", + iconName: "Sun", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.weatherAction() + } + }) + ) + ), AnyComponentWithIdentity( id: "audio", component: AnyComponent( diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index ffe0785437..1848125e56 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -36,27 +36,59 @@ struct CameraState: Equatable { case holding case handsFree } + enum FlashTint: Equatable { + case white + case yellow + case blue + + var color: UIColor { + switch self { + case .white: + return .white + case .yellow: + return UIColor(rgb: 0xffed8c) + case .blue: + return UIColor(rgb: 0x8cdfff) + } + } + } let position: Camera.Position + let flashMode: Camera.FlashMode + let flashModeDidChange: Bool + let flashTint: FlashTint + let flashTintSize: CGFloat let recording: Recording let duration: Double let isDualCameraEnabled: Bool let isViewOnceEnabled: Bool func updatedPosition(_ position: Camera.Position) -> CameraState { - return CameraState(position: position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + return CameraState(position: position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { + return CameraState(position: self.position, flashMode: flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashTint(_ flashTint: FlashTint) -> CameraState { + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + } + + func updatedFlashTintSize(_ flashTintSize: CGFloat) -> CameraState { + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(position: self.position, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(position: self.position, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: self.isViewOnceEnabled) } func updatedIsViewOnceEnabled(_ isViewOnceEnabled: Bool) -> CameraState { - return CameraState(position: self.position, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled) + return CameraState(position: self.position, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, flashTint: self.flashTint, flashTintSize: self.flashTintSize, recording: self.recording, duration: self.duration, isDualCameraEnabled: self.isDualCameraEnabled, isViewOnceEnabled: isViewOnceEnabled) } } @@ -143,7 +175,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { final class State: ComponentState { enum ImageKey: Hashable { case flip + case flash case buttonBackground + case flashImage } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey, theme: PresentationTheme) -> UIImage { @@ -154,9 +188,23 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { switch key { case .flip: image = UIImage(bundleImageName: "Camera/VideoMessageFlip")!.withRenderingMode(.alwaysTemplate) + case .flash: + image = UIImage(bundleImageName: "Camera/VideoMessageFlash")!.withRenderingMode(.alwaysTemplate) case .buttonBackground: let innerSize = CGSize(width: 40.0, height: 40.0) image = generateFilledCircleImage(diameter: innerSize.width, color: theme.rootController.navigationBar.opaqueBackgroundColor, strokeColor: theme.chat.inputPanel.panelSeparatorColor, strokeWidth: 0.5, backgroundColor: nil)! + case .flashImage: + image = generateImage(CGSize(width: 393.0, height: 852.0), rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + + var locations: [CGFloat] = [0.0, 0.2, 0.6, 1.0] + let colors: [CGColor] = [UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 0.25).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor, UIColor(rgb: 0xffffff, alpha: 1.0).cgColor] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0 - 10.0) + context.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: size.width, options: .drawsAfterEndLocation) + })!.withRenderingMode(.alwaysTemplate) } cachedImages[key] = image return image @@ -175,6 +223,8 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { var cameraState: CameraState? var didDisplayViewOnce = false + + var displayingFlashTint = false private let hapticFeedback = HapticFeedback() @@ -238,6 +288,81 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { self.hapticFeedback.impact(.veryLight) } + func toggleFlashMode() { + guard let controller = self.getController(), let camera = controller.camera else { + return + } + var flashOn = false + switch controller.cameraState.flashMode { + case .off: + flashOn = true + camera.setFlashMode(.on) + case .on: + camera.setFlashMode(.off) + default: + camera.setFlashMode(.off) + } + self.hapticFeedback.impact(.light) + + self.updateScreenBrightness(flashOn: flashOn) + } + + private var initialBrightness: CGFloat? + private var brightnessArguments: (Double, Double, CGFloat, CGFloat)? + private var brightnessAnimator: ConstantDisplayLinkAnimator? + + func updateScreenBrightness(flashOn: Bool?) { + guard let controller = self.getController() else { + return + } + let isFrontCamera = controller.cameraState.position == .front + let isVideo = true + let isFlashOn = flashOn ?? (controller.cameraState.flashMode == .on) + + if isFrontCamera && isVideo && isFlashOn { + if self.initialBrightness == nil { + self.initialBrightness = UIScreen.main.brightness + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, 1.0) + self.animateBrightnessChange() + } + } else { + if let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } + } + } + + private func animateBrightnessChange() { + if self.brightnessAnimator == nil { + self.brightnessAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.animateBrightnessChange() + }) + self.brightnessAnimator?.isPaused = true + } + + if let (startTime, duration, initial, target) = self.brightnessArguments { + self.brightnessAnimator?.isPaused = false + + let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration))) + let value = initial + (target - initial) * t + + UIScreen.main.brightness = value + + if t >= 1.0 { + self.brightnessArguments = nil + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil + } + } else { + self.brightnessAnimator?.isPaused = true + self.brightnessAnimator?.invalidate() + self.brightnessAnimator = nil + } + } + func startVideoRecording(pressing: Bool) { guard let controller = self.getController(), let camera = controller.camera else { return @@ -312,6 +437,12 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { controller.updateCameraState({ $0.updatedRecording(.none) }, transition: .spring(duration: 0.4)) } })) + + if case .front = controller.cameraState.position, let initialBrightness = self.initialBrightness { + self.initialBrightness = nil + self.brightnessArguments = (CACurrentMediaTime(), 0.2, UIScreen.main.brightness, initialBrightness) + self.animateBrightnessChange() + } } func lockVideoRecording() { @@ -334,7 +465,9 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } static var body: Body { + let frontFlash = Child(Image.self) let flipButton = Child(CameraButton.self) + let flashButton = Child(CameraButton.self) let viewOnceButton = Child(PlainButtonComponent.self) let recordMoreButton = Child(PlainButtonComponent.self) @@ -381,6 +514,20 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } if !component.isPreviewing { + if case .on = component.cameraState.flashMode { + let frontFlash = frontFlash.update( + component: Image(image: state.image(.flashImage, theme: environment.theme), tintColor: component.cameraState.flashTint.color), + availableSize: availableSize, + transition: .easeInOut(duration: 0.2) + ) + context.add(frontFlash + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + .scale(1.5 - component.cameraState.flashTintSize * 0.5) + .appear(.default(alpha: true)) + .disappear(.default(alpha: true)) + ) + } + let flipButton = flipButton.update( component: CameraButton( content: AnyComponentWithIdentity( @@ -409,6 +556,35 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) + + let flashButton = flashButton.update( + component: CameraButton( + content: AnyComponentWithIdentity( + id: "flash", + component: AnyComponent( + Image( + image: state.image(.flash, theme: environment.theme), + tintColor: environment.theme.list.itemAccentColor, + size: CGSize(width: 30.0, height: 30.0) + ) + ) + ), + minSize: CGSize(width: 44.0, height: 44.0), + isExclusive: false, + action: { [weak state] in + if let state { + state.toggleFlashMode() + } + } + ), + availableSize: availableSize, + transition: context.transition + ) + context.add(flashButton + .position(CGPoint(x: flipButton.size.width + 8.0 + flashButton.size.width / 2.0 + 8.0, y: availableSize.height - flashButton.size.height / 2.0 - 8.0)) + .appear(.default(scale: true, alpha: true)) + .disappear(.default(scale: true, alpha: true)) + ) } if showViewOnce { @@ -655,6 +831,10 @@ public class VideoMessageCameraScreen: ViewController { self.cameraState = CameraState( position: isFrontPosition ? .front : .back, + flashMode: .off, + flashModeDidChange: false, + flashTint: .white, + flashTintSize: 1.0, recording: .none, duration: 0.0, isDualCameraEnabled: isDualCameraEnabled, @@ -760,12 +940,15 @@ public class VideoMessageCameraScreen: ViewController { secondaryPreviewView: self.additionalPreviewView ) - self.cameraStateDisposable = (camera.position - |> deliverOnMainQueue).start(next: { [weak self] position in + self.cameraStateDisposable = combineLatest( + queue: Queue.mainQueue(), + camera.flashMode, + camera.position + ).start(next: { [weak self] flashMode, position in guard let self else { return } - self.cameraState = self.cameraState.updatedPosition(position) + self.cameraState = self.cameraState.updatedPosition(position).updatedFlashMode(flashMode) if !self.cameraState.isDualCameraEnabled { self.animatePositionChange() diff --git a/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json new file mode 100644 index 0000000000..4b8655a27a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf new file mode 100644 index 0000000000..cc1baf3f94 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/Contents.json new file mode 100644 index 0000000000..971ab0d3b0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "readinglist.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/readinglist.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/readinglist.pdf new file mode 100644 index 0000000000..3b67d5ae01 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/readinglist.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/Contents.json new file mode 100644 index 0000000000..017f341bad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "newtab.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/newtab.pdf b/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/newtab.pdf new file mode 100644 index 0000000000..6c1a20cedf Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/newtab.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/Contents.json new file mode 100644 index 0000000000..5f3b4af07c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "goto.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/goto.pdf b/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/goto.pdf new file mode 100644 index 0000000000..39f161b805 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/goto.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json new file mode 100644 index 0000000000..638f9f6618 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "giftstars_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf new file mode 100644 index 0000000000..ed459de9e7 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/Contents.json new file mode 100644 index 0000000000..eea8a06148 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "balance_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/balance_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/balance_30.pdf new file mode 100644 index 0000000000..92f719d741 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/balance_30.pdf differ diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift index c3d6c7070b..d44e93258b 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenLinkContextMenu.swift @@ -183,7 +183,7 @@ extension ChatControllerImpl { if canAddToReadingList { items.append( - .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in + .action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_AddToReadingList, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) if let link = URL(string: url) { diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index db054f42a9..6415b406e9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1167,6 +1167,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = PremiumIntroScreen(context: strongSelf.context, source: .gift(from: fromPeerId, to: toPeerId, duration: duration, giftCode: nil)) strongSelf.push(controller) return true + case .giftStars: + let controller = strongSelf.context.sharedContext.makeStarsGiftScreen(context: strongSelf.context, message: EngineMessage(message)) + strongSelf.push(controller) + return true case let .giftCode(slug, _, _, _, _, _, _, _, _): strongSelf.openResolved(result: .premiumGiftCode(slug: slug), sourceMessageId: message.id, progress: params.progress) return true diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift index f65710f110..4f54e76bcf 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenMessageReactionContextMenu.swift @@ -161,7 +161,15 @@ extension ChatControllerImpl { self.window?.presentInGlobalOverlay(controller) }) } else { - if self.context.sharedContext.applicationBindings.appBuildType == .internal, case .custom(MessageReaction.starsReactionId) = value { + var debug = false + #if DEBUG + debug = true + #endif + if self.context.sharedContext.applicationBindings.appBuildType == .internal { + debug = true + } + + if debug, case .custom(MessageReaction.starsReactionId) = value { let _ = (ChatSendStarsScreen.initialData(context: self.context, peerId: message.id.peerId) |> deliverOnMainQueue).start(next: { [weak self] initialData in guard let self, let initialData else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f54e75f141..c06a8e383f 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -3121,7 +3121,7 @@ private final class ChatReadReportContextItemNode: ASDisplayNode, ContextMenuCus } let avatarsSize = self.avatarsNode.update(context: self.item.context, content: avatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) - self.avatarsNode.frame = CGRect(origin: CGPoint(x: size.width - sideInset - 12.0 - avatarsSize.width, y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) + self.avatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(size.width - 28.0 - avatarsSize.width / 2.0), y: floor((size.height - avatarsSize.height) / 2.0)), size: avatarsSize) transition.updateAlpha(node: self.avatarsNode, alpha: self.currentStats == nil ? 0.0 : 1.0) let placeholderAvatarsSize = self.placeholderAvatarsNode.update(context: self.item.context, content: placeholderAvatarsContent, itemSize: CGSize(width: 24.0, height: 24.0), customSpacing: 10.0, animated: false, synchronousLoad: true) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 78043bd9ae..7f3a277dac 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -20,7 +20,7 @@ import TooltipUI import AnimatedStickerNode import TelegramAnimatedStickerNode import ShimmerEffect -import WebUI +import AttachmentUI private struct ThemeSettingsThemeEntry: Comparable, Identifiable { let index: Int diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift index 5b1f86bbe6..a598a5a4a1 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionController.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionController.swift @@ -169,7 +169,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection strongSelf.updateTitle() }) - case let .premiumGifting(birthdays, selectToday): + case let .premiumGifting(birthdays, selectToday, _): if let birthdays, selectToday { let today = Calendar(identifier: .gregorian).component(.day, from: Date()) var todayPeers: [EnginePeer.Id] = [] @@ -256,13 +256,13 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection self.rightNavigationButton = rightNavigationButton self.navigationItem.rightBarButtonItem = self.rightNavigationButton } - case .premiumGifting: + case let .premiumGifting(_, _, hasActions): let maxCount: Int32 = self.limit ?? 10 var count = 0 if case let .contacts(contactsNode) = self.contactsNode.contentNode { count = contactsNode.selectionState?.selectedPeerIndices.count ?? 0 } - self.titleView.title = CounterControllerTitle(title: self.presentationData.strings.Premium_Gift_ContactSelection_Title, counter: "\(count)/\(maxCount)") + self.titleView.title = CounterControllerTitle(title: hasActions ? self.presentationData.strings.Premium_Gift_ContactSelection_Title : self.presentationData.strings.Stars_Purchase_GiftStars, counter: "\(count)/\(maxCount)") case .requestedUsersSelection: let maxCount: Int32 = self.limit ?? 10 var count = 0 diff --git a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift index 93863b9988..2c748bf009 100644 --- a/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/ContactMultiselectionControllerNode.swift @@ -195,10 +195,10 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } else { let displayTopPeers: ContactListPresentation.TopPeers var selectedPeers: [EnginePeer.Id] = [] - if case let .premiumGifting(birthdays, selectToday) = mode { + if case let .premiumGifting(birthdays, selectToday, hasActions) = mode { if let birthdays { let today = Calendar(identifier: .gregorian).component(.day, from: Date()) - var sections: [(String, [EnginePeer.Id])] = [] + var sections: [(String, [EnginePeer.Id], Bool)] = [] var todayPeers: [EnginePeer.Id] = [] var yesterdayPeers: [EnginePeer.Id] = [] var tomorrowPeers: [EnginePeer.Id] = [] @@ -217,13 +217,13 @@ final class ContactMultiselectionControllerNode: ASDisplayNode { } if !todayPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayToday, todayPeers, hasActions)) } if !yesterdayPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayYesterday, yesterdayPeers, hasActions)) } if !tomorrowPeers.isEmpty { - sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers)) + sections.append((presentationData.strings.Premium_Gift_ContactSelection_BirthdayTomorrow, tomorrowPeers, hasActions)) } displayTopPeers = .custom(sections) diff --git a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift index 896e5879da..ca8e9f055c 100644 --- a/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift +++ b/submodules/TelegramUI/Sources/FetchCachedRepresentations.swift @@ -585,7 +585,7 @@ private func fetchVideoStickerRepresentation(account: Account, resource: MediaRe private func fetchPreparedPatternWallpaperRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPreparedPatternWallpaperRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let data = prepareSvgImage(unpackedData) { + if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let data = prepareSvgImage(unpackedData, true) { let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let _ = try? data.write(to: url) @@ -600,7 +600,7 @@ private func fetchPreparedPatternWallpaperRepresentation(resource: MediaResource private func fetchPreparedSvgRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPreparedSvgRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let data = prepareSvgImage(data) { + if let data = prepareSvgImage(data, true) { let path = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max))" let url = URL(fileURLWithPath: path) let _ = try? data.write(to: url) diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index e63b7b104c..1422973870 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -375,9 +375,8 @@ func openChatInstantPageImpl(context: AccountContext, message: Message, sourcePe let sourceLocation = InstantPageSourceLocation(userLocation: .peer(message.id.peerId), peerType: sourcePeerType ?? .channel) let pageController: ViewController - if !"".isEmpty, context.sharedContext.immediateExperimentalUISettings.browserExperiment { - let _ = anchor - pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, sourceLocation: sourceLocation)) + if context.sharedContext.immediateExperimentalUISettings.browserExperiment { + pageController = BrowserScreen(context: context, subject: .instantPage(webPage: webpage, anchor: anchor, sourceLocation: sourceLocation)) } else { pageController = InstantPageController(context: context, webPage: webpage, sourceLocation: sourceLocation, anchor: anchor) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index b734ce065b..54f2d73f79 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2185,10 +2185,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return PremiumLimitScreen(context: context, subject: mappedSubject, count: count, forceDark: forceDark, cancel: cancel, action: action) } - public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (() -> Void)?) -> ViewController { - let options = Promise<[PremiumGiftCodeOption]>() - options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) - + public func makePremiumGiftController(context: AccountContext, source: PremiumGiftSource, completion: (([EnginePeer.Id]) -> Void)?) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let limit: Int32 = 10 @@ -2197,15 +2194,18 @@ public final class SharedAccountContextImpl: SharedAccountContext { let mode: ContactMultiselectionControllerMode var currentBirthdays: [EnginePeer.Id: TelegramBirthday]? if case let .chatList(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: true) + mode = .premiumGifting(birthdays: birthdays, selectToday: true, hasActions: true) currentBirthdays = birthdays } else if case let .settings(birthdays) = source, let birthdays, !birthdays.isEmpty { - mode = .premiumGifting(birthdays: birthdays, selectToday: false) + mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: true) + currentBirthdays = birthdays + } else if case let .stars(birthdays) = source { + mode = .premiumGifting(birthdays: birthdays, selectToday: false, hasActions: false) currentBirthdays = birthdays } else { - mode = .premiumGifting(birthdays: nil, selectToday: false) + mode = .premiumGifting(birthdays: nil, selectToday: false, hasActions: true) } - + let contactOptions: Signal<[ContactListAdditionalOption], NoError> if currentBirthdays != nil || "".isEmpty { contactOptions = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)) @@ -2231,30 +2231,93 @@ public final class SharedAccountContextImpl: SharedAccountContext { var openProfileImpl: ((EnginePeer) -> Void)? var sendMessageImpl: ((EnginePeer) -> Void)? - let controller = context.sharedContext.makeContactMultiselectionController( - ContactMultiselectionControllerParams( + let controller: ViewController + if case .stars = source { + let options = Promise<[StarsGiftOption]>() + options.set(context.engine.payments.starsGiftOptions(peerId: nil)) + let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, - mode: mode, - options: contactOptions, - isPeerEnabled: { peer in - if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { - return true - } else { - return false - } - }, - limit: limit, - reachedLimit: { limit in - reachedLimitImpl?(limit) - }, - openProfile: { peer in - openProfileImpl?(peer) - }, - sendMessage: { peer in - sendMessageImpl?(peer) + title: { strings in return strings.Stars_Purchase_GiftStars } + )) + let _ = (contactsController.result + |> deliverOnMainQueue).start(next: { result in + if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer { + completion?([peer.id]) } + }) + controller = contactsController + } else { + let options = Promise<[PremiumGiftCodeOption]>() + options.set(context.engine.payments.premiumGiftCodeOptions(peerId: nil)) + let contactsController = context.sharedContext.makeContactMultiselectionController( + ContactMultiselectionControllerParams( + context: context, + mode: mode, + options: contactOptions, + isPeerEnabled: { peer in + if case let .user(user) = peer, user.botInfo == nil && !peer.isService && !user.flags.contains(.isSupport) { + return true + } else { + return false + } + }, + limit: limit, + reachedLimit: { limit in + reachedLimitImpl?(limit) + }, + openProfile: { peer in + openProfileImpl?(peer) + }, + sendMessage: { peer in + sendMessageImpl?(peer) + } + ) ) - ) + let _ = combineLatest(queue: Queue.mainQueue(), contactsController.result, options.get()) + .startStandalone(next: { [weak contactsController] result, options in + guard let controller = contactsController else { + return + } + var peerIds: [PeerId] = [] + if case let .result(peerIdsValue, _) = result { + peerIds = peerIdsValue.compactMap({ peerId in + if case let .peer(peerId) = peerId { + return peerId + } else { + return nil + } + }) + } + guard !peerIds.isEmpty else { + return + } + + let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } + var pushImpl: ((ViewController) -> Void)? + var filterImpl: (() -> Void)? + let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in + pushImpl?(c) + }, completion: { + filterImpl?() + + if case .chatList = source, let _ = currentBirthdays { + let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() + } + }) + pushImpl = { [weak giftController] c in + giftController?.push(c) + } + filterImpl = { [weak giftController] in + if let navigationController = giftController?.navigationController as? NavigationController { + var controllers = navigationController.viewControllers + controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } + navigationController.setViewControllers(controllers, animated: true) + } + } + controller.push(giftController) + }) + controller = contactsController + } reachedLimitImpl = { [weak controller] limit in guard let controller else { @@ -2263,52 +2326,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { HapticFeedback().error() controller.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Premium_Gift_ContactSelection_MaximumReached("\(limit)").string, timeout: nil, customUndoText: nil), elevatedLayout: true, position: .bottom, animateInAsReplacement: false, action: { _ in return false }), in: .current) } - - let _ = combineLatest(queue: Queue.mainQueue(), controller.result, options.get()) - .startStandalone(next: { [weak controller] result, options in - guard let controller else { - return - } - var peerIds: [PeerId] = [] - if case let .result(peerIdsValue, _) = result { - peerIds = peerIdsValue.compactMap({ peerId in - if case let .peer(peerId) = peerId { - return peerId - } else { - return nil - } - }) - } - guard !peerIds.isEmpty else { - return - } - let mappedOptions = options.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) } - var pushImpl: ((ViewController) -> Void)? - var filterImpl: (() -> Void)? - let giftController = PremiumGiftScreen(context: context, peerIds: peerIds, options: mappedOptions, source: source, pushController: { c in - pushImpl?(c) - }, completion: { - filterImpl?() - completion?() - - if case .chatList = source, let _ = currentBirthdays { - let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: .todayBirthdays).startStandalone() - } - }) - pushImpl = { [weak giftController] c in - giftController?.push(c) - } - filterImpl = { [weak giftController] in - if let navigationController = giftController?.navigationController as? NavigationController { - var controllers = navigationController.viewControllers - controllers = controllers.filter { !($0 is ContactMultiselectionController) && !($0 is PremiumGiftScreen) } - navigationController.setViewControllers(controllers, animated: true) - } - } - controller.push(giftController) - }) - sendMessageImpl = { [weak self, weak controller] peer in guard let self, let controller, let navigationController = controller.navigationController as? NavigationController else { return @@ -2630,10 +2648,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionsScreen(context: context, starsContext: starsContext) } - public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { - return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion) + public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [Any], purpose: StarsPurchasePurpose, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, purpose: purpose, completion: completion) } - + public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, extendedMedia: [TelegramExtendedMedia], inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController { return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, extendedMedia: extendedMedia, inputData: inputData, completion: completion) } @@ -2657,6 +2675,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { public func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController { return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion) } + + public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController { + return StarsTransactionScreen(context: context, subject: .gift(message), action: {}) + } } private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? { diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 67f47dff20..45b3e874fa 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -408,6 +408,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { case topic(id: Int64, info: EngineMessageHistoryThread.Info) case nameColors([UInt32]) case stars(tinted: Bool) + case ton } public let interactivelySelectedFromPackId: ItemCollectionId? diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 6cf8280091..995cc1d22b 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -27,161 +27,10 @@ import LocalAuth import OpenInExternalAppUI import ShareController import UndoUI +import AvatarNode private let durgerKingBotIds: [Int64] = [5104055776, 2200339955] -public class WebAppCancelButtonNode: ASDisplayNode { - public enum State { - case cancel - case back - } - - public let buttonNode: HighlightTrackingButtonNode - private let arrowNode: ASImageNode - private let labelNode: ImmediateTextNode - - public var state: State = .cancel - - private var color: UIColor? - - private var _theme: PresentationTheme - public var theme: PresentationTheme { - get { - return self._theme - } - set { - self._theme = newValue - self.setState(self.state, animated: false, animateScale: false, force: true) - } - } - private let strings: PresentationStrings - - private weak var colorSnapshotView: UIView? - - public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) { - let previousColor = self.color - self.color = color - - if case let .animated(duration, curve) = transition, previousColor != color, !self.animatingStateChange { - if let snapshotView = self.view.snapshotContentTree() { - snapshotView.frame = self.bounds - self.view.addSubview(snapshotView) - self.colorSnapshotView = snapshotView - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in - snapshotView.removeFromSuperview() - }) - self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) - self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) - } - } - self.setState(self.state, animated: false, animateScale: false, force: true) - } - - public init(theme: PresentationTheme, strings: PresentationStrings) { - self._theme = theme - self.strings = strings - - self.buttonNode = HighlightTrackingButtonNode() - - self.arrowNode = ASImageNode() - self.arrowNode.displaysAsynchronously = false - - self.labelNode = ImmediateTextNode() - self.labelNode.displaysAsynchronously = false - - super.init() - - self.addSubnode(self.buttonNode) - self.buttonNode.addSubnode(self.arrowNode) - self.buttonNode.addSubnode(self.labelNode) - - self.buttonNode.highligthedChanged = { [weak self] highlighted in - guard let strongSelf = self else { - return - } - if highlighted { - strongSelf.arrowNode.layer.removeAnimation(forKey: "opacity") - strongSelf.arrowNode.alpha = 0.4 - strongSelf.labelNode.layer.removeAnimation(forKey: "opacity") - strongSelf.labelNode.alpha = 0.4 - } else { - strongSelf.arrowNode.alpha = 1.0 - strongSelf.arrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.labelNode.alpha = 1.0 - strongSelf.labelNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - - self.setState(.cancel, animated: false, force: true) - } - - public func setTheme(_ theme: PresentationTheme, animated: Bool) { - self._theme = theme - var animated = animated - if self.animatingStateChange { - animated = false - } - self.setState(self.state, animated: animated, animateScale: false, force: true) - } - - private var animatingStateChange = false - public func setState(_ state: State, animated: Bool, animateScale: Bool = true, force: Bool = false) { - guard self.state != state || force else { - return - } - self.state = state - - if let colorSnapshotView = self.colorSnapshotView { - self.colorSnapshotView = nil - colorSnapshotView.removeFromSuperview() - } - - if animated, let snapshotView = self.buttonNode.view.snapshotContentTree() { - self.animatingStateChange = true - snapshotView.layer.sublayerTransform = self.buttonNode.subnodeTransform - self.view.addSubview(snapshotView) - - let duration: Double = animateScale ? 0.25 : 0.3 - if animateScale { - snapshotView.layer.animateScale(from: 1.0, to: 0.001, duration: 0.25, removeOnCompletion: false) - } - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - self.animatingStateChange = false - }) - - if animateScale { - self.buttonNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.25) - } - self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) - } - - let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor - - self.arrowNode.isHidden = state == .cancel - self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Close : self.strings.Common_Back, font: Font.regular(17.0), textColor: color) - - let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0)) - - self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height)) - self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color) - if let image = self.arrowNode.image { - self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size) - } - self.labelNode.frame = CGRect(origin: self.labelNode.frame.origin, size: labelSize) - self.buttonNode.subnodeTransform = CATransform3DMakeTranslation(state == .back ? 11.0 : 0.0, 0.0, 0.0) - } - - override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: self.buttonNode.frame.width, height: constrainedSize.height)) - self.arrowNode.frame = CGRect(origin: CGPoint(x: -19.0, y: floorToScreenPixels((constrainedSize.height - self.arrowNode.frame.size.height) / 2.0)), size: self.arrowNode.frame.size) - self.labelNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((constrainedSize.height - self.labelNode.frame.size.height) / 2.0)), size: self.labelNode.frame.size) - - return CGSize(width: 70.0, height: 56.0) - } -} - public struct WebAppParameters { public enum Source { case generic @@ -299,12 +148,13 @@ public final class WebAppController: ViewController, AttachmentContainable { private var queryId: Int64? fileprivate let canMinimize = true - private var placeholderDisposable: Disposable? - private var iconDisposable: Disposable? + private var placeholderDisposable = MetaDisposable() private var keepAliveDisposable: Disposable? - private var paymentDisposable: Disposable? + private var iconDisposable: Disposable? + fileprivate var icon: UIImage? + private var lastExpansionTimestamp: Double? private var didTransitionIn = false @@ -395,7 +245,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - self.placeholderDisposable = (placeholder + self.placeholderDisposable.set((placeholder |> deliverOnMainQueue).start(next: { [weak self] fileReferenceAndIsPlaceholder in guard let strongSelf = self else { return @@ -413,7 +263,7 @@ public final class WebAppController: ViewController, AttachmentContainable { if let fileReference = fileReference { let _ = freeMediaFileInteractiveFetched(account: strongSelf.context.account, userLocation: .other, fileReference: fileReference).start() } - strongSelf.iconDisposable = (svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder) + strongSelf.placeholderDisposable.set((svgIconImageFile(account: strongSelf.context.account, fileReference: fileReference, stickToTop: isPlaceholder) |> deliverOnMainQueue).start(next: { [weak self] transform in if let strongSelf = self { let imageSize: CGSize @@ -433,13 +283,27 @@ public final class WebAppController: ViewController, AttachmentContainable { } strongSelf.placeholderNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } - }) + })) + })) + + self.iconDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: controller.botId)) + |> mapToSignal { peer -> Signal in + guard let peer else { + return .complete() + } + return peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 32.0, height: 32.0), round: false) + } + |> deliverOnMainQueue).start(next: { [weak self] icon in + guard let self else { + return + } + self.icon = icon }) } deinit { - self.placeholderDisposable?.dispose() self.iconDisposable?.dispose() + self.placeholderDisposable.dispose() self.keepAliveDisposable?.dispose() self.paymentDisposable?.dispose() @@ -2240,6 +2104,10 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate var canMinimize: Bool { return self.controllerNode.canMinimize } + + public var minimizedIcon: UIImage? { + return self.controllerNode.icon + } } final class WebAppPickerContext: AttachmentMediaPickerContext { diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 48c4d5dc8f..27e3224a55 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -175,6 +175,10 @@ final class WebAppWebView: WKWebView { fatalError("init(coder:) has not been implemented") } + deinit { + print() + } + override func didMoveToSuperview() { super.didMoveToSuperview()