From 4216ee3933cf3eca15e71a25d387de53dc300cf9 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 13 Jul 2024 18:13:58 +0400 Subject: [PATCH] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 12 + .../Sources/AccountContext.swift | 5 +- .../ContactMultiselectionController.swift | 2 +- .../AccountContext/Sources/Premium.swift | 9 + .../Sources/AttachmentController.swift | 8 +- .../AttachmentUI/Sources/BackButtonNode.swift | 157 ++++ submodules/BrowserUI/BUILD | 8 + .../BrowserUI/Sources/BrowserContent.swift | 91 ++- .../Sources/BrowserInstantPageContent.swift | 697 +++++++++++++----- .../BrowserNavigationBarComponent.swift | 2 +- .../BrowserUI/Sources/BrowserScreen.swift | 388 ++++++++-- .../Sources/BrowserToolbarComponent.swift | 51 +- .../BrowserUI/Sources/BrowserWebContent.swift | 290 +++++++- submodules/BrowserUI/Sources/Favicon.swift | 38 + .../Source/Components/Button.swift | 10 +- .../Sources/ReactionButtonListComponent.swift | 42 +- .../Sources/ContactListNode.swift | 10 +- .../Navigation/MinimizedContainer.swift | 5 + submodules/Display/Source/UIKitUtils.swift | 10 + .../Sources/DrawingEntitiesView.swift | 13 + .../Sources/DrawingWeatherEntityView.swift | 643 ++++++++++++++++ .../Sources/InAppPurchaseManager.swift | 19 +- .../InstantPageGalleryController.swift | 2 +- .../Sources/InstantPageImageItem.swift | 2 +- .../Sources/InstantPageMediaPlaylist.swift | 22 +- .../InstantPagePeerReferenceNode.swift | 14 +- .../InstantPagePlayableVideoItem.swift | 2 +- .../Sources/InstantPageTheme.swift | 2 +- .../Sources/InvisibleInkDustNode.swift | 16 +- .../Sources/MediaDustNode.swift | 14 +- .../Sources/ItemListVenueItem.swift | 19 +- .../LocationUI/Sources/LocationMapNode.swift | 1 + .../Sources/LocationPickerController.swift | 8 +- .../LocationPickerControllerNode.swift | 23 +- .../Sources/LocationSearchContainerNode.swift | 46 +- submodules/MediaPickerUI/BUILD | 1 + .../Sources/MediaPickerScreen.swift | 162 +++- .../Sources/MediaPickerTitleView.swift | 31 +- submodules/PremiumUI/Resources/gift | Bin 21826 -> 0 bytes submodules/PremiumUI/Resources/gift2.scn | Bin 0 -> 69788 bytes submodules/PremiumUI/Resources/star | Bin 59131 -> 0 bytes submodules/PremiumUI/Resources/star2 | Bin 0 -> 67438 bytes submodules/PremiumUI/Resources/star2.scn | Bin 145346 -> 0 bytes .../PremiumUI/Sources/PremiumGiftScreen.swift | 3 +- .../Sources/ChannelStatsController.swift | 13 +- submodules/Svg/PublicHeaders/Svg/Svg.h | 2 +- submodules/Svg/Sources/Svg.m | 32 +- .../Statistics/RevenueStatistics.swift | 88 ++- .../SyncCore/SyncCore_Namespaces.swift | 1 + .../PresentationResourcesSettings.swift | 1 + .../Sources/ServiceMessageStrings.swift | 50 ++ .../Components/Ads/AdsReportScreen/BUILD | 1 + .../Sources/AdsReportScreen.swift | 295 +------- .../Sources/ChatMessageBubbleItemNode.swift | 2 + .../ChatMessageGiftBubbleContentNode.swift | 10 + .../Sources/ChatSendStarsScreen.swift | 334 ++++++++- .../Sources/EmojiTextAttachmentView.swift | 18 +- .../Sources/EntityKeyboard.swift | 2 +- .../Drawing/CodableDrawingEntity.swift | 19 + .../Drawing/DrawingWeatherEntity.swift | 182 +++++ .../Sources/MediaEditorScreen.swift | 69 +- .../Sources/StickerCutoutOutlineView.swift | 12 +- .../Sources/MinimizedContainer.swift | 13 +- .../Sources/MinimizedHeaderNode.swift | 35 +- .../Components/NavigationStackComponent/BUILD | 21 + .../Sources/NavigationStackComponent.swift | 297 ++++++++ .../Sources/EmojiSelectionComponent.swift | 2 +- .../Components/PeerInfo/PeerInfoScreen/BUILD | 1 + .../ListItems/PeerInfoScreenActionItem.swift | 3 +- .../ListItems/PeerInfoScreenAddressItem.swift | 2 +- .../PeerInfoScreenBirthdatePickerItem.swift | 2 +- .../PeerInfoScreenBusinessHoursItem.swift | 3 +- .../PeerInfoScreenCallListItem.swift | 2 +- .../ListItems/PeerInfoScreenCommentItem.swift | 3 +- .../PeerInfoScreenContactInfoItem.swift | 2 +- ...nfoScreenDisclosureEncryptionKeyItem.swift | 3 +- .../PeerInfoScreenDisclosureItem.swift | 27 +- .../ListItems/PeerInfoScreenHeaderItem.swift | 3 +- .../ListItems/PeerInfoScreenInfoItem.swift | 2 +- .../PeerInfoScreenLabeledValueItem.swift | 13 +- .../ListItems/PeerInfoScreenMemberItem.swift | 2 +- .../PeerInfoScreenPersonalChannelItem.swift | 2 +- .../ListItems/PeerInfoScreenSwitchItem.swift | 3 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 60 +- .../Sources/PeerInfoScreen.swift | 101 ++- .../PeerInfoScreenMultilineInputtem.swift | 3 +- .../Sources/GiftAvatarComponent.swift | 186 ++--- .../Sources/SliderComponent.swift | 16 +- .../Sources/StarsAvatarComponent.swift | 2 +- .../Stars/StarsImageComponent/BUILD | 2 + .../Sources/StarsImageComponent.swift | 36 + .../Sources/StarsPurchaseScreen.swift | 408 +++++----- .../Stars/StarsTransactionScreen/BUILD | 1 + .../Sources/StarsTransactionScreen.swift | 275 ++++--- .../Sources/StarsBalanceComponent.swift | 29 +- .../StarsTransactionsListPanelComponent.swift | 18 +- .../Sources/StarsTransactionsScreen.swift | 100 ++- .../Sources/StarsTransferScreen.swift | 11 +- .../Sources/StickerPickerScreen.swift | 64 +- .../Sources/VideoMessageCameraScreen.swift | 197 ++++- .../VideoMessageFlash.imageset/Contents.json | 12 + .../VideoMessageFlash.imageset/flash.pdf | Bin 0 -> 1549 bytes .../ReadingList.imageset/Contents.json | 12 + .../ReadingList.imageset/readinglist.pdf | Bin 0 -> 1189 bytes .../NewTab.imageset/Contents.json | 12 + .../Instant View/NewTab.imageset/newtab.pdf | Bin 0 -> 2549 bytes .../Location/GoTo.imageset/Contents.json | 12 + .../Location/GoTo.imageset/goto.pdf | Bin 0 -> 1739 bytes .../Premium/Stars/Gift.imageset/Contents.json | 12 + .../Stars/Gift.imageset/giftstars_24.pdf | Bin 0 -> 9544 bytes .../Menu/Balance.imageset/Contents.json | 12 + .../Menu/Balance.imageset/balance_30.pdf | Bin 0 -> 3506 bytes .../ChatControllerOpenLinkContextMenu.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 4 + ...rollerOpenMessageReactionContextMenu.swift | 10 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../TelegramUI/Sources/ChatThemeScreen.swift | 2 +- .../ContactMultiselectionController.swift | 6 +- .../ContactMultiselectionControllerNode.swift | 10 +- .../Sources/FetchCachedRepresentations.swift | 4 +- .../TelegramUI/Sources/OpenChatMessage.swift | 5 +- .../Sources/SharedAccountContext.swift | 176 +++-- .../Sources/ChatTextInputAttributes.swift | 1 + .../WebUI/Sources/WebAppController.swift | 186 +---- submodules/WebUI/Sources/WebAppWebView.swift | 4 + 125 files changed, 4969 insertions(+), 1474 deletions(-) create mode 100644 submodules/AttachmentUI/Sources/BackButtonNode.swift create mode 100644 submodules/BrowserUI/Sources/Favicon.swift create mode 100644 submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift delete mode 100644 submodules/PremiumUI/Resources/gift create mode 100644 submodules/PremiumUI/Resources/gift2.scn delete mode 100644 submodules/PremiumUI/Resources/star create mode 100644 submodules/PremiumUI/Resources/star2 delete mode 100644 submodules/PremiumUI/Resources/star2.scn create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingWeatherEntity.swift create mode 100644 submodules/TelegramUI/Components/NavigationStackComponent/BUILD create mode 100644 submodules/TelegramUI/Components/NavigationStackComponent/Sources/NavigationStackComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Camera/VideoMessageFlash.imageset/flash.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/ReadingList.imageset/readinglist.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Instant View/NewTab.imageset/newtab.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Location/GoTo.imageset/goto.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Gift.imageset/giftstars_24.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Balance.imageset/balance_30.pdf 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 87a9c2a5e8490c3eb9efd5fa2d842144210e5f77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21826 zcmV)&K#ad1iwFo=14zHYfPdh##mxEYGO1RQ%sDpCHkM;zOZ4ln_xKo#r?Ghr6& z0SjSwSOoilj&KYlp%spWFTrteJe&Y0!k6JBI2lfXQ{gM19c&M$!DVncY=kS|O87c_ z1FnK^!qxB{xDjrJhu~rO5&Re)fuF#m@KbmU9)~C3N%$E&1y93E@JskJkifej7XAVs z!p98A;4xwu`HWtS0!D8}A4XqBA)|=Vk5SC%<d{U<_i6WQ<~rXG~%&V!Xy!$#|2o zlkqO&0OKIz9OFFWOU8A^eZ~XELyUp3F+WU*MPMDVPFMn#f+;W!mX3A7x?1Gq*5zGxsx3GCyNpWL{zZ#Jt0N#C*c?V6|oiutHc;Rst)9)t!~g%479n z6|hQK6|AwW@vJGVsjOF7^H|GRZ?e|1HnGmIZnC~%{lxl(&1Li09&B%RTXr;C%UfaZRYLb?d2Wh-QxYmd(3C^`TQV$ zFh7#to}bKD@-_S{zL7thKZn1Vzns5`|1N(o|2Y2y|2+Q^|2F?e{saDRxEt<{`{6;j z7?nXaduS`)^oU1yR8)Sg)?90^s*lcC-b-Jt>(gWbOWcJmH$z)pW6-6c zGHd&yGSW;2lew_MtgY7d)Yj_y>5N15W|OfRu~uj?)tSq51wC^LEmTcZ=>yOu8OCyZ zr6kh|SBcaB0In!~H~=IgTzMPN8`lb#S>by*wj)wW{S60An>%!Ox57Q)s_wR9D}2KW z_nrpvs9QUNP9Om!f+UcPI*Hd LhK^jPZHoI^4{9Jkp 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 E0pn~p82|tP literal 0 HcmV?d00001 diff --git a/submodules/PremiumUI/Resources/star b/submodules/PremiumUI/Resources/star deleted file mode 100644 index 3b27e0220e6c6fa2b28a4e5fd665c0c47182bc7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59131 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 R2G>sNIUJ}9kwRZ1`!7e_S!@6R diff --git a/submodules/PremiumUI/Resources/star2 b/submodules/PremiumUI/Resources/star2 new file mode 100644 index 0000000000000000000000000000000000000000..2ab06065543c4b090218d7a82211cec8d9e864ab GIT binary patch literal 67438 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 z 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∋GwVj{ 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 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 0000000000000000000000000000000000000000..cc1baf3f944593e2784a1061c22d3c00f5ef0c84 GIT binary patch literal 1549 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!aK>2)HOJZ;~) zG}keS*T#vPC8)V&LPee3a=yD9RZl;MKDx1A|M}I|{PuSD?eLE#wjYlE&~o*+{+qa*86S@{S9*sk@3>jiaav31gkjeg z!`%-~N4bf=UcN2W(dNgdwCo=pRdeq1q?kNSW}RUr_An**ZN<|wU%oVMXpi`OPu2Zt zWdG!ge+BYu7Ygmn=svP#v)`#tr?qq^&RGAd@A~~4ZzFu$PCc5-zDN9x^6R8_P3ITRUAkW(HdM#&GPAVe^w3iWEQNZxSM>#kyYt87Ux{^N zb=n^~HELyGrg{9olR0xT4G=vG0|8+##@uGwo_Cy?e(Wj zmAxnUrFbrF>u}4c&|xvTS$Bpb^EapBJuS{{mPr$~o5i1bTxy;5<5Usbx31$dE4Rt} z#|B+Wn*Dy|;iCA)2@%I?0;+Ue0-y1(mh#;Gv^FpyZ*#lF@gteuPSTxX2BLB8hEudA z8wzvz1-TXkF1dWDCud@wPT!}i$CI>0W^eJzwLCR{O6KH;Q7s>xO-@AlSbU#jXLyWn zn$w!C0vdS&JS__o!W}2Q&{Avb6x;Pqb$06J33*}517^iCp43d9EW*^vbI~(Dbj}ll z@U08Za`fHrS1D^d{Zex2)_~5dP1nvi`KtML%Dvf?z28>wm-2_W7Jm=xg$A3QROJFb zg!gU_RC^qe(G_yNVa9>2)hi`uhBY%v)|82MY&o$^z$#>N@Pl139x5|mzSc4=sggM1 z@ZLA`+Q&VDf2Hm)d@N-T`s}#w`qk`^hROfcTKJM;FP7)je-nAi&YE48?X-}iVfC7s zSvs?tU##N|?SB2({`mFBZ$AoGA55_{MkX*J1Pf+TP-=00X;E@& zu>z=&0hJD*0>e2!uOu}OXd|eEfe8jA76HW+Orb(Z0188ap@9OJrH}^~G6uR61Q0@&mX>Hjh6YB!&_PjUXlRM9(a^}u0hA^s6`xo` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3b67d5ae016836d3045bbe3e37d92be2722651e1 GIT binary patch literal 1189 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!Z<**sZAp4RU+N7raiO<+#G zv2E(Eyx$xep;~ucRw;e`SPlMyfpFMwg`|*DJ#~+`6*FSze{^7y@$FhG& zYb^UI`_k@5j`HlqGm2lT%L+xW>-;Hke&46Z4@&p2tomrP`rpYTD(u&$%&1B~$=A8C zGtnw*%A6%rxV0uOXj~yEQ?%y7ixcTtSp_R2!<-|uCN4a4R&nc!z>Z>5=j&_O?#^9V z$vf@p1rGm(?t%}e^vif358uixlKpgA*WuZR)LbNzr)52^3ux2#y7c{+!==pHKb~tl42J?yvPY9OM_QU*5j`z>On4zw`t=c83W~C=onkc_~h`x9J}1 zwKFl#F0BnyT6M|dd=^KjPyHOf_7|ttEEcsocVoqb1-7fB7MK-hZoMYzd?EU-_2$EO zlRcx?KD@HPern*Qb6x-Xw`P{4nj4r-x)oO?_Ga_12P^wEe(b#!k<#+iY1!?g(>jku zuUw?^^}>d{r_N_MR=b9mF(-eqo4k4JL*uONznv0-l0&!|=I77TyvY}Br=0Qd?sUU9 zC+^y+6>ga1^@i*F>c-^K!p3aRDXF2wiu7T@Xg6qe%DGjnE7n&`c6 z`G?=Z6`b~m>KVStCxOxwG*Q4(7bv+vQk|)h5j@SAz(s*+QQt8yFTbQ%AvzYC(n2Z= zQWf+A64PNx&o?zCGtnu(LLpkgK+gaS5DX&|m=J;mGbt#wIKQ+gIki{;l=?x*7L<~m z^Ycnl^ME#jl0Hl@Ah8H2reF#cLdqZ@A$Z2o_s&cKI#mJWgCGT%J1K)r?-?k|R9 z6j)%Gp_l~nU@_c6ki#GzcTOxx%*jtj)ml-Mn#N_IV9o{cK8RE>Gc`3fRR9V@fuVr{ zn5B>h7cvIA69f=KW+oV_%*=oRfvU;^XfT?PrGX_}sH7+{Gbgo(3lw{vF2LZ>D9+DK w)l|^POwoh{ihfXjeu)Ce!{7kc56-Mg1-c$wY9tnwfW2dBX2GSZ>gw+X0B@(RGynhq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6c1a20cedfdbccb6ecbe16ab4b6d72fcdf147456 GIT binary patch literal 2549 zcmZWrdpy&7A5VC&=9bEGsUM@runR+v9JybHZ4tRlGrw%ZRx@6@oQzZ&dF(MHU5sZAHUc4`+j{tm-pxQ$NR01cW^RBnOHy|C;$mC zL+B6yu(k$lY#<;bRC0x&|BjnF(IVI&3ovzxAhSUSkirZF#Tnu-#es}4HWk2#pA*H9 zxIz@01(G8nu~Ly)uGQlXDvlyjhbEj{&n)HKg!$c2lP}q!=e>SP^!N^Yu%$KqO&kfU zYH=PHwu1pj%L0d{I;*SZ0ESyfBO?;D3VB)7Ka2o?#w zYg49Q%=Qy{HodRx9U6z$HDo^8CK#Bz8)jS_u8dE1w9#6IQW^pkORbTr$`qra$}Q&a zIsOI$he8q^Jh8|N4c2aO^r(}w-suc$(hNC^-nUp;?0uNfdx74aSN;|O(^Yt-K$nf{ zS?{lPil}f=56>v3U12S#Mx|iQMR+yg-E4{>A%KG`JGL+w(nnXnp(ztCJilWQrL^s3 z?iIoOz9GBBXpBrD1^aLATG>wHlBRA0?eLLg&Wi+P>W2PlLZ1aoR&*bmW~5Cd8aJW5 zQhsYaa^UU$;@HbXYLHcZXCl&Yqv1km)=baDxX>A^5Z1VP=c#l>dMeDFIBU=~sOu?c zc+o0zAK0!k(4f6*d+D|ah5lO|szh_MiOCXz%^?B1^j+y=6YP~o$h3Idla}-h^v&8a zOj%5y@t9}Y9lotqn3AypeklnHIktC1%hjC!pb=qP&l_8(BU)KgROHsBvmBp}m-%$Q$Ydd|U>Rp@NWji~^n6I( zH(#~0dLk;M5v`RuYgN1Ja$(`Ss=PA?bG15E2D;b5M87W!ro2o=9)04PqeO4ul{Xy!giFqoUAu-o zQsg@J)^?^k!aV&t5AI;3D;LH%&E3~(;FREp*I(PV`JE;XOrM&os@UrqLzl6N6`GYM%sGk9v>o>- z#tRmlyj3_u-L8C{O@#OQ4kp@zPRj%1(&KcdmZg5!ywtId9BO92wxxBJ33X$+w;%j7 z`<r8ztB?yaZm9SB=mtgnOV5ZO81n|N5}TWXf|lK@A6z93=INbd3>!_ic4( zBl)&=*R?C+6_@{5J2nz6tCz93F^%HsYdfDf1-YYBU(MlnHLopQf!xWHx8G364wBl< zR!fnaF)Ant@@#ywjSApLxO3j%PWa`JzQN6%$-0C`=1Bv- zGmku<#Un5W1!>(Q@u0D2+lTD@5&a_W6dSUb8)b&~SPTz zaE6ZGb{K)8KP%^wRbgm-udL?Dsj}yyLbXc}Qx5D+S}AU} zv)w71gn#J;FCr<@q*dB<`|Pr``D(kbsJ%g}J50_D{&8$APxsY&GeVUY>C5+G4kcDJcS^r2?<_vleV@M6 z+unH8*BKYRq7}lmx7jtYZR_ZjGw$|nv&h#2_f>Odo7$i&jT{Bfju6FuFT?_Hw0CiI znyhttWcayszi#E5%BPrlwW(i{YTk6u&u3W6H|M9H4(hRa0`*z=mshOlOtRF7fP6GP z#}FR(d17g5X?aOqNAAK=i9&yq*j25Ti0@YoM_F1Tze~8;cUUa#rn?yoCOZlU2>K@6 z-iHr@fGM6FwyNYFU?`1j$BYF60Hg`>OZ+&X%~qiwr`1YCFp3$?qJU9=#7szBfW#Qs zGZ}1b;8;}tGvS8Cav?HK+`Y$Go%wm6`gT|mOAo}`_UQYi5!xJ+v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..39f161b80599f4374911d7fca05fc98b61601fdf GIT binary patch literal 1739 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 literal 0 HcmV?d00001 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()