diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1adeb98195..74ff1cce81 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12299,6 +12299,7 @@ Sorry for the inconvenience."; "Stars.Transfer.PurchasedText" = "You acquired **%1$@** in **%2$@** for **%3$@**."; "Stars.Transfer.Purchased.Stars_1" = "%@ Star"; "Stars.Transfer.Purchased.Stars_any" = "%@ Stars"; +"Stars.Transfer.UnlockInfo" = "Do you want to unlock media for **%1$@**?"; "Stars.Transfer.Balance" = "Balance"; @@ -12325,3 +12326,39 @@ Sorry for the inconvenience."; "HashtagSearch.StoriesFound_1" = "%@ Story Found"; "HashtagSearch.StoriesFound_any" = "%@ Stories Found"; "HashtagSearch.StoriesFoundInfo" = "View stories with %@"; + +"Stars.BotRevenue.Title" = "Stars Balance"; +"Stars.BotRevenue.Revenue.Title" = "Revenue"; +"Stars.BotRevenue.Proceeds.Title" = "Proceeds Overview"; +"Stars.BotRevenue.Proceeds.Available" = "Available Balance"; +"Stars.BotRevenue.Proceeds.Total" = "Total Lifetime Proceeds"; + +"Stars.BotRevenue.Withdraw.Balance" = "Available Balance"; +"Stars.BotRevenue.Withdraw.Withdraw" = "Withdraw via Fragment"; +"Stars.BotRevenue.Withdraw.Info" = "You can withdraw Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()"; + +"Stars.Withdraw.Title" = "Withdraw"; +"Stars.Withdraw.AmountTitle" = "ENTER AMOUNT TO WITHDRAW"; +"Stars.Withdraw.AmountPlaceholder" = "Stars Amount"; +"Stars.Withdraw.Withdraw" = "Withdraw"; + +"Stars.Withdraw.Withdraw.ErrorMinimum" = "You cannot withdraw less than %@"; +"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_1" = "%@ Star"; +"Stars.Withdraw.Withdraw.ErrorMinimum.Stars_any" = "%@ Stars"; + +"Stars.PaidContent.Title" = "Paid Content"; +"Stars.PaidContent.AmountTitle" = "ENTER UNLOCK COST"; +"Stars.PaidContent.AmountPlaceholder" = "Stars to Unlock"; +"Stars.PaidContent.AmountInfo" = "Users will have to transfer this amount of Stars to your channel in order to view this media.\n[More about Stars >]()"; +"Stars.PaidContent.Create" = "Make This Media Paid"; + +"MediaEditor.AddLink" = "LINK"; + +"MediaEditor.Link.CreateTitle" = "Create Link"; +"MediaEditor.Link.EditTitle" = "Edit Link"; +"MediaEditor.Link.LinkTo.Title" = "LINK TO"; +"MediaEditor.Link.LinkTo.Placeholder" = "https://somesite.com"; +"MediaEditor.Link.LinkName.Title" = "LINK NAME (OPTIONAL)"; +"MediaEditor.Link.LinkName.Placeholder" = "Enter a Name"; + +"Story.Editor.TooltipLinkPremium" = "Subscribe to [Telegram Premium]() to add links."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index c531adf03a..314c5b73cc 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1051,7 +1051,8 @@ public protocol SharedAccountContext: AnyObject { func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, completion: @escaping (Bool) -> Void) -> ViewController func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController func makeStarsReceiptScreen(context: AccountContext, receipt: BotPaymentReceipt) -> ViewController - func makeStarsStatisticsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController + func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController + func makeStarsAmountScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController func makeDebugSettingsController(context: AccountContext?) -> ViewController? diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index d847a744d4..fee54b6b28 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -31,6 +31,7 @@ public enum PremiumIntroSource { case storiesExpirationDurations case storiesSuggestedReactions case storiesHigherQuality + case storiesLinks case channelBoost(EnginePeer.Id) case nameColor case similarChannels diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 6c4d3c4f85..898cd2b5fe 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -34,13 +34,13 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { private(set) var dismissProgress: CGFloat = 0.0 var isReadyUpdated: (() -> Void)? var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)? - var interactivelyDismissed: (() -> Void)? + var interactivelyDismissed: (() -> Bool)? var controllerRemoved: ((ViewController) -> Void)? var shouldCancelPanGesture: (() -> Bool)? var requestDismiss: (() -> Void)? - var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)? + var updateModalProgress: ((CGFloat, CGFloat, ContainedViewLayoutTransition) -> Void)? private var isUpdatingState = false private var isDismissed = false @@ -308,8 +308,9 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { var dismissing = false if (bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0)) && !ignoreDismiss { - self.interactivelyDismissed?() - dismissing = true + if self.interactivelyDismissed?() == true { + dismissing = true + } } else if self.isExpanded { if velocity.y > 300.0 || offset > topInset / 2.0 { self.isExpanded = false @@ -436,7 +437,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate { }) let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / defaultTopInset) - self.updateModalProgress?(modalProgress, transition) + self.updateModalProgress?(modalProgress, topInset, transition) let containerLayout: ContainerViewLayout let containerFrame: CGRect diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 4043453c91..e2ddf47415 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -232,7 +232,7 @@ public class AttachmentController: ViewController { private final class Node: ASDisplayNode { private weak var controller: AttachmentController? - private let dim: ASDisplayNode + fileprivate let dim: ASDisplayNode private let shadowNode: ASImageNode fileprivate let container: AttachmentContainer private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? @@ -305,6 +305,8 @@ public class AttachmentController: ViewController { private let wrapperNode: ASDisplayNode + private var isMinimizing = false + init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.controller = controller self.makeEntityInputView = makeEntityInputView @@ -327,6 +329,8 @@ public class AttachmentController: ViewController { super.init() + self.clipsToBounds = false + self.addSubnode(self.dim) self.addSubnode(self.shadowNode) self.addSubnode(self.wrapperNode) @@ -338,16 +342,20 @@ public class AttachmentController: ViewController { } } - self.container.updateModalProgress = { [weak self] progress, transition in + self.container.updateModalProgress = { [weak self] progress, topInset, transition in if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing { var transition = transition if strongSelf.container.supernode == nil { transition = .animated(duration: 0.4, curve: .spring) } - strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) strongSelf.modalProgress = progress - strongSelf.containerLayoutUpdated(layout, transition: transition) + strongSelf.controller?.modalTopEdgeOffset = topInset + + if !strongSelf.isMinimizing { + strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) + strongSelf.containerLayoutUpdated(layout, transition: transition) + } } } self.container.isReadyUpdated = { [weak self] in @@ -358,8 +366,20 @@ public class AttachmentController: ViewController { self.container.interactivelyDismissed = { [weak self] in if let strongSelf = self { - strongSelf.controller?.dismiss(animated: true) + if let controller = strongSelf.controller, controller.shouldMinimizeOnSwipe?() == true, let navigationController = controller.navigationController as? NavigationController { + navigationController.minimizeViewController(controller, animated: true) + + Queue.mainQueue().after(0.5) { + strongSelf.isMinimizing = true + strongSelf.container.update(isExpanded: true, transition: .immediate) + strongSelf.isMinimizing = false + } + return false + } else { + strongSelf.controller?.dismiss(animated: true) + } } + return true } self.container.isPanningUpdated = { [weak self] value in @@ -798,7 +818,7 @@ public class AttachmentController: ViewController { return } - transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) + transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 2.0))) let fromMenu = controller.fromMenu @@ -871,7 +891,7 @@ public class AttachmentController: ViewController { self.wrapperNode.view.mask = nil } - + var containerInsets = containerLayout.intrinsicInsets var hasPanel = false let previousHasButton = self.hasButton @@ -982,6 +1002,8 @@ public class AttachmentController: ViewController { public var getSourceRect: (() -> CGRect?)? + public var shouldMinimizeOnSwipe: (() -> Bool)? + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -1016,6 +1038,13 @@ public class AttachmentController: ViewController { return self.buttons.contains(.standalone) } + public override var isMinimized: Bool { + didSet { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.node.dim, alpha: self.isMinimized ? 0.0 : 1.0) + } + } + public func updateSelectionCount(_ count: Int) { self.node.updateSelectionCount(count, animated: false) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index dc1795ee3a..d0c288876a 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -31,7 +31,7 @@ private enum InnerState: Equatable { public final class AuthorizationSequenceController: NavigationController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { static func navigationBarTheme(_ theme: PresentationTheme) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) + return NavigationBarTheme(buttonColor: theme.intro.accentTextColor, disabledButtonColor: theme.intro.disabledTextColor, primaryTextColor: theme.intro.primaryTextColor, backgroundColor: .clear, opaqueBackgroundColor: .clear, enableBackgroundBlur: false, separatorColor: .clear, badgeBackgroundColor: theme.rootController.navigationBar.badgeBackgroundColor, badgeStrokeColor: theme.rootController.navigationBar.badgeStrokeColor, badgeTextColor: theme.rootController.navigationBar.badgeTextColor) } private let sharedContext: SharedAccountContext diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift index 04b7ea9619..988398d999 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutController.swift @@ -17,7 +17,7 @@ public final class BotCheckoutController: ViewController { public let validatedFormInfo: BotPaymentValidatedFormInfo? public let botPeer: EnginePeer? - private init( + public init( form: BotPaymentForm, validatedFormInfo: BotPaymentValidatedFormInfo?, botPeer: EnginePeer? diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 4cfac4cabe..67bc7a1af1 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -104,6 +104,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/BirthdayPickerScreen", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 472b4961a4..d1e8f5ac17 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -50,6 +50,7 @@ import FullScreenEffectView import PeerInfoStoryGridScreen import ArchiveInfoScreen import BirthdayPickerScreen +import OldChannelsController private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 73f260438a..83030931d6 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -374,7 +374,7 @@ public final class SheetComponent: Component { transition.setFrame(view: effectView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) } } else { - transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) + transition.setFrame(view: contentView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 100.0)), completion: nil) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) if let effectView = self.effectView { transition.setFrame(view: effectView, frame: CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height + 1000.0)), completion: nil) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index 365152e825..9fcf1c5682 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -180,6 +180,15 @@ open class NavigationController: UINavigationController, ContainableController, } } + private var _minimizedViewControllers: [ViewController] = [] + open var minimizedViewControllers: [UIViewController] { + get { + return self._minimizedViewControllers.map { $0 as UIViewController } + } set(value) { + self.setMinimizedViewControllers(value, animated: false) + } + } + private var _viewControllersPromise = ValuePromise<[UIViewController]>() public var viewControllersSignal: Signal<[UIViewController], NoError> { return _viewControllersPromise.get() @@ -466,7 +475,7 @@ open class NavigationController: UINavigationController, ContainableController, transition.updateFrame(node: globalOverlayContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size)) } - let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers) + let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers, minimizedControllers: self._minimizedViewControllers) var transition = transition var statusBarStyle: StatusBarStyle = .Ignore @@ -488,8 +497,9 @@ open class NavigationController: UINavigationController, ContainableController, let modalContainer: NavigationModalContainer if let existingModalContainer = existingModalContainer { modalContainer = existingModalContainer + modalContainer.isMinimized = navigationLayout.modal[i].isMinimized } else { - modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, controllerRemoved: { [weak self] controller in + modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, isMinimized: navigationLayout.modal[i].isMinimized, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) }) modalContainer.container.statusBarStyleUpdated = { [weak self] transition in @@ -524,6 +534,32 @@ open class NavigationController: UINavigationController, ContainableController, strongSelf.setViewControllers(controllers, animated: false) strongSelf.ignoreInputHeight = false } + modalContainer.minimizedRequestMaximize = { [weak self] in + guard let self else { + return + } + + var controllers = self._viewControllers + for controller in self._minimizedViewControllers { + controllers.append(controller) + } + self._viewControllers = controllers + self._minimizedViewControllers = [] + + self.updateContainersNonReentrant(transition: .animated(duration: 0.5, curve: .spring)) + } + modalContainer.minimizedRequestDismiss = { [weak self, weak modalContainer] animated in + guard let self, let modalContainer else { + return + } + + let minimizedControllers = self.minimizedViewControllers.filter { controller in + return !modalContainer.container.controllers.contains(where: { $0 === controller }) + } + if minimizedControllers.count != self.minimizedViewControllers.count { + self.setMinimizedViewControllers(minimizedControllers, animated: animated) + } + } } modalContainers.append(modalContainer) } @@ -695,6 +731,7 @@ open class NavigationController: UINavigationController, ContainableController, var topVisibleModalContainerWithStatusBar: NavigationModalContainer? var visibleModalCount = 0 var topModalIsFlat = false + var topModalIsMinimized = false var topFlatModalHasProgress = false let isLandscape = layout.orientation == .landscape var hasVisibleStandaloneModal = false @@ -754,13 +791,16 @@ open class NavigationController: UINavigationController, ContainableController, } if modalContainer.supernode != nil { - if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat { + if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat && !modalContainer.isMinimized { visibleModalCount += 1 } if isStandaloneModal { hasVisibleStandaloneModal = true visibleModalCount = 0 } + + topModalIsMinimized = modalContainer.isMinimized + if previousModalContainer == nil { topModalIsFlat = modalContainer.isFlat @@ -825,15 +865,23 @@ open class NavigationController: UINavigationController, ContainableController, if let rootContainer = self.rootContainer { switch rootContainer { case let .flat(flatContainer): - if previousModalContainer == nil { - flatContainer.keyboardViewManager = self.keyboardViewManager - flatContainer.canHaveKeyboardFocus = true - } else { + if let previousModalContainer, !previousModalContainer.isMinimized { flatContainer.keyboardViewManager = nil flatContainer.canHaveKeyboardFocus = false + } else { + flatContainer.keyboardViewManager = self.keyboardViewManager + flatContainer.canHaveKeyboardFocus = true } - transition.updateFrame(node: flatContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) - flatContainer.update(layout: layout, canBeClosed: false, controllers: controllers, transition: transition) + + var updatedSize = layout.size + var updatedIntrinsicInsets = layout.intrinsicInsets + if topModalIsMinimized && (layout.inputHeight ?? 0.0).isZero { + updatedSize.height -= 81.0 + updatedIntrinsicInsets.bottom = 0.0 + } + let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets) + transition.updateFrame(node: flatContainer, frame: CGRect(origin: CGPoint(), size: updatedSize)) + flatContainer.update(layout: updatedLayout, canBeClosed: false, controllers: controllers, transition: transition) case let .split(splitContainer): let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in self?.controllerRemoved(controller) @@ -1384,6 +1432,11 @@ open class NavigationController: UINavigationController, ContainableController, self.setViewControllers(controllers, animated: animated) self.ignoreInputHeight = false } + + let minimizedControllers = self.minimizedViewControllers.filter({ $0 !== controller }) + if minimizedControllers.count != self.minimizedViewControllers.count { + self.setMinimizedViewControllers(minimizedControllers, animated: animated) + } } public func replaceController(_ controller: ViewController, with other: ViewController, animated: Bool) { @@ -1523,6 +1576,29 @@ open class NavigationController: UINavigationController, ContainableController, self._viewControllersPromise.set(self.viewControllers) } + private func setMinimizedViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + self._viewControllers = self._viewControllers.filter { controller in + return !viewControllers.contains(controller) + } + + self._minimizedViewControllers = viewControllers.map { controller in + let controller = controller as! ViewController + controller.navigation_setNavigationController(self) + return controller + } + if let layout = self.validLayout { + self.updateContainers(layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { [weak self] in + self?.notifyAccessibilityScreenChanged() + }) + } + } + + public func minimizeViewController(_ viewController: UIViewController, animated: Bool) { + var controllers = self.minimizedViewControllers + controllers.append(viewController) + self.setMinimizedViewControllers(controllers, animated: animated) + } + public var _keepModalDismissProgress = false public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) { let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in @@ -1786,5 +1862,8 @@ open class NavigationController: UINavigationController, ContainableController, return } transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + if let minimizedModalContainer = self.modalContainers.first(where: { $0.isMinimized }) { + transition.updateTransform(node: minimizedModalContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + } } } diff --git a/submodules/Display/Source/Navigation/NavigationLayout.swift b/submodules/Display/Source/Navigation/NavigationLayout.swift index d31ac9f6c1..9e6d68f47d 100644 --- a/submodules/Display/Source/Navigation/NavigationLayout.swift +++ b/submodules/Display/Source/Navigation/NavigationLayout.swift @@ -12,6 +12,7 @@ struct ModalContainerLayout { var controllers: [ViewController] var isFlat: Bool var isStandalone: Bool + var isMinimized: Bool } struct NavigationLayout { @@ -19,7 +20,7 @@ struct NavigationLayout { var modal: [ModalContainerLayout] } -func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController]) -> NavigationLayout { +func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController], minimizedControllers: [ViewController]) -> NavigationLayout { var rootControllers: [ViewController] = [] var modalStack: [ModalContainerLayout] = [] for controller in controllers { @@ -54,7 +55,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL if requiresModal { controller._presentedInModal = true if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } @@ -64,7 +65,7 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL controller._presentedInModal = true } if modalStack[modalStack.count - 1].isStandalone { - modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone)) + modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, isStandalone: isStandalone, isMinimized: false)) } else { modalStack[modalStack.count - 1].controllers.append(controller) } @@ -73,6 +74,23 @@ func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewL rootControllers.append(controller) } } + + var minimizedModalContainer: ModalContainerLayout? + for controller in minimizedControllers { + controller._presentedInModal = false + if var container = minimizedModalContainer { + container.controllers.append(controller) + minimizedModalContainer = container + } else { + let container = ModalContainerLayout(controllers: [controller], isFlat: false, isStandalone: false, isMinimized: true) + minimizedModalContainer = container + } + } + + if let minimizedModalContainer { + modalStack.insert(minimizedModalContainer, at: 0) + } + let rootLayout: RootNavigationLayout switch mode { case .single: diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 1e7603dcbf..2d5325d8a0 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -4,14 +4,36 @@ import AsyncDisplayKit import SwiftSignalKit import UIKitRuntimeUtils +private let minimizedMask: UIImage? = { + return generateImage(CGSize(width: 22.0, height: 24.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + + let path = UIBezierPath(roundedRect: CGRect(x: 0, y: -10, width: 22, height: 20), cornerRadius: 10) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 12) +}() + final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate { private var theme: NavigationControllerTheme let isFlat: Bool + var isMinimized: Bool + var appliedIsMinimized: Bool = false + private let minimizedFrameNode: ASImageNode private let dim: ASDisplayNode private let scrollNode: ASScrollNode let container: NavigationContainer + private let minimizedBackgroundNode: ASDisplayNode + private let minimizedTitleNode: ImmediateTextNode + private let minimizedCloseButton: HighlightableButtonNode + private var minimizedTitleDisposable: Disposable? + private var panRecognizer: InteractiveTransitionGestureRecognizer? private(set) var isReady: Bool = false @@ -20,6 +42,9 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)? var interactivelyDismissed: ((Bool) -> Void)? + var minimizedRequestDismiss: ((Bool) -> Void)? + var minimizedRequestMaximize: (() -> Void)? + private var isUpdatingState = false private var ignoreScrolling = false private var isDismissed = false @@ -42,9 +67,14 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes } } - init(theme: NavigationControllerTheme, isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) { + init(theme: NavigationControllerTheme, isFlat: Bool, isMinimized: Bool, controllerRemoved: @escaping (ViewController) -> Void) { self.theme = theme self.isFlat = isFlat + self.isMinimized = isMinimized + + self.minimizedFrameNode = ASImageNode() + self.minimizedFrameNode.contentMode = .scaleToFill + self.minimizedFrameNode.image = minimizedMask self.dim = ASDisplayNode() self.dim.alpha = 0.0 @@ -54,12 +84,28 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved) self.container.clipsToBounds = true + self.minimizedBackgroundNode = ASDisplayNode() + self.minimizedBackgroundNode.clipsToBounds = true + self.minimizedBackgroundNode.backgroundColor = theme.navigationBar.opaqueBackgroundColor + + self.minimizedTitleNode = ImmediateTextNode() + + self.minimizedCloseButton = HighlightableButtonNode() + self.minimizedCloseButton.setImage(UIImage(bundleImageName: "Instant View/Close"), for: .normal) + super.init() + self.addSubnode(self.minimizedFrameNode) self.addSubnode(self.dim) self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.container) + self.addSubnode(self.minimizedBackgroundNode) + self.minimizedBackgroundNode.addSubnode(self.minimizedTitleNode) + self.minimizedBackgroundNode.addSubnode(self.minimizedCloseButton) + + self.minimizedCloseButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) + self.isReady = self.container.isReady self.container.isReadyUpdated = { [weak self] in guard let strongSelf = self else { @@ -74,6 +120,11 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes } applySmoothRoundedCorners(self.container.layer) + applySmoothRoundedCorners(self.minimizedBackgroundNode.layer) + } + + deinit { + self.minimizedTitleDisposable?.dispose() } override func didLoad() { @@ -116,26 +167,34 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.view.addGestureRecognizer(panRecognizer) self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } + + self.minimizedBackgroundNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.maximizeTapGesture(_:)))) } public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 { - let translation = gestureRecognizer.velocity(in: gestureRecognizer.view) - if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { - return false + if gestureRecognizer.view === self.minimizedBackgroundNode.view { + return self.isMinimized + } else if !self.isMinimized { + if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 { + let translation = gestureRecognizer.velocity(in: gestureRecognizer.view) + if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 { + return false + } + if translation.x < 4.0 { + return false + } + if self.isDismissed { + return false + } + return true + } else { + return true } - if translation.x < 4.0 { - return false - } - if self.isDismissed { - return false - } - return true } else { return true } } - + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return false } @@ -325,6 +384,8 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.validLayout = layout + let lastControllerUpdated = self.container.controllers.last !== controllers.last + var isStandaloneModal = false if let controller = controllers.first, case .standaloneModal = controller.navigationPresentation { isStandaloneModal = true @@ -357,7 +418,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes if layout.metrics.widthClass == .compact || self.isFlat { self.panRecognizer?.isEnabled = true self.container.clipsToBounds = true - if self.isFlat { + if self.isFlat || self.isMinimized { self.dim.backgroundColor = .clear } else { self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) @@ -366,6 +427,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.container.cornerRadius = 0.0 } else { self.container.cornerRadius = 10.0 + self.minimizedBackgroundNode.cornerRadius = self.container.cornerRadius } if #available(iOS 11.0, *) { @@ -374,6 +436,8 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes } else { self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] } + + self.minimizedBackgroundNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } var topInset: CGFloat @@ -386,10 +450,18 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes containerFrame = unscaledFrame } else { topInset = 10.0 - if self.isFlat { - topInset = 0.0 - } else if let statusBarHeight = layout.statusBarHeight { - topInset += statusBarHeight + + let height: CGFloat + if self.isMinimized { + height = layout.size.height - topInset + topInset = layout.size.height - 78.0 + } else { + if self.isFlat { + topInset = 0.0 + } else if let statusBarHeight = layout.statusBarHeight { + topInset += statusBarHeight + } + height = layout.size.height - topInset } let effectiveStatusBarHeight: CGFloat? @@ -399,7 +471,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes effectiveStatusBarHeight = nil } - containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver) + containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver) let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size) let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition @@ -407,6 +479,59 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) } + + for controller in controllers { + controller.isMinimized = self.isMinimized + } + + if self.isMinimized != self.appliedIsMinimized { + self.appliedIsMinimized = self.isMinimized + + if self.isMinimized { + let modalTopEdgeOffset = (controllers.last?.modalTopEdgeOffset ?? 0.0) + 96.0 + if transition.isAnimated { + self.minimizedBackgroundNode.position = self.minimizedBackgroundNode.position.offsetBy(dx: 0.0, dy: modalTopEdgeOffset) + } + } + } + + transition.updateFrame(node: self.minimizedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: layout.size.width, height: 243.0))) + transition.updateAlpha(node: self.minimizedBackgroundNode, alpha: self.isMinimized ? 1.0 : 0.0) + self.minimizedBackgroundNode.cornerRadius = 10.0 + self.minimizedBackgroundNode.isUserInteractionEnabled = self.isMinimized + + let titleSideInset: CGFloat = 56.0 + if self.isMinimized, let controller = controllers.last { + if lastControllerUpdated || self.minimizedTitleDisposable == nil { + var isFirstUpdate = true + self.minimizedTitleDisposable = (controller.titleSignal + |> deliverOnMainQueue).start(next: { [weak self] title in + guard let self, let layout = self.validLayout else { + return + } + + self.minimizedTitleNode.attributedText = NSAttributedString(string: title ?? "", font: Font.bold(17.0), textColor: self.theme.navigationBar.primaryTextColor) + + if !isFirstUpdate { + let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0)) + self.minimizedTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize) + } else { + isFirstUpdate = false + } + }) + } + } else { + self.minimizedTitleDisposable?.dispose() + self.minimizedTitleDisposable = nil + } + + let titleSize = self.minimizedTitleNode.updateLayout(CGSize(width: layout.size.width - titleSideInset * 2.0, height: 56.0)) + transition.updateFrame(node: self.minimizedTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)) + + transition.updateFrame(node: self.minimizedCloseButton, frame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 46.0, height: 52.0))) + + transition.updateAlpha(node: self.minimizedFrameNode, alpha: self.isMinimized ? 1.0 : 0.0) + transition.updateFrame(node: self.minimizedFrameNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - 81.0 - 10.0), size: CGSize(width: layout.size.width, height: 24.0 + 81.0))) } else { self.panRecognizer?.isEnabled = false if self.isFlat { @@ -485,7 +610,17 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true) - positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in + + let targetY: CGFloat + if self.isMinimized { + let offset: CGFloat = 81.0 + 15.0 + targetY = self.container.position.y + offset + positionTransition.updatePosition(node: self.minimizedBackgroundNode, position: CGPoint(x: self.minimizedBackgroundNode.position.x, y: self.minimizedBackgroundNode.position.y + offset)) + } else { + targetY = self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height + } + + positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: targetY), beginWithCurrentState: true, completion: { [weak self] _ in guard let strongSelf = self else { return } @@ -513,7 +648,14 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes return nil } if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) { - return self.dim.view + if self.isMinimized { + return nil + } else { + return self.dim.view + } + } + if self.isMinimized && result == self.minimizedBackgroundNode.view { + return result } if self.isFlat { return result @@ -568,4 +710,22 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat return result } + + @objc private func closePressed() { + if !self.isDismissed { + self.minimizedRequestDismiss?(true) + } + } + + @objc private func maximizeTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + if !self.isDismissed { + if self.container.controllers.count == 1 { + self.minimizedRequestMaximize?() + } else { + + } + } + } + } } diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 4e71fb78ab..b33274d480 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -64,17 +64,19 @@ public final class NavigationBarTheme { public let disabledButtonColor: UIColor public let primaryTextColor: UIColor public let backgroundColor: UIColor + public let opaqueBackgroundColor: UIColor public let enableBackgroundBlur: Bool public let separatorColor: UIColor public let badgeBackgroundColor: UIColor public let badgeStrokeColor: UIColor public let badgeTextColor: UIColor - public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { + public init(buttonColor: UIColor, disabledButtonColor: UIColor, primaryTextColor: UIColor, backgroundColor: UIColor, opaqueBackgroundColor: UIColor? = nil, enableBackgroundBlur: Bool, separatorColor: UIColor, badgeBackgroundColor: UIColor, badgeStrokeColor: UIColor, badgeTextColor: UIColor) { self.buttonColor = buttonColor self.disabledButtonColor = disabledButtonColor self.primaryTextColor = primaryTextColor self.backgroundColor = backgroundColor + self.opaqueBackgroundColor = opaqueBackgroundColor ?? backgroundColor self.enableBackgroundBlur = enableBackgroundBlur self.separatorColor = separatorColor self.badgeBackgroundColor = badgeBackgroundColor @@ -83,11 +85,11 @@ public final class NavigationBarTheme { } public func withUpdatedBackgroundColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme { - return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) + return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, opaqueBackgroundColor: self.opaqueBackgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor) } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index bb316752f1..c34ef5e3b5 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -230,6 +230,9 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { private var navigationBarOrigin: CGFloat = 0.0 + public var modalTopEdgeOffset: CGFloat = 0.0 + open var isMinimized: Bool = false + open var interactiveNavivationGestureEdgeWidth: InteractiveTransitionGestureRecognizerEdgeWidth? { return nil } @@ -349,6 +352,23 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } } + public var titleSignal: Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + subscriber.putNext(self.navigationItem.title) + let listenerIndex = self.navigationItem.addSetTitleListener { title, _ in + subscriber.putNext(title) + } + return ActionDisposable { [weak self] in + if let self { + self.navigationItem.removeSetTitleListener(listenerIndex) + } + } + } + } + public init(navigationBarPresentationData: NavigationBarPresentationData?) { self.statusBar = StatusBar() if let navigationBarPresentationData = navigationBarPresentationData { diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 876b2c01e9..575a953841 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -28,6 +28,8 @@ private func makeEntityView(context: AccountContext, entity: DrawingEntity) -> D return DrawingMediaEntityView(context: context, entity: entity) } else if let entity = entity as? DrawingLocationEntity { return DrawingLocationEntityView(context: context, entity: entity) + } else if let entity = entity as? DrawingLinkEntity { + return DrawingLinkEntityView(context: context, entity: entity) } else { return nil } @@ -54,6 +56,9 @@ private func prepareForRendering(entityView: DrawingEntityView) { if let entityView = entityView as? DrawingLocationEntityView { entityView.entity.renderImage = entityView.getRenderImage() } + if let entityView = entityView as? DrawingLinkEntityView { + entityView.entity.renderImage = entityView.getRenderImage() + } } public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { @@ -384,6 +389,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { location.width = floor(self.size.width * 0.85) location.scale = zoomScale } + } else if let location = entity as? DrawingLinkEntity { + location.position = center + if setup { + location.rotation = rotation + location.referenceDrawingSize = self.size + location.width = floor(self.size.width * 0.85) + location.scale = zoomScale + } } } diff --git a/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift new file mode 100644 index 0000000000..cc72db2116 --- /dev/null +++ b/submodules/DrawingUI/Sources/DrawingLinkEntityView.swift @@ -0,0 +1,620 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import AccountContext +import TelegramCore +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import StickerResources +import MediaEditor + +private func generateIcon(style: DrawingLinkEntity.Style) -> UIImage? { + guard let image = UIImage(bundleImageName: "Premium/Link") 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: 0x64d2ff) + blue = UIColor(rgb: 0x64d2ff) + } else { + green = UIColor(rgb: 0x0a84ff) + blue = UIColor(rgb: 0x0a84ff) + } + + 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 DrawingLinkEntityView: DrawingEntityView, UITextViewDelegate { + private var linkEntity: DrawingLinkEntity { + return self.entity as! DrawingLinkEntity + } + + let imageView: UIImageView + + let backgroundView: UIView + let blurredBackgroundView: BlurredBackgroundView + + let textView: DrawingTextView + let iconView: UIImageView + private let imageNode: TransformImageNode + + private let cachedDisposable = MetaDisposable() + + init(context: AccountContext, entity: DrawingLinkEntity) { + self.imageView = UIImageView() + + 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.imageView) + self.addSubview(self.backgroundView) + self.addSubview(self.blurredBackgroundView) + self.addSubview(self.textView) + self.addSubview(self.iconView) + + self.update(animated: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var textSize: CGSize = .zero + public override func sizeThatFits(_ size: CGSize) -> CGSize { + if self.linkEntity.webpage != nil, let image = self.linkEntity.renderImage { + self.imageView.frame = CGRect(origin: .zero, size: image.size) + return image.size + } else { + var result = self.textView.sizeThatFits(CGSize(width: self.linkEntity.width, height: .greatestFiniteMagnitude)) + self.textSize = result + + let 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 + 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: DrawingLinkEntity.Style + if self.linkEntity.webpage != nil { + switch self.linkEntity.style { + case .white: + updatedStyle = .black + default: + updatedStyle = .white + } + } else { + switch self.linkEntity.style { + case .white: + updatedStyle = .black + case .black: + updatedStyle = .transparent + case .transparent: + if self.linkEntity.hasCustomColor { + updatedStyle = .custom + } else { + updatedStyle = .white + } + case .custom: + updatedStyle = .white + case .blur: + updatedStyle = .white + } + } + self.linkEntity.style = updatedStyle + + self.update() + + return true + } + + private var displayFontSize: CGFloat { + var textFontSize: CGFloat = 0.07 + let textLength = self.linkEntity.url.count + if textLength > 10 { + textFontSize = max(0.01, 0.07 - CGFloat(textLength - 10) / 100.0) + } + + let minFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.025) + let maxFontSize = max(10.0, max(self.linkEntity.referenceDrawingSize.width, self.linkEntity.referenceDrawingSize.height) * 0.25) + let fontSize = minFontSize + (maxFontSize - minFontSize) * textFontSize + return fontSize + } + + private func updateText() { + let string: String + if !self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + string = self.linkEntity.name.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + } else { + string = self.linkEntity.url.uppercased() + } + let text = NSMutableAttributedString(string: string) + 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.linkEntity.style { + case .white: + textColor = UIColor(rgb: 0x0a84ff) + case .black, .blur: + textColor = UIColor(rgb: 0x64d2ff) + case .transparent: + textColor = .white + case .custom: + let color = self.linkEntity.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: DrawingLinkEntity.Style? + public override func update(animated: Bool = false) { + self.center = self.linkEntity.position + self.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(self.linkEntity.rotation), self.linkEntity.scale, self.linkEntity.scale) + + if self.linkEntity.webpage != nil { + self.textView.isHidden = true + self.backgroundView.isHidden = true + self.blurredBackgroundView.isHidden = true + self.iconView.isHidden = true + + self.imageView.image = self.linkEntity.style == .white ? self.linkEntity.renderImage : self.linkEntity.secondaryRenderImage + } else { + self.textView.isHidden = false + self.textView.frameInsets = UIEdgeInsets(top: 0.15, left: 0.0, bottom: 0.15, right: 0.0) + switch self.linkEntity.style { + case .white: + self.textView.textColor = UIColor(rgb: 0x0a84ff) + self.backgroundView.backgroundColor = .white + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true + case .black: + self.textView.textColor = UIColor(rgb: 0x64d2ff) + 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.linkEntity.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.iconView.isHidden = false + if self.currentStyle != self.linkEntity.style { + self.currentStyle = self.linkEntity.style + self.iconView.image = generateIcon(style: self.linkEntity.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 + } + } + + self.sizeToFit() + + super.update(animated: animated) + } + + override func updateSelectionView() { + guard let selectionView = self.selectionView as? DrawingLinkEntitySelectionView 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.linkEntity.scale) * scale + selectionView.selectionInset * 2.0, height: (bounds.height * self.linkEntity.scale) * scale + selectionView.selectionInset * 2.0)) + selectionView.transform = CGAffineTransformMakeRotation(self.linkEntity.rotation) + + self.popIdentityTransformForMeasurement() + } + + override func makeSelectionView() -> DrawingEntitySelectionView? { + if let selectionView = self.selectionView { + return selectionView + } + let selectionView = DrawingLinkEntitySelectionView() + 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 DrawingLinkEntitySelectionView: 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? DrawingLinkEntity 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? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity 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? DrawingLinkEntityView, let entity = entityView.entity as? DrawingLinkEntity 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/DrawingUI/Sources/DrawingLocationEntityView.swift b/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift index 3ed974a97c..fae19f3b7d 100644 --- a/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingLocationEntityView.swift @@ -345,7 +345,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingLocationEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingLocationEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -367,7 +367,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingLocationEntititySelectionView() + let selectionView = DrawingLocationEntitySelectionView() selectionView.entityView = self return selectionView } @@ -386,7 +386,7 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg } } -final class DrawingLocationEntititySelectionView: DrawingEntitySelectionView { +final class DrawingLocationEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index 075cd90b65..43431fefcd 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -3088,15 +3088,12 @@ public final class DrawingToolsInteraction { var isVideo = false var isAdditional = false var isMessage = false - var isLink = false if let entity = entityView.entity as? DrawingStickerEntity { if case let .dualVideoReference(isAdditionalValue) = entity.content { isVideo = true isAdditional = isAdditionalValue } else if case .message = entity.content { isMessage = true - } else if case .link = entity.content { - isLink = true } } @@ -3115,7 +3112,7 @@ public final class DrawingToolsInteraction { } })) } - if entityView is DrawingLocationEntityView || isLink { + if entityView is DrawingLocationEntityView || entityView is DrawingLinkEntityView { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Edit, accessibilityLabel: presentationData.strings.Paint_Edit), action: { [weak self, weak entityView] in if let self, let entityView { self.editEntity(entityView.entity) @@ -3129,7 +3126,7 @@ public final class DrawingToolsInteraction { self.entitiesView.selectEntity(entityView.entity) } })) - } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage && !isLink { + } else if (entityView is DrawingStickerEntityView || entityView is DrawingBubbleEntityView) && !isVideo && !isMessage { actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Paint_Flip, accessibilityLabel: presentationData.strings.Paint_Flip), action: { [weak self] in if let self { self.flipSelectedEntity() diff --git a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift index b2ce0322e9..09d2c47c8c 100644 --- a/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingSimpleShapeEntityView.swift @@ -103,7 +103,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { override func updateSelectionView() { super.updateSelectionView() - guard let selectionView = self.selectionView as? DrawingSimpleShapeEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingSimpleShapeEntitySelectionView else { return } @@ -117,7 +117,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingSimpleShapeEntititySelectionView() + let selectionView = DrawingSimpleShapeEntitySelectionView() selectionView.entityView = self return selectionView } @@ -136,7 +136,7 @@ final class DrawingSimpleShapeEntityView: DrawingEntityView { } } -final class DrawingSimpleShapeEntititySelectionView: DrawingEntitySelectionView { +final class DrawingSimpleShapeEntitySelectionView: DrawingEntitySelectionView { private let leftHandle = SimpleShapeLayer() private let topLeftHandle = SimpleShapeLayer() private let topHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift index afa49a37d3..ae5a1f2e6e 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntityView.swift @@ -142,8 +142,6 @@ public class DrawingStickerEntityView: DrawingEntityView { return image } else if case .message = self.stickerEntity.content { return self.animatedImageView?.image - } else if case .link = self.stickerEntity.content { - return self.animatedImageView?.image } else { return nil } @@ -171,13 +169,6 @@ public class DrawingStickerEntityView: DrawingEntityView { return CGSize(width: 512.0, height: 512.0) case let .message(_, size, _, _, _): return size - case let .link(_, _, _, _, size, compactSize, style): - switch style { - case .white, .black: - return size ?? compactSize - case .whiteCompact, .blackCompact: - return compactSize - } } } @@ -305,10 +296,6 @@ public class DrawingStickerEntityView: DrawingEntityView { if let file, let _ = mediaRect { self.setupWithVideo(file) } - } else if case .link = self.stickerEntity.content { - if let image = self.stickerEntity.renderImage { - self.setupWithImage(image, overlayImage: nil) - } } } @@ -694,37 +681,7 @@ public class DrawingStickerEntityView: DrawingEntityView { func onDeselection() { } - - override func selectedTapAction() -> Bool { - if case let .link(url, name, positionBelowText, largeMedia, size, compactSize, style) = self.stickerEntity.content { - 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: DrawingStickerEntity.Content.LinkStyle - switch style { - case .white: - updatedStyle = .black - case .black: - updatedStyle = .whiteCompact - case .whiteCompact: - updatedStyle = .blackCompact - case .blackCompact: - if let _ = size { - updatedStyle = .white - } else { - updatedStyle = .whiteCompact - } - } - self.stickerEntity.content = .link(url, name, positionBelowText, largeMedia, size, compactSize, updatedStyle) - self.animatedImageView?.image = nil - self.update(animated: false) - return true - } else { - return super.selectedTapAction() - } - } - + func innerLayoutSubview(boundingSize: CGSize) -> CGSize { return boundingSize } @@ -744,21 +701,6 @@ public class DrawingStickerEntityView: DrawingEntityView { if let image { self.setupWithImage(image) } - } else if case let .link(_, _, _, _, _, _, style) = self.stickerEntity.content, self.animatedImageView?.image == nil { - let image: UIImage? - switch style { - case .white: - image = self.stickerEntity.renderImage - case .black: - image = self.stickerEntity.secondaryRenderImage - case .whiteCompact: - image = self.stickerEntity.tertiaryRenderImage - case .blackCompact: - image = self.stickerEntity.quaternaryRenderImage - } - if let image { - self.setupWithImage(image) - } } self.updateMirroring(animated: animated) @@ -811,7 +753,7 @@ public class DrawingStickerEntityView: DrawingEntityView { } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingStickerEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingStickerEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -834,7 +776,7 @@ public class DrawingStickerEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingStickerEntititySelectionView() + let selectionView = DrawingStickerEntitySelectionView() selectionView.entityView = self return selectionView } @@ -880,7 +822,7 @@ public class DrawingStickerEntityView: DrawingEntityView { } } -final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { +final class DrawingStickerEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() @@ -1160,14 +1102,6 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { if case .message = entity.content { cornerRadius *= 2.1 count = 24 - } else if case .link = entity.content { - count = 24 - if height > 0.0 && width / height > 5.0 { - height *= 1.6 - } else { - cornerRadius *= 2.1 - height *= 1.2 - } } else if case .image = entity.content { count = 24 } diff --git a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift index 1b89f1a6bc..4e9b833497 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntityView.swift @@ -257,7 +257,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate self.updateEditingPosition(animated: true) - if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + if let selectionView = self.selectionView as? DrawingTextEntitySelectionView { selectionView.alpha = 0.0 if !self.textEntity.text.string.isEmpty { selectionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) @@ -353,7 +353,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } self.update(animated: false) - if let selectionView = self.selectionView as? DrawingTextEntititySelectionView { + if let selectionView = self.selectionView as? DrawingTextEntitySelectionView { selectionView.alpha = 1.0 selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } @@ -587,7 +587,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingTextEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingTextEntitySelectionView else { return } self.pushIdentityTransformForMeasurement() @@ -609,7 +609,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingTextEntititySelectionView() + let selectionView = DrawingTextEntitySelectionView() selectionView.entityView = self return selectionView } @@ -655,7 +655,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } } -final class DrawingTextEntititySelectionView: DrawingEntitySelectionView { +final class DrawingTextEntitySelectionView: DrawingEntitySelectionView { private let border = SimpleShapeLayer() private let leftHandle = SimpleShapeLayer() private let rightHandle = SimpleShapeLayer() diff --git a/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift b/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift index bc5415c2b3..2f5b3ea490 100644 --- a/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingVectorEntityView.swift @@ -59,7 +59,7 @@ final class DrawingVectorEntityView: DrawingEntityView { } override func updateSelectionView() { - guard let selectionView = self.selectionView as? DrawingVectorEntititySelectionView else { + guard let selectionView = self.selectionView as? DrawingVectorEntitySelectionView else { return } @@ -96,7 +96,7 @@ final class DrawingVectorEntityView: DrawingEntityView { if let selectionView = self.selectionView { return selectionView } - let selectionView = DrawingVectorEntititySelectionView() + let selectionView = DrawingVectorEntitySelectionView() selectionView.entityView = self return selectionView } @@ -131,7 +131,7 @@ private func midPointPositionFor(start: CGPoint, end: CGPoint, length: CGFloat, return p2 } -final class DrawingVectorEntititySelectionView: DrawingEntitySelectionView { +final class DrawingVectorEntitySelectionView: DrawingEntitySelectionView { private let startHandle = SimpleShapeLayer() private let midHandle = SimpleShapeLayer() private let endHandle = SimpleShapeLayer() diff --git a/submodules/JoinLinkPreviewUI/BUILD b/submodules/JoinLinkPreviewUI/BUILD index 480a72f6fd..aa2d7aae0e 100644 --- a/submodules/JoinLinkPreviewUI/BUILD +++ b/submodules/JoinLinkPreviewUI/BUILD @@ -20,11 +20,11 @@ swift_library( "//submodules/PresentationDataUtils:PresentationDataUtils", "//submodules/ShareController:ShareController", "//submodules/SelectablePeerNode:SelectablePeerNode", - "//submodules/PeerInfoUI:PeerInfoUI", "//submodules/UndoUI:UndoUI", "//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode", "//submodules/ComponentFlow", "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift index 5bbf773208..fde42317a1 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewController.swift @@ -8,8 +8,8 @@ import TelegramPresentationData import AccountContext import AlertUI import PresentationDataUtils -import PeerInfoUI import UndoUI +import OldChannelsController public final class JoinLinkPreviewController: ViewController { private var controllerNode: JoinLinkPreviewControllerNode { diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h index 7871f47ecc..01b5704ddd 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaEditingContext.h @@ -92,6 +92,12 @@ - (void)setSpoiler:(bool)spoiler forItem:(NSObject *)item; - (SSignal *)spoilersUpdatedSignal; +- (NSNumber *)priceForItem:(NSObject *)item; +- (SSignal *)priceSignalForItem:(NSObject *)item; +- (SSignal *)priceSignalForIdentifier:(NSString *)identifier; +- (void)setPrice:(NSNumber *)price forItem:(NSObject *)item; +- (SSignal *)pricesUpdatedSignal; + - (UIImage *)paintingImageForItem:(NSObject *)item; - (UIImage *)stillPaintingImageForItem:(NSObject *)item; - (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video; diff --git a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m index 5578835d54..a9aa9b8e2e 100644 --- a/submodules/LegacyComponents/Sources/TGMediaEditingContext.m +++ b/submodules/LegacyComponents/Sources/TGMediaEditingContext.m @@ -64,6 +64,16 @@ @end +@interface TGMediaPriceUpdate : NSObject + +@property (nonatomic, readonly, strong) id item; +@property (nonatomic, readonly, strong) NSNumber *price; + ++ (instancetype)priceUpdateWithItem:(id)item price:(NSNumber *)price; ++ (instancetype)priceUpdate:(NSNumber *)timer; + +@end + @interface TGModernCache (Private) @@ -81,7 +91,8 @@ NSNumber *_timer; NSMutableDictionary *_spoilers; - + NSMutableDictionary *_prices; + SQueue *_queue; NSMutableDictionary *_temporaryRepCache; @@ -112,6 +123,7 @@ SPipe *_captionPipe; SPipe *_timerPipe; SPipe *_spoilerPipe; + SPipe *_pricePipe; SPipe *_fullSizePipe; SPipe *_cropPipe; @@ -133,6 +145,7 @@ _adjustments = [[NSMutableDictionary alloc] init]; _timers = [[NSMutableDictionary alloc] init]; _spoilers = [[NSMutableDictionary alloc] init]; + _prices = [[NSMutableDictionary alloc] init]; _imageCache = [[TGMemoryImageCache alloc] initWithSoftMemoryLimit:[[self class] imageSoftMemoryLimit] hardMemoryLimit:[[self class] imageHardMemoryLimit]]; @@ -180,6 +193,7 @@ _captionPipe = [[SPipe alloc] init]; _timerPipe = [[SPipe alloc] init]; _spoilerPipe = [[SPipe alloc] init]; + _pricePipe = [[SPipe alloc] init]; _fullSizePipe = [[SPipe alloc] init]; _cropPipe = [[SPipe alloc] init]; } @@ -676,6 +690,74 @@ }]; } +#pragma mark - + +- (NSNumber *)priceForItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return nil; + + return [self _priceForItemId:itemId]; +} + +- (NSNumber *)_priceForItemId:(NSString *)itemId +{ + if (itemId == nil) + return nil; + + return _prices[itemId]; +} + +- (void)setPrice:(NSNumber *)price forItem:(NSObject *)item +{ + NSString *itemId = [self _contextualIdForItemId:item.uniqueIdentifier]; + if (itemId == nil) + return; + + if (price.integerValue != 0) + _prices[itemId] = price; + else + [_prices removeObjectForKey:itemId]; + + _pricePipe.sink([TGMediaPriceUpdate priceUpdateWithItem:item price:price]); +} + +- (SSignal *)priceSignalForItem:(NSObject *)item +{ + SSignal *updateSignal = [[_pricePipe.signalProducer() filter:^bool(TGMediaPriceUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:item.uniqueIdentifier]; + }] map:^NSNumber *(TGMediaPriceUpdate *update) + { + return update.price; + }]; + + return [[SSignal single:[self priceForItem:item]] then:updateSignal]; +} + +- (SSignal *)priceSignalForIdentifier:(NSString *)identifier +{ + SSignal *updateSignal = [[_pricePipe.signalProducer() filter:^bool(TGMediaPriceUpdate *update) + { + return [update.item.uniqueIdentifier isEqualToString:identifier]; + }] map:^NSNumber *(TGMediaPriceUpdate *update) + { + return update.price; + }]; + + return [[SSignal single:[self _priceForItemId:identifier]] then:updateSignal]; +} + +- (SSignal *)pricesUpdatedSignal +{ + return [_pricePipe.signalProducer() map:^id(__unused id value) + { + return @true; + }]; +} + + #pragma mark - - (void)setImage:(UIImage *)image thumbnailImage:(UIImage *)thumbnailImage forItem:(id)item synchronous:(bool)synchronous @@ -1189,3 +1271,23 @@ } @end + + +@implementation TGMediaPriceUpdate + ++ (instancetype)priceUpdateWithItem:(id)item price:(NSNumber *)price +{ + TGMediaPriceUpdate *update = [[TGMediaPriceUpdate alloc] init]; + update->_item = item; + update->_price = price; + return update; +} + ++ (instancetype)priceUpdate:(NSNumber *)price +{ + TGMediaPriceUpdate *update = [[TGMediaPriceUpdate alloc] init]; + update->_price = price; + return update; +} + +@end diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index ddf9101ee6..6cabd1f33b 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -147,7 +147,7 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image, _): self.file = nil self.imagePromise.set(.single(image)) - case .animatedImage, .video, .dualVideoReference, .message, .link: + case .animatedImage, .video, .dualVideoReference, .message: self.file = nil } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 3d829fecb2..2c18e0b3fd 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -134,6 +134,10 @@ final class MediaPickerGridItemNode: GridItemNode { private let spoilerDisposable = MetaDisposable() var spoilerNode: SpoilerOverlayNode? + var priceBackgroundNode: NavigationBackgroundNode? + var priceIconNode: ASImageNode? + var priceLabelNode: ImmediateTextNode? + private let progressDisposable = MetaDisposable() private var currentIsPreviewing = false @@ -545,12 +549,29 @@ final class MediaPickerGridItemNode: GridItemNode { } } - self.spoilerDisposable.set((spoilerSignal - |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in + let priceSignal = Signal { subscriber in + if let signal = editingContext.priceSignal(forIdentifier: asset.localIdentifier) { + let disposable = signal.start(next: { next in + if let next = next as? Int64 { + subscriber.putNext(next) + } + }, error: { _ in + }, completed: nil)! + + return ActionDisposable { + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + + self.spoilerDisposable.set((combineLatest(spoilerSignal, priceSignal) + |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler, price in guard let strongSelf = self else { return } - strongSelf.updateHasSpoiler(hasSpoiler) + strongSelf.updateHasSpoiler(hasSpoiler, price: price) })) if self.currentDraftState != nil { @@ -616,14 +637,14 @@ final class MediaPickerGridItemNode: GridItemNode { } private var didSetupSpoiler = false - private func updateHasSpoiler(_ hasSpoiler: Bool) { + private func updateHasSpoiler(_ hasSpoiler: Bool, price: Int64?) { var animated = true if !self.didSetupSpoiler { animated = false self.didSetupSpoiler = true } - if hasSpoiler { + if hasSpoiler || price != nil { if self.spoilerNode == nil { let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations) self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode) @@ -635,8 +656,45 @@ final class MediaPickerGridItemNode: GridItemNode { spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } - self.spoilerNode?.update(size: self.bounds.size, transition: .immediate) - self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size) + let bounds = self.bounds + self.spoilerNode?.update(size: bounds.size, transition: .immediate) + self.spoilerNode?.frame = CGRect(origin: .zero, size: bounds.size) + + if let price { + let backgroundNode: NavigationBackgroundNode + let labelNode: ImmediateTextNode + let iconNode: ASImageNode + + if let currentBackground = self.priceBackgroundNode, let currentLabel = self.priceLabelNode, let currentIcon = self.priceIconNode { + backgroundNode = currentBackground + labelNode = currentLabel + iconNode = currentIcon + } else { + backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x000000, alpha: 0.5), enableBlur: true) + labelNode = ImmediateTextNode() + iconNode = ASImageNode() + iconNode.displaysAsynchronously = false + iconNode.image = UIImage(bundleImageName: "Premium/Stars/StarSmall") + + if let spoilerNode = self.spoilerNode { + self.insertSubnode(backgroundNode, aboveSubnode: spoilerNode) + } + backgroundNode.addSubnode(labelNode) + backgroundNode.addSubnode(iconNode) + } + labelNode.attributedText = NSAttributedString(string: "\(price)", font: Font.semibold(15.0), textColor: .white) + + let labelSize = labelNode.updateLayout(CGSize(width: 200.0, height: 50.0)) + let size = CGSize(width: labelSize.width + 40.0, height: 34.0) + + backgroundNode.update(size: size, cornerRadius: 17.0, transition: .immediate) + backgroundNode.frame = CGRect(origin: CGPoint(x: floor((bounds.width - size.width) / 2.0), y: floor((bounds.height - size.height) / 2.0)), size: size) + + if let icon = iconNode.image { + iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: floor((size.height - icon.size.height) / 2.0)), size: icon.size) + } + labelNode.frame = CGRect(origin: CGPoint(x: 30.0, y: floor((size.height - labelSize.height) / 2.0)), size: labelSize) + } } else if let spoilerNode = self.spoilerNode { self.spoilerNode = nil spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 4513f5623c..449cf98f7e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2467,31 +2467,43 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { }))) } if selectionCount > 1 { - if !items.isEmpty { - items.append(.separator) - } - items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in - if !grouped { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) + items.append(.action(ContextMenuActionItem(text: "Send Without Grouping", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/GroupingOff"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) - self?.groupedValue = true - }))) - items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in - if grouped { - return nil - } - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) - }, action: { [weak self] _, f in - f(.default) - self?.groupedValue = false + self?.controllerNode.send(asFile: false, silently: false, scheduleTime: nil, animated: true, parameters: nil, completion: {}) }))) + +// if !items.isEmpty { +// items.append(.separator) +// } +// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Grouped, icon: { theme in +// if !grouped { +// return nil +// } +// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) +// }, action: { [weak self] _, f in +// f(.default) +// +// self?.groupedValue = true +// }))) +// items.append(.action(ContextMenuActionItem(text: strings.Attachment_Ungrouped, icon: { theme in +// if grouped { +// return nil +// } +// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) +// }, action: { [weak self] _, f in +// f(.default) +// +// self?.groupedValue = false +// }))) } - if isSpoilerAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) { + + let isPaidAvailable = true + + if isSpoilerAvailable || isPaidAvailable || (selectionCount > 0 && isCaptionAboveMediaAvailable) { if !items.isEmpty { items.append(.separator) } @@ -2532,6 +2544,28 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } }))) } + if isPaidAvailable { + items.append(.action(ContextMenuActionItem(text: "Make This Content Paid", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + + let controller = self.context.sharedContext.makeStarsAmountScreen(context: self.context, completion: { [weak self] amount in + guard let strongSelf = self else { + return + } + if let selectionContext = strongSelf.interaction?.selectionState, let editingContext = strongSelf.interaction?.editingState { + for case let item as TGMediaEditableItem in selectionContext.selectedItems() { + editingContext.setPrice(NSNumber(value: amount), for: item) + } + } + }) + self.parentController()?.push(controller) + }))) + } } return ContextController.Items(content: .list(items)) } diff --git a/submodules/PeerInfoUI/BUILD b/submodules/PeerInfoUI/BUILD index 74a9533af7..4e467a69e5 100644 --- a/submodules/PeerInfoUI/BUILD +++ b/submodules/PeerInfoUI/BUILD @@ -77,6 +77,8 @@ swift_library( "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", "//submodules/TelegramUI/Components/SendInviteLinkScreen", "//submodules/TelegramUI/Components/GroupStickerPackSetupController", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 87ba5d676e..914ed416ac 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -14,6 +14,8 @@ import Emoji import LocalizedPeerData import Markdown import SendInviteLinkScreen +import OwnershipTransferController +import OldChannelsController private let rankMaxLength: Int32 = 16 diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index f7b2f1b24a..bca367086e 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -13,6 +13,7 @@ import AccountContext import AlertUI import PresentationDataUtils import ItemListAvatarAndNameInfoItem +import OldChannelsController private final class ChannelBannedMemberControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift index 0efb5f7960..3e283a4dd9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift @@ -15,6 +15,7 @@ import ItemListPeerItem import ItemListPeerActionItem import ChatListFilterSettingsHeaderItem import UndoUI +import OldChannelsController private final class ChannelDiscussionGroupSetupControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index bdaeb73de9..e26fd8e287 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -18,6 +18,7 @@ import ItemListPeerActionItem import Markdown import UndoUI import Postbox +import OldChannelsController private final class ChannelPermissionsControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift index 87898ed215..4423eb34d5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelVisibilityController.swift @@ -3,6 +3,7 @@ import UIKit import Display import AsyncDisplayKit import SwiftSignalKit +import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences @@ -23,7 +24,8 @@ import UndoUI import QrCodeUI import PremiumUI import TextFormat -import Postbox +import PremiumUI +import OldChannelsController private final class ChannelVisibilityControllerArguments { let context: AccountContext diff --git a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift index 6b49dc70d3..8bf2deb94e 100644 --- a/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift +++ b/submodules/PeerInfoUI/Sources/GroupPreHistorySetupController.swift @@ -8,6 +8,7 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext +import OldChannelsController private final class GroupPreHistorySetupArguments { let toggle: (Bool) -> Void diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift b/submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift similarity index 94% rename from submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift rename to submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift index befb39d4f9..3b9493ef11 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitFooterItem.swift +++ b/submodules/PremiumUI/Sources/IncreaseLimitFooterItem.swift @@ -8,20 +8,20 @@ import PresentationDataUtils import SolidRoundedButtonNode import AppBundle -final class IncreaseLimitFooterItem: ItemListControllerFooterItem { +public final class IncreaseLimitFooterItem: ItemListControllerFooterItem { let theme: PresentationTheme let title: String let colorful: Bool let action: () -> Void - init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) { + public init(theme: PresentationTheme, title: String, colorful: Bool, action: @escaping () -> Void) { self.theme = theme self.title = title self.colorful = colorful self.action = action } - func isEqual(to: ItemListControllerFooterItem) -> Bool { + public func isEqual(to: ItemListControllerFooterItem) -> Bool { if let item = to as? IncreaseLimitFooterItem { return self.theme === item.theme && self.title == item.title && self.colorful == item.colorful } else { @@ -29,7 +29,7 @@ final class IncreaseLimitFooterItem: ItemListControllerFooterItem { } } - func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { + public func node(current: ItemListControllerFooterItemNode?) -> ItemListControllerFooterItemNode { if let current = current as? IncreaseLimitFooterItemNode { current.item = self return current diff --git a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift b/submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift similarity index 90% rename from submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift rename to submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift index fde1773c7a..ff7bcf55f4 100644 --- a/submodules/PeerInfoUI/Sources/IncreaseLimitHeaderItem.swift +++ b/submodules/PremiumUI/Sources/IncreaseLimitHeaderItem.swift @@ -7,11 +7,10 @@ import TelegramPresentationData import ItemListUI import PresentationDataUtils import Markdown -import PremiumUI import ComponentFlow -class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { - enum Icon { +public class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { + public enum Icon { case group case link } @@ -24,9 +23,9 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { let premiumCount: Int32 let text: String let isPremiumDisabled: Bool - let sectionId: ItemListSectionId + public let sectionId: ItemListSectionId - init(theme: PresentationTheme, strings: PresentationStrings, icon: Icon, count: Int32, limit: Int32, premiumCount: Int32, text: String, isPremiumDisabled: Bool, sectionId: ItemListSectionId) { + public init(theme: PresentationTheme, strings: PresentationStrings, icon: Icon, count: Int32, limit: Int32, premiumCount: Int32, text: String, isPremiumDisabled: Bool, sectionId: ItemListSectionId) { self.theme = theme self.strings = strings self.icon = icon @@ -38,7 +37,7 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { self.sectionId = sectionId } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = IncreaseLimitHeaderItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -54,7 +53,7 @@ class IncreaseLimitHeaderItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { guard let nodeValue = node() as? IncreaseLimitHeaderItemNode else { assertionFailure() diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 6c31bd1c3a..db05bcd226 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -242,6 +242,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .storiesLinks: + if case .storiesLinks = rhs { + return true + } else { + return false + } case let .channelBoost(peerId): if case .channelBoost(peerId) = rhs { return true @@ -326,6 +332,7 @@ public enum PremiumSource: Equatable { case storiesFormatting case storiesExpirationDurations case storiesSuggestedReactions + case storiesLinks case storiesHigherQuality case channelBoost(EnginePeer.Id) case nameColor @@ -406,6 +413,8 @@ public enum PremiumSource: Equatable { return "stories__expiration_durations" case .storiesSuggestedReactions: return "stories__suggested_reactions" + case .storiesLinks: + return "stories__links" case .storiesHigherQuality: return "stories__quality" case let .channelBoost(peerId): diff --git a/submodules/StatisticsUI/BUILD b/submodules/StatisticsUI/BUILD index 0d09b77234..d336056b54 100644 --- a/submodules/StatisticsUI/BUILD +++ b/submodules/StatisticsUI/BUILD @@ -47,7 +47,9 @@ swift_library( "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/TelegramNotices", "//submodules/UIKitRuntimeUtils", - "//submodules/PeerInfoUI", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", ], visibility = [ "//visibility:public", diff --git a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift index 77755aa3ec..7664d1e2cd 100644 --- a/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift +++ b/submodules/StatisticsUI/Sources/RevenueWithdrawalController.swift @@ -4,10 +4,10 @@ import SwiftSignalKit import TelegramCore import TelegramPresentationData import PresentationDataUtils -import PeerInfoUI import AccountContext import PasswordSetupUI import Markdown +import OwnershipTransferController func confirmRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } diff --git a/submodules/StatisticsUI/Sources/StatsGraphItem.swift b/submodules/StatisticsUI/Sources/StatsGraphItem.swift index 4a618efcb9..834625e9a6 100644 --- a/submodules/StatisticsUI/Sources/StatsGraphItem.swift +++ b/submodules/StatisticsUI/Sources/StatsGraphItem.swift @@ -10,18 +10,19 @@ import PresentationDataUtils import GraphCore import GraphUI import ActivityIndicator +import ListItemComponentAdaptor -class StatsGraphItem: ListViewItem, ItemListItem { +public final class StatsGraphItem: ListViewItem, ItemListItem, ListItemComponentAdaptor.ItemGenerator { let presentationData: ItemListPresentationData let graph: StatsGraph let type: ChartType let noInitialZoom: Bool let conversionRate: Double let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? - let sectionId: ItemListSectionId + public let sectionId: ItemListSectionId let style: ItemListStyle - init(presentationData: ItemListPresentationData, graph: StatsGraph, type: ChartType, noInitialZoom: Bool = false, conversionRate: Double = 1.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { + public init(presentationData: ItemListPresentationData, graph: StatsGraph, type: ChartType, noInitialZoom: Bool = false, conversionRate: Double = 1.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) { self.presentationData = presentationData self.graph = graph self.type = type @@ -32,7 +33,7 @@ class StatsGraphItem: ListViewItem, ItemListItem { self.style = style } - func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = StatsGraphItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) @@ -48,7 +49,7 @@ class StatsGraphItem: ListViewItem, ItemListItem { } } - func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? StatsGraphItemNode { let makeLayout = nodeValue.asyncLayout() @@ -65,7 +66,36 @@ class StatsGraphItem: ListViewItem, ItemListItem { } } - var selectable: Bool = false + public func item() -> ListViewItem { + return self + } + + public static func ==(lhs: StatsGraphItem, rhs: StatsGraphItem) -> Bool { + if lhs.presentationData !== rhs.presentationData { + return false + } + if lhs.graph != rhs.graph { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.noInitialZoom != rhs.noInitialZoom { + return false + } + if lhs.conversionRate != rhs.conversionRate { + return false + } + if lhs.sectionId != rhs.sectionId { + return false + } + if lhs.style != rhs.style { + return false + } + return true + } + + public var selectable: Bool = false } class StatsGraphItemNode: ListViewItemNode { diff --git a/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift index a3377d99df..28e0cdaa28 100644 --- a/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StarsRevenueStatistics.swift @@ -6,6 +6,7 @@ import MtProtoKit public struct StarsRevenueStats: Equatable { public struct Balances: Equatable { + public let canWithdraw: Bool public let currentBalance: Int64 public let availableBalance: Int64 public let overallRevenue: Int64 @@ -58,8 +59,7 @@ extension StarsRevenueStats.Balances { init(apiStarsRevenueStatus: Api.StarsRevenueStatus) { switch apiStarsRevenueStatus { case let .starsRevenueStatus(flags, currentBalance, availableBalance, overallRevenue): - let _ = flags - self.init(currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue) + self.init(canWithdraw: (flags & (1 << 0)) != 0, currentBalance: currentBalance, availableBalance: availableBalance, overallRevenue: overallRevenue) } } } @@ -109,6 +109,7 @@ private final class StarsRevenueStatsContextImpl { } private let disposable = MetaDisposable() + private let updateDisposable = MetaDisposable() init(account: Account, peerId: PeerId) { assert(Queue.mainQueue().isCurrent()) @@ -124,6 +125,17 @@ private final class StarsRevenueStatsContextImpl { deinit { assert(Queue.mainQueue().isCurrent()) self.disposable.dispose() + self.updateDisposable.dispose() + } + + public func setUpdated(_ f: @escaping () -> Void) { + let peerId = self.peerId + self.updateDisposable.set((account.stateManager.updatedStarsRevenueStatus() + |> deliverOnMainQueue).startStrict(next: { updates in + if let _ = updates[peerId] { + f() + } + })) } fileprivate func load() { @@ -187,6 +199,12 @@ public final class StarsRevenueStatsContext { }) } + public func setUpdated(_ f: @escaping () -> Void) { + self.impl.with { impl in + impl.setUpdated(f) + } + } + public func reload() { self.impl.with { impl in impl.load() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index bb180303ab..0b48aa6634 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -140,11 +140,11 @@ private final class StarsContextImpl { private let disposable = MetaDisposable() private var updateDisposable: Disposable? - init(account: Account, peerId: EnginePeer.Id) { + init(account: Account) { assert(Queue.mainQueue().isCurrent()) self.account = account - self.peerId = peerId + self.peerId = account.peerId self._state = nil self._statePromise.set(.single(nil)) @@ -426,9 +426,9 @@ public final class StarsContext { } } - init(account: Account, peerId: EnginePeer.Id) { + init(account: Account) { self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { - return StarsContextImpl(account: account, peerId: peerId) + return StarsContextImpl(account: account) }) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 9fea23af87..58e2cd2d8c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -70,10 +70,9 @@ public extension TelegramEngine { return _internal_starsTopUpOptions(account: self.account) } - public func peerStarsContext(peerId: EnginePeer.Id) -> StarsContext { - return StarsContext(account: self.account, peerId: peerId) + public func peerStarsContext() -> StarsContext { + return StarsContext(account: self.account) } - public func peerStarsTransactionsContext(subject: StarsTransactionsContext.Subject, mode: StarsTransactionsContext.Mode) -> StarsTransactionsContext { return StarsTransactionsContext(account: self.account, subject: subject, mode: mode) diff --git a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift index f47d7ea5fc..3b76534e4e 100644 --- a/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift +++ b/submodules/TelegramPresentationData/Sources/ComponentsThemes.swift @@ -48,7 +48,7 @@ public extension ToolbarTheme { public extension NavigationBarTheme { convenience init(rootControllerTheme: PresentationTheme, enableBackgroundBlur: Bool = true, hideBackground: Bool = false, hideBadge: Bool = false, hideSeparator: Bool = false) { let theme = rootControllerTheme.rootController.navigationBar - self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) + self.init(buttonColor: theme.buttonColor, disabledButtonColor: theme.disabledButtonColor, primaryTextColor: theme.primaryTextColor, backgroundColor: hideBackground ? .clear : theme.blurredBackgroundColor, opaqueBackgroundColor: hideBackground ? .clear : theme.opaqueBackgroundColor, enableBackgroundBlur: enableBackgroundBlur, separatorColor: hideBackground || hideSeparator ? .clear : theme.separatorColor, badgeBackgroundColor: hideBadge ? .clear : theme.badgeBackgroundColor, badgeStrokeColor: hideBadge ? .clear : theme.badgeStrokeColor, badgeTextColor: hideBadge ? .clear : theme.badgeTextColor) } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index b343de0591..cf7fd66196 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -550,7 +550,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, 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), range: NSRange(range, in: amountAttributedString.string)) + 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) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 34dc1055d9..84ab3ce44d 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -451,6 +451,8 @@ swift_library( "//submodules/TelegramUI/Components/Stars/StarsPurchaseScreen", "//submodules/TelegramUI/Components/Stars/StarsTransferScreen", "//submodules/TelegramUI/Components/Chat/FactCheckAlertController", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ] + select({ "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, "//build-system:ios_sim_arm64": [], diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD index 13dc5b9ce1..4762ea4204 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/BUILD @@ -18,11 +18,11 @@ swift_library( "//submodules/TelegramPresentationData", "//submodules/AlertUI", "//submodules/PresentationDataUtils", - "//submodules/PeerInfoUI", "//submodules/UndoUI", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/Chat/ChatInputPanelNode", "//submodules/AccountContext", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift index 31675f6772..dbc280b66e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatChannelSubscriberInputPanelNode/Sources/ChatChannelSubscriberInputPanelNode.swift @@ -8,11 +8,11 @@ import SwiftSignalKit import TelegramPresentationData import AlertUI import PresentationDataUtils -import PeerInfoUI import UndoUI import ChatPresentationInterfaceState import ChatInputPanelNode import AccountContext +import OldChannelsController private enum SubscriberAction: Equatable { case join diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD index 1e1cb0ffcd..57d0a07d66 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/BUILD @@ -39,6 +39,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", "//submodules/TelegramUI/Components/WallpaperPreviewMedia", + "//submodules/TelegramUI/Components/TextNodeWithEntities", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 17108c48db..1bb2c05bd8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -29,6 +29,7 @@ import ChatMessageDateAndStatusNode import ChatHistoryEntry import ChatMessageItemCommon import WallpaperPreviewMedia +import TextNodeWithEntities private struct FetchControls { let fetch: (Bool) -> Void @@ -213,12 +214,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } } } + private let context: AccountContext + private let blurredImageNode: TransformImageNode private let dustNode: MediaDustNode fileprivate let buttonNode: HighlightTrackingButtonNode private let highlightedBackgroundNode: ASDisplayNode private let iconNode: ASImageNode - private let textNode: ImmediateTextNode + private let textNode: ImmediateTextNodeWithEntities private var maskView: UIView? private var maskLayer: CAShapeLayer? @@ -227,7 +230,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { var isRevealed = false var tapped: () -> Void = {} - init(hasImageOverlay: Bool, icon: Icon, enableAnimations: Bool) { + init(context: AccountContext, hasImageOverlay: Bool, icon: Icon?, enableAnimations: Bool) { + self.context = context + self.blurredImageNode = TransformImageNode() self.blurredImageNode.contentAnimations = [] @@ -244,9 +249,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false - self.iconNode.image = icon.image + self.iconNode.image = icon?.image - self.textNode = ImmediateTextNode() + self.textNode = ImmediateTextNodeWithEntities() self.textNode.isUserInteractionEnabled = false super.init() @@ -366,16 +371,28 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.buttonNode.isHidden = false self.textNode.isHidden = false - self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(14.0), textColor: .white, paragraphAlignment: .center) - let textSize = self.textNode.updateLayout(size) - if let iconSize = self.iconNode.image?.size { - let contentSize = CGSize(width: iconSize.width + textSize.width + spacing + padding * 2.0, height: 32.0) - self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) - self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) - - self.iconNode.frame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) - self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) + self.textNode.arguments = TextNodeWithEntities.Arguments(context: self.context, cache: self.context.animationCache, renderer: self.context.animationRenderer, placeholderColor: .clear, attemptSynchronous: true) + + let string = NSMutableAttributedString(string: text, font: Font.semibold(15.0), textColor: .white) + if let range = string.string.range(of: "⭐️") { + string.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: string.string)) + string.addAttribute(.baselineOffset, value: 0.5, range: NSRange(range, in: string.string)) } + + self.textNode.attributedText = string + let textSize = self.textNode.updateLayout(size) + let iconSize = self.iconNode.image?.size ?? .zero + + var contentSize = CGSize(width: textSize.width + padding * 2.0, height: 32.0) + if iconSize.width > 0.0 { + contentSize.width += iconSize.width + spacing + } + + self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) + self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) + + self.iconNode.frame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) + self.textNode.frame = CGRect(origin: CGPoint(x: contentSize.width - padding - textSize.width, y: floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) } var leftOffset: CGFloat = 0.0 @@ -1819,6 +1836,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr automaticPlayback = false } } + if let media = self.media as? TelegramMediaInvoice { + invoice = media + } var progressRequired = false if let updatingMedia = attributes.updatingMedia, case .update = updatingMedia.media { @@ -2243,9 +2263,12 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr badgeNode.removeFromSupernode() } - var icon: ExtendedMediaOverlayNode.Icon = .lock + var icon: ExtendedMediaOverlayNode.Icon? = .lock var displaySpoiler = false if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia { + if invoice.currency == "XTR" { + icon = nil + } displaySpoiler = true } else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) { displaySpoiler = true @@ -2257,9 +2280,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } if displaySpoiler { - if self.extendedMediaOverlayNode == nil { - let enableAnimations = (self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) && !isPreview - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) + if self.extendedMediaOverlayNode == nil, let context = self.context { + let enableAnimations = context.sharedContext.energyUsageSettings.fullTranslucency && !isPreview + let extendedMediaOverlayNode = ExtendedMediaOverlayNode(context: context, hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) extendedMediaOverlayNode.tapped = { [weak self] in guard let self else { return @@ -2308,6 +2331,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } } + if let invoice, invoice.currency == "XTR" && viewText.isEmpty { + viewText = "Unlock for ⭐️\(invoice.totalAmount)" + } self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) } else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { self.extendedMediaOverlayNode = nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index bedbbf5c46..a2e5ce6965 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -15,6 +15,7 @@ import ChatMessageBubbleContentNode import ChatMessageItemCommon import ChatMessageInteractiveMediaNode import ChatControllerInteraction +import InvisibleInkDustNode public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { override public var supportsMosaic: Bool { @@ -106,7 +107,15 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if selectedMedia == nil { for media in item.message.media { if let telegramImage = media as? TelegramMediaImage { + #if DEBUG + if item.message.text == "#" { + selectedMedia = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: 100, startParam: "", extendedMedia: .preview(dimensions: telegramImage.representations.first?.dimensions ?? PixelDimensions(width: 1, height: 1), immediateThumbnailData: telegramImage.immediateThumbnailData, videoDuration: nil), flags: [], version: 0) + } else { + selectedMedia = telegramImage + } + #else selectedMedia = telegramImage + #endif if shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: telegramImage) { automaticDownload = .full } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 18f78c5b22..80801a2828 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -433,9 +433,11 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.updateTopicInfo(topicInfo: (id, info)) case let .nameColors(colors): self.updateNameColors(colors: colors) - case .stars: - self.updateStars() - self.updateTintColor() + case let .stars(tinted): + self.updateStars(tinted: tinted) + if tinted { + self.updateTintColor() + } } } else if let file = file { self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) @@ -617,8 +619,8 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.contents = image?.cgImage } - private func updateStars() { - self.contents = starImage?.cgImage + private func updateStars(tinted: Bool) { + self.contents = tinted ? tintedStarImage?.cgImage : starImage?.cgImage } private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { @@ -881,7 +883,7 @@ public final class CustomEmojiContainerView: UIView { } } -private let starImage: UIImage? = { +private let tintedStarImage: UIImage? = { generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in context.clear(CGRect(origin: .zero, size: size)) @@ -890,3 +892,14 @@ private let starImage: UIImage? = { } })?.withRenderingMode(.alwaysTemplate) }() + + +private let starImage: UIImage? = { + generateImage(CGSize(width: 32.0, height: 32.0), contextGenerator: { size, context in + 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) + } + })?.withRenderingMode(.alwaysTemplate) +}() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift index 60bb5521f4..81a31995aa 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/CodableDrawingEntity.swift @@ -23,6 +23,7 @@ public enum CodableDrawingEntity: Equatable { case bubble(DrawingBubbleEntity) case vector(DrawingVectorEntity) case location(DrawingLocationEntity) + case link(DrawingLinkEntity) public init?(entity: DrawingEntity) { if let entity = entity as? DrawingStickerEntity { @@ -37,6 +38,8 @@ public enum CodableDrawingEntity: Equatable { self = .vector(entity) } else if let entity = entity as? DrawingLocationEntity { self = .location(entity) + } else if let entity = entity as? DrawingLinkEntity { + self = .link(entity) } else { return nil } @@ -56,6 +59,8 @@ public enum CodableDrawingEntity: Equatable { return entity case let .location(entity): return entity + case let .link(entity): + return entity } } @@ -87,6 +92,11 @@ public enum CodableDrawingEntity: Equatable { size = entitySize rotation = entityRotation scale = entityScale + case let .link(entity): + position = entity.position + size = entity.renderImage?.size + rotation = entity.rotation + scale = entity.scale default: return nil } @@ -122,16 +132,7 @@ public enum CodableDrawingEntity: Equatable { ) ) case let .sticker(entity): - if case let .link(url, _, _, _, _, _, _) = entity.content { - var url = url - if !url.hasPrefix("http://") && !url.hasPrefix("https://") { - url = "https://\(url)" - } - return .link( - coordinates: coordinates, - url: url - ) - } else if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { + if case let .file(_, type) = entity.content, case let .reaction(reaction, style) = type { var flags: MediaArea.ReactionFlags = [] if case .black = style { flags.insert(.isDark) @@ -152,6 +153,15 @@ public enum CodableDrawingEntity: Equatable { } else { return nil } + case let .link(entity): + var url = entity.url + if !url.hasPrefix("http://") && !url.hasPrefix("https://") { + url = "https://\(url)" + } + return .link( + coordinates: coordinates, + url: url + ) default: return nil } @@ -171,6 +181,7 @@ extension CodableDrawingEntity: Codable { case bubble case vector case location + case link } public init(from decoder: Decoder) throws { @@ -189,6 +200,8 @@ extension CodableDrawingEntity: Codable { self = .vector(try container.decode(DrawingVectorEntity.self, forKey: .entity)) case .location: self = .location(try container.decode(DrawingLocationEntity.self, forKey: .entity)) + case .link: + self = .link(try container.decode(DrawingLinkEntity.self, forKey: .entity)) } } @@ -213,6 +226,9 @@ extension CodableDrawingEntity: Codable { case let .location(payload): try container.encode(EntityType.location, forKey: .type) try container.encode(payload, forKey: .entity) + case let .link(payload): + try container.encode(EntityType.link, forKey: .type) + try container.encode(payload, forKey: .entity) } } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift new file mode 100644 index 0000000000..d1dced3944 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLinkEntity.swift @@ -0,0 +1,225 @@ +import Foundation +import UIKit +import Display +import AccountContext +import TextFormat +import Postbox +import TelegramCore + +public final class DrawingLinkEntity: DrawingEntity, Codable { + private enum CodingKeys: String, CodingKey { + case uuid + case url + case name + case webpage + case positionBelowText + case largeMedia + case expandedSize + case style + case color + case hasCustomColor + 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 url: String + public var name: String + public var webpage: TelegramMediaWebpage? + public var positionBelowText: Bool + public var largeMedia: Bool? + public var expandedSize: CGSize? + public var style: Style + + 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 secondaryRenderImage: UIImage? + + public var renderSubEntities: [DrawingEntity]? + + public var isMedia: Bool { + return false + } + + public init( + url: String, + name: String, + webpage: TelegramMediaWebpage?, + positionBelowText: Bool, + largeMedia: Bool?, + style: Style + ) { + self.uuid = UUID() + + self.url = url + self.name = name + self.webpage = webpage + self.positionBelowText = positionBelowText + self.largeMedia = largeMedia + self.style = style + + 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.url = try container.decode(String.self, forKey: .url) + self.name = try container.decode(String.self, forKey: .name) + self.positionBelowText = try container.decode(Bool.self, forKey: .positionBelowText) + self.largeMedia = try container.decodeIfPresent(Bool.self, forKey: .largeMedia) + self.style = try container.decode(Style.self, forKey: .style) + + if let webpageData = try container.decodeIfPresent(Data.self, forKey: .webpage) { + self.webpage = PostboxDecoder(buffer: MemoryBuffer(data: webpageData)).decodeRootObject() as? TelegramMediaWebpage + } else { + self.webpage = nil + } + + self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white) + self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false + + 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.url, forKey: .url) + try container.encode(self.name, forKey: .name) + try container.encode(self.positionBelowText, forKey: .positionBelowText) + try container.encodeIfPresent(self.largeMedia, forKey: .largeMedia) + + if let webpage = self.webpage { + let encoder = PostboxEncoder() + encoder.encodeRootObject(webpage) + let webpageData = encoder.makeData() + try container.encode(webpageData, forKey: .webpage) + } else { + try container.encodeNil(forKey: .webpage) + } + + try container.encode(self.style, forKey: .style) + try container.encode(self.color, forKey: .color) + try container.encode(self.hasCustomColor, forKey: .hasCustomColor) + + 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 = DrawingLinkEntity(url: self.url, name: self.name, webpage: self.webpage, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, style: self.style) + 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? DrawingLinkEntity else { + return false + } + if self.uuid != other.uuid { + return false + } + if self.url != other.url { + return false + } + if self.name != other.name { + return false + } + if self.webpage != other.webpage { + return false + } + if self.positionBelowText != other.positionBelowText { + return false + } + if self.largeMedia != other.largeMedia { + 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/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index cbe92efd82..2ccb866433 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -33,20 +33,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case reaction(MessageReaction.Reaction, ReactionStyle) } - public enum LinkStyle: Int32 { - case white - case black - case whiteCompact - case blackCompact - } - case file(FileMediaReference, FileType) case image(UIImage, ImageType) case animatedImage(Data, UIImage) case video(TelegramMediaFile) case dualVideoReference(Bool) case message([MessageId], CGSize, TelegramMediaFile?, CGRect?, CGFloat?) - case link(String, String, Bool, Bool?, CGSize?, CGSize, LinkStyle) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -86,12 +78,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } - case let .link(lhsUrl, lhsName, lhsPositionBelowText, lhsLargeMedia, lhsSize, lhsCompactSize, lhsStyle): - if case let .link(rhsUrl, rhsName, rhsPositionBelowText, rhsLargeMedia, rhsSize, rhsCompactSize, rhsStyle) = rhs { - return lhsUrl == rhsUrl && lhsName == rhsName && lhsPositionBelowText == rhsPositionBelowText && lhsLargeMedia == rhsLargeMedia && lhsSize == rhsSize && lhsCompactSize == rhsCompactSize && lhsStyle == rhsStyle - } else { - return false - } } } } @@ -112,13 +98,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case messageSize case messageMediaRect case messageMediaCornerRadius - case linkUrl - case linkName - case linkPositionBelowText - case linkLargeMedia - case linkSize - case linkCompactSize - case linkStyle case referenceDrawingSize case position case scale @@ -185,13 +164,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { dimensions = CGSize(width: 512.0, height: 512.0) case let .message(_, size, _, _, _): dimensions = size - case let .link(_, _, _, _, size, compactSize, style): - switch style { - case .white, .black: - dimensions = size ?? compactSize - case .whiteCompact, .blackCompact: - dimensions = compactSize - } } let boundingSize = CGSize(width: size, height: size) @@ -221,8 +193,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true case .message: return !(self.renderSubEntities ?? []).isEmpty - case .link: - return false } } @@ -234,8 +204,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true case .message: return true - case .link: - return true default: return false } @@ -264,18 +232,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.uuid = try container.decode(UUID.self, forKey: .uuid) - if let url = try container.decodeIfPresent(String.self, forKey: .linkUrl) { - let name = try container.decode(String.self, forKey: .linkName) - let positionBelowText = try container.decode(Bool.self, forKey: .linkPositionBelowText) - let largeMedia = try container.decodeIfPresent(Bool.self, forKey: .linkLargeMedia) - let size = try container.decodeIfPresent(CGSize.self, forKey: .linkSize) - let compactSize = try container.decode(CGSize.self, forKey: .linkCompactSize) - var linkStyle: Content.LinkStyle = .white - if let style = try container.decodeIfPresent(Int32.self, forKey: .linkStyle) { - linkStyle = DrawingStickerEntity.Content.LinkStyle(rawValue: style) ?? .white - } - self.content = .link(url, name, positionBelowText, largeMedia, size, compactSize, linkStyle) - } else if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { + if let messageIds = try container.decodeIfPresent([MessageId].self, forKey: .messageIds) { let size = try container.decodeIfPresent(CGSize.self, forKey: .messageSize) ?? .zero let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .messageFile) let mediaRect = try container.decodeIfPresent(CGRect.self, forKey: .messageMediaRect) @@ -386,14 +343,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encodeIfPresent(file, forKey: .messageFile) try container.encodeIfPresent(mediaRect, forKey: .messageMediaRect) try container.encodeIfPresent(mediaCornerRadius, forKey: .messageMediaCornerRadius) - case let .link(link, name, positionBelowText, largeMedia, size, compactSize, style): - try container.encode(link, forKey: .linkUrl) - try container.encode(name, forKey: .linkName) - try container.encode(positionBelowText, forKey: .linkPositionBelowText) - try container.encode(largeMedia, forKey: .linkLargeMedia) - try container.encodeIfPresent(size, forKey: .linkSize) - try container.encode(compactSize, forKey: .linkCompactSize) - try container.encode(style.rawValue, forKey: .linkStyle) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) @@ -473,7 +422,7 @@ public extension UIImage { var images = [UIImage]() var duration = 0.0 - for i in 0.. Void) { - Queue.mainQueue().after(0.1) { + Queue.mainQueue().after(0.066) { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let defaultPresentationData = defaultPresentationData() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 1e3051dc28..b38a45fcad 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -28,6 +28,10 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, angle = -entity.rotation scale = entity.scale position = entity.position + } else if let entity = entity as? DrawingLinkEntity { + angle = -entity.rotation + scale = entity.scale + position = entity.position } else { fatalError() } @@ -84,7 +88,7 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti content = .video(file) case .dualVideoReference: return [] - case .message, .link: + case .message: if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { var entities: [MediaEditorComposerEntity] = [] entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: false)) @@ -118,6 +122,8 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return entities } else if let entity = entity as? DrawingLocationEntity { return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] + } else if let entity = entity as? DrawingLinkEntity { + return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } } return [] diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index e7c6f106a1..a7d31ae2bc 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -62,6 +62,7 @@ swift_library( "//submodules/Components/HierarchyTrackingLayer", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/WebsiteType", + "//submodules/UrlEscaping", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift index c9874c1aa9..a094676e31 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/CreateLinkScreen.swift @@ -21,6 +21,7 @@ import PresentationDataUtils import ListSectionComponent import TelegramStringFormatting import MediaEditor +import UrlEscaping private let linkTag = GenericComponentViewTag() @@ -28,19 +29,22 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let link: CreateLinkScreen.Link? + let isEdit: Bool + let link: String let webpage: TelegramMediaWebpage? let state: CreateLinkSheetComponent.State let dismiss: () -> Void init( context: AccountContext, - link: CreateLinkScreen.Link?, + isEdit: Bool, + link: String, webpage: TelegramMediaWebpage?, state: CreateLinkSheetComponent.State, dismiss: @escaping () -> Void ) { self.context = context + self.isEdit = isEdit self.link = link self.webpage = webpage self.state = state @@ -51,6 +55,9 @@ private final class SheetContent: CombinedComponent { if lhs.context !== rhs.context { return false } + if lhs.isEdit != rhs.isEdit { + return false + } if lhs.link != rhs.link { return false } @@ -111,6 +118,12 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: sideInset + cancelButton.size.width / 2.0, y: contentSize.height + cancelButton.size.height / 2.0)) ) + let explicitLink = explicitUrl(context.component.link) + var isValidLink = false + if isValidUrl(explicitLink) { + isValidLink = true + } + let controller = environment.controller let doneButton = doneButton.update( component: Button( @@ -121,7 +134,7 @@ private final class SheetContent: CombinedComponent { color: state.link.isEmpty ? theme.actionSheet.secondaryTextColor : theme.actionSheet.controlAccentColor ) ), - isEnabled: !state.link.isEmpty, + isEnabled: isValidLink, action: { [weak state] in if let controller = controller() as? CreateLinkScreen { state?.complete(controller: controller) @@ -136,8 +149,9 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - sideInset - doneButton.size.width / 2.0, y: contentSize.height + doneButton.size.height / 2.0)) ) + let title = title.update( - component: Text(text: component.link == nil ? "Create Link" : "Edit Link", font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + component: Text(text: component.isEdit ? strings.MediaEditor_Link_EditTitle : strings.MediaEditor_Link_CreateTitle, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) @@ -180,7 +194,7 @@ private final class SheetContent: CombinedComponent { placeholderColor: theme.list.itemPlaceholderTextColor, text: state.link, link: true, - placeholderText: "https://somesite.com", + placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder, textUpdated: { [weak state] text in state?.link = text state?.updated() @@ -196,7 +210,7 @@ private final class SheetContent: CombinedComponent { theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "LINK TO".uppercased(), + string: strings.MediaEditor_Link_LinkTo_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor )), @@ -223,7 +237,7 @@ private final class SheetContent: CombinedComponent { theme: theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "LINK NAME (OPTIONAL)".uppercased(), + string: strings.MediaEditor_Link_LinkName_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: theme.list.freeTextColor )), @@ -239,7 +253,7 @@ private final class SheetContent: CombinedComponent { placeholderColor: theme.list.itemPlaceholderTextColor, text: state.name, link: false, - placeholderText: "Enter a Name", + placeholderText: strings.MediaEditor_Link_LinkTo_Placeholder, textUpdated: { [weak state] text in state?.name = text } @@ -319,6 +333,7 @@ private final class CreateLinkSheetComponent: CombinedComponent { self.link = link?.url ?? "" self.name = link?.name ?? "" + self.webpage = link?.webpage self.positionBelowText = link?.positionBelowText ?? true self.largeMedia = link?.largeMedia @@ -339,10 +354,7 @@ private final class CreateLinkSheetComponent: CombinedComponent { return } - var link = link - if !link.hasPrefix("http://") && !link.hasPrefix("https://") { - link = "https://\(link)" - } + let link = explicitUrl(link) if self.dismissed { self.dismissed = false @@ -375,10 +387,7 @@ private final class CreateLinkSheetComponent: CombinedComponent { guard let webpage = self.webpage else { return } - var link = self.link - if !link.hasPrefix("http://") && !link.hasPrefix("https://") { - link = "https://\(link)" - } + let link = explicitUrl(self.link) var name: String = self.name if name.isEmpty { name = self.link @@ -399,16 +408,11 @@ private final class CreateLinkSheetComponent: CombinedComponent { } func complete(controller: CreateLinkScreen) { - var link = self.link - if !link.hasPrefix("http://") && !link.hasPrefix("https://") { - link = "https://\(link)" - } - let text = !self.name.isEmpty ? self.name : self.link - var media: [Media] = [] - if let webpage = self.webpage, !self.dismissed { - media = [webpage] + var effectiveMedia: TelegramMediaWebpage? + if let webpage = self.webpage, case .Loaded = webpage.content, !self.dismissed { + effectiveMedia = webpage } var attributes: [MessageAttribute] = [] @@ -418,48 +422,8 @@ private final class CreateLinkSheetComponent: CombinedComponent { } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1)) - let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: text, attributes: attributes, media: effectiveMedia.flatMap { [$0] } ?? [], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) - let whiteString = NSAttributedString(string: text, font: Font.with(size: 36, design: .camera, weight: .semibold), textColor: UIColor(rgb: 0x0a84ff)) - let blackString = NSAttributedString(string: text, font: Font.with(size: 36, design: .camera, weight: .semibold), textColor: UIColor(rgb: 0x64d2ff)) - - let textSize = whiteString.boundingRect(with: CGSize(width: 1000.0, height: 1000.0), context: nil) - - let whiteCompactImage = generateImage(CGSize(width: textSize.width + 64.0, height: floor(textSize.height * 1.2)), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - - context.setFillColor(UIColor.white.cgColor) - context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: textSize.height * 0.2).cgPath) - context.fillPath() - - let inset = floor((size.height - 36.0) / 2.0) - if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Link"), color: UIColor(rgb: 0x0a84ff)) { - context.draw(image.cgImage!, in: CGRect(x: inset, y: inset, width: 36.0, height: 36.0)) - } - - UIGraphicsPushContext(context) - whiteString.draw(at: CGPoint(x: inset + 42.0, y: 2.0)) - UIGraphicsPopContext() - })! - - let blackCompactImage = generateImage(CGSize(width: textSize.width + 64.0, height: floor(textSize.height * 1.2)), rotatedContext: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - - context.setFillColor(UIColor.black.cgColor) - context.addPath(UIBezierPath(roundedRect: bounds, cornerRadius: textSize.height * 0.2).cgPath) - context.fillPath() - - let inset = floor((size.height - 36.0) / 2.0) - if let image = generateTintedImage(image: UIImage(bundleImageName: "Premium/Link"), color: UIColor(rgb: 0x64d2ff)) { - context.draw(image.cgImage!, in: CGRect(x: inset, y: inset, width: 36.0, height: 36.0)) - } - - UIGraphicsPushContext(context) - blackString.draw(at: CGPoint(x: inset + 42.0, y: 2.0)) - UIGraphicsPopContext() - })! let completion = controller.completion let renderer = DrawingMessageRenderer(context: self.context, messages: [message], parentView: controller.view, isLink: true) @@ -468,13 +432,11 @@ private final class CreateLinkSheetComponent: CombinedComponent { CreateLinkScreen.Result( url: self.link, name: self.name, - webpage: !self.dismissed ? self.webpage : nil, + webpage: effectiveMedia, positionBelowText: self.positionBelowText, largeMedia: self.largeMedia, - image: !media.isEmpty ? result.dayImage : nil, - nightImage: !media.isEmpty ? result.nightImage : nil, - compactLightImage: whiteCompactImage, - compactDarkImage: blackCompactImage + image: effectiveMedia != nil ? result.dayImage : nil, + nightImage: effectiveMedia != nil ? result.nightImage : nil ) ) }) @@ -499,11 +461,14 @@ private final class CreateLinkSheetComponent: CombinedComponent { webpage = nil } + let link = context.state.link + let sheet = sheet.update( component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, - link: context.component.link, + isEdit: context.component.link != nil, + link: link, webpage: webpage, state: context.state, dismiss: { @@ -517,6 +482,7 @@ private final class CreateLinkSheetComponent: CombinedComponent { backgroundColor: .blur(.dark), followContentSizeChanges: true, clipsContent: true, + isScrollEnabled: false, animateOut: animateOut ), environment: { @@ -558,17 +524,20 @@ public final class CreateLinkScreen: ViewControllerComponentContainer { public struct Link: Equatable { let url: String let name: String? + let webpage: TelegramMediaWebpage? let positionBelowText: Bool let largeMedia: Bool? init( url: String, name: String?, + webpage: TelegramMediaWebpage?, positionBelowText: Bool, largeMedia: Bool? ) { self.url = url self.name = name + self.webpage = webpage self.positionBelowText = positionBelowText self.largeMedia = largeMedia } @@ -582,8 +551,6 @@ public final class CreateLinkScreen: ViewControllerComponentContainer { let largeMedia: Bool? let image: UIImage? let nightImage: UIImage? - let compactLightImage: UIImage - let compactDarkImage: UIImage } private let context: AccountContext @@ -716,7 +683,7 @@ private final class LinkFieldComponent: Component { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) - if newText.count > 128 { + if let component = self.component, !component.link && newText.count > 48 { textField.layer.addShakeAnimation() let hapticFeedback = HapticFeedback() hapticFeedback.error() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index a03ea5fac3..917324e44d 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3300,8 +3300,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let self { if let location = entity as? DrawingLocationEntity { self.presentLocationPicker(location) - } else if let sticker = entity as? DrawingStickerEntity, case .link = sticker.content { - self.addOrEditLink(sticker) + } else if let link = entity as? DrawingLinkEntity { + self.addOrEditLink(link) } } }, @@ -4480,18 +4480,19 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.controller?.present(contextController, in: .window(.root)) } - func addOrEditLink(_ existingEntity: DrawingStickerEntity? = nil) { + func addOrEditLink(_ existingEntity: DrawingLinkEntity? = nil) { guard let controller = self.controller else { return } var link: CreateLinkScreen.Link? - if let existingEntity, case let .link(url, name, positionBelowText, largeMedia, _, _, _) = existingEntity.content { + if let existingEntity { link = CreateLinkScreen.Link( - url: url, - name: name, - positionBelowText: positionBelowText, - largeMedia: largeMedia + url: existingEntity.url, + name: existingEntity.name, + webpage: existingEntity.webpage, + positionBelowText: existingEntity.positionBelowText, + largeMedia: existingEntity.largeMedia ) } @@ -4500,45 +4501,34 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate return } - var linkStyle: DrawingStickerEntity.Content.LinkStyle - if let existingEntity, case let .link(_, _, _, _, _, _, existingStyle) = existingEntity.content { - if [.white, .black].contains(existingStyle), result.image == nil { - switch existingStyle { - case .white: - linkStyle = .whiteCompact - case .black: - linkStyle = .blackCompact - default: - linkStyle = existingStyle - } + let style: DrawingLinkEntity.Style + if let existingEntity { + if ![.white, .black].contains(existingEntity.style), result.webpage != nil { + style = .white } else { - linkStyle = existingStyle + style = existingEntity.style } } else { - linkStyle = result.image != nil ? .white : .whiteCompact + style = .white } - - let entity = DrawingStickerEntity( - content: .link(result.url, result.name, result.positionBelowText, result.largeMedia, result.image?.size, result.compactLightImage.size, linkStyle) - ) + + let entity = DrawingLinkEntity(url: result.url, name: result.name, webpage: result.webpage, positionBelowText: result.positionBelowText, largeMedia: result.largeMedia, style: style) entity.renderImage = result.image entity.secondaryRenderImage = result.nightImage - entity.tertiaryRenderImage = result.compactLightImage - entity.quaternaryRenderImage = result.compactDarkImage - let fraction: CGFloat - if let image = result.image { - fraction = max(image.size.width, image.size.height) / 353.0 - } else { - fraction = max(result.compactLightImage.size.width, result.compactLightImage.size.height) / 353.0 - } + let fraction: CGFloat = 1.0 +// if let image = result.image { +// fraction = max(image.size.width, image.size.height) / 353.0 +// } else { +// fraction = 1.0 +// } if let existingEntity { self.entitiesView.remove(uuid: existingEntity.uuid, animated: true) } self.interaction?.insertEntity( entity, - scale: existingEntity?.scale ?? min(6.0, 3.3 * fraction) * 0.5, + scale: existingEntity?.scale ?? min(6.0, fraction), position: existingEntity?.position ) }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 4799b93086..d00acf4c33 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -145,6 +145,7 @@ swift_library( "//submodules/TelegramUI/Components/Settings/PeerSelectionScreen", "//submodules/ConfettiEffect", "//submodules/ContactsPeerItem", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index db1861d0fc..6b31bf4129 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -349,6 +349,8 @@ final class PeerInfoScreenData { let isPremiumRequiredForStoryPosting: Bool let personalChannel: PeerInfoPersonalChannelData? let starsState: StarsContext.State? + let starsRevenueStatsState: StarsRevenueStats? + let starsRevenueStatsContext: StarsRevenueStatsContext? let _isContact: Bool var forceIsContact: Bool = false @@ -389,7 +391,9 @@ final class PeerInfoScreenData { hasSavedMessageTags: Bool, isPremiumRequiredForStoryPosting: Bool, personalChannel: PeerInfoPersonalChannelData?, - starsState: StarsContext.State? + starsState: StarsContext.State?, + starsRevenueStatsState: StarsRevenueStats?, + starsRevenueStatsContext: StarsRevenueStatsContext? ) { self.peer = peer self.chatPeer = chatPeer @@ -419,6 +423,8 @@ final class PeerInfoScreenData { self.isPremiumRequiredForStoryPosting = isPremiumRequiredForStoryPosting self.personalChannel = personalChannel self.starsState = starsState + self.starsRevenueStatsState = starsRevenueStatsState + self.starsRevenueStatsContext = starsRevenueStatsContext } } @@ -912,7 +918,9 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, hasSavedMessageTags: false, isPremiumRequiredForStoryPosting: true, personalChannel: personalChannel, - starsState: starsState + starsState: starsState, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil ) } } @@ -952,7 +960,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: false, isPremiumRequiredForStoryPosting: true, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil )) case let .user(userPeerId, secretChatId, kind): let groupsInCommon: GroupsInCommonContext? @@ -1169,6 +1179,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags = .single(false) } + let starsRevenueStatsContextPromise = Promise(nil) + let starsRevenueStatsStatePromise = Promise(nil) + return combineLatest( context.account.viewTracker.peerView(peerId, updateData: true), peerInfoAvailableMediaPanes(context: context, peerId: peerId, chatLocation: chatLocation, isMyProfile: isMyProfile, chatLocationContextHolder: chatLocationContextHolder), @@ -1183,11 +1196,12 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessages, hasSavedMessageTags, peerInfoPersonalChannel(context: context, peerId: peerId, isSettings: false), - privacySettings + privacySettings, + starsRevenueStatsContextPromise.get(), + starsRevenueStatsStatePromise.get() ) - |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel, privacySettings -> PeerInfoScreenData in + |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories, hasStoryArchive, accountIsPremium, savedMessagesPeer, hasSavedMessagesChats, hasSavedMessages, hasSavedMessageTags, personalChannel, privacySettings, currentStarsRevenueStatsContext, starsRevenueStatsState -> PeerInfoScreenData in var availablePanes = availablePanes - if isMyProfile { availablePanes?.insert(.stories, at: 0) if let hasStoryArchive, hasStoryArchive { @@ -1252,6 +1266,14 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen enableQRLogin: false) } + if case .bot = kind, let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) { + if currentStarsRevenueStatsContext == nil { + let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId) + starsRevenueStatsContextPromise.set(.single(starsRevenueStatsContext)) + starsRevenueStatsStatePromise.set(starsRevenueStatsContext.state |> map { $0.stats }) + } + } + return PeerInfoScreenData( peer: peer, chatPeer: peerView.peers[peerId], @@ -1280,7 +1302,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: false, personalChannel: personalChannel, - starsState: nil + starsState: nil, + starsRevenueStatsState: starsRevenueStatsState, + starsRevenueStatsContext: currentStarsRevenueStatsContext ) } case .channel: @@ -1452,7 +1476,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil ) } case let .group(groupId): @@ -1747,7 +1773,9 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen hasSavedMessageTags: hasSavedMessageTags, isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting, personalChannel: nil, - starsState: nil + starsState: nil, + starsRevenueStatsState: nil, + starsRevenueStatsContext: nil )) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 45190ed7c0..1a129a3872 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -108,6 +108,7 @@ import GroupStickerPackSetupController import PeerNameColorItem import PeerSelectionScreen import UIKitRuntimeUtils +import OldChannelsController public enum PeerInfoAvatarEditingMode { case generic @@ -1733,9 +1734,8 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenPublicLinkSetup() })) - if "".isEmpty { - let balance: Int64 = 2275 - items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemStars, label: .text(presentationData.strings.PeerInfo_Bot_Balance_Stars(Int32(balance))), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.stars, action: { + if let starsRevenueStats = data.starsRevenueStatsState { + 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() })) } @@ -8344,10 +8344,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func editingOpenStars() { - guard let starsContext = self.context.starsContext else { + guard let revenueContext = self.data?.starsRevenueStatsContext else { return } - self.controller?.push(self.context.sharedContext.makeStarsStatisticsScreen(context: self.context, starsContext: starsContext)) + self.controller?.push(self.context.sharedContext.makeStarsStatisticsScreen(context: self.context, peerId: self.peerId, revenueContext: revenueContext)) } private func editingOpenReactionsSetup() { diff --git a/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD new file mode 100644 index 0000000000..b4afc963ea --- /dev/null +++ b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/BUILD @@ -0,0 +1,34 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "OldChannelsController", + module_name = "OldChannelsController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/ContactsPeerItem", + "//submodules/ItemListUI", + "//submodules/SearchUI", + "//submodules/SearchBarNode", + "//submodules/SolidRoundedButtonNode", + "//submodules/PremiumUI", + "//submodules/ChatListSearchItemHeader", + "//submodules/MergeLists", + ], + visibility = [ + "//visibility:public", + ], +) + + diff --git a/submodules/PeerInfoUI/Sources/OldChannelsController.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift similarity index 100% rename from submodules/PeerInfoUI/Sources/OldChannelsController.swift rename to submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsController.swift diff --git a/submodules/PeerInfoUI/Sources/OldChannelsSearch.swift b/submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift similarity index 100% rename from submodules/PeerInfoUI/Sources/OldChannelsSearch.swift rename to submodules/TelegramUI/Components/PeerManagement/OldChannelsController/Sources/OldChannelsSearch.swift diff --git a/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD new file mode 100644 index 0000000000..86c4687698 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "OwnershipTransferController", + module_name = "OwnershipTransferController", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/Postbox", + "//submodules/TelegramPresentationData", + "//submodules/PresentationDataUtils", + "//submodules/AccountContext", + "//submodules/TextFormat", + "//submodules/AlertUI", + "//submodules/PasswordSetupUI", + "//submodules/Markdown", + "//submodules/ActivityIndicator", + "//submodules/TelegramUI/Components/PeerManagement/OldChannelsController", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift similarity index 98% rename from submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift rename to submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift index 9a939217bc..e68b26f827 100644 --- a/submodules/PeerInfoUI/Sources/ChannelOwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/ChannelOwnershipTransferController.swift @@ -12,6 +12,7 @@ import AlertUI import PresentationDataUtils import PasswordSetupUI import Markdown +import OldChannelsController private final class ChannelOwnershipTransferPasswordFieldNode: ASDisplayNode, UITextFieldDelegate { private var theme: PresentationTheme @@ -562,7 +563,7 @@ private func confirmChannelOwnershipTransferController(context: AccountContext, return controller } -func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { +public func channelOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peer: EnginePeer, member: TelegramUser, initialError: ChannelOwnershipTransferError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (EnginePeer.Id?) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let theme = AlertControllerTheme(presentationData: presentationData) diff --git a/submodules/TelegramUI/Sources/OwnershipTransferController.swift b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift similarity index 93% rename from submodules/TelegramUI/Sources/OwnershipTransferController.swift rename to submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift index f721758b0e..4d5f6e9edc 100644 --- a/submodules/TelegramUI/Sources/OwnershipTransferController.swift +++ b/submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController/Sources/OwnershipTransferController.swift @@ -12,7 +12,6 @@ import AlertUI import PresentationDataUtils import PasswordSetupUI import Markdown -import PeerInfoUI private func commitOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -79,7 +78,7 @@ private func commitOwnershipTransferController(context: AccountContext, updatedP } -func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { +public func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController { let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } let theme = AlertControllerTheme(presentationData: presentationData) diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD index 37eebaff13..99fda0c389 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/BUILD @@ -21,6 +21,7 @@ swift_library( "//submodules/PhotoResources", "//submodules/AvatarNode", "//submodules/AccountContext", + "//submodules/InvisibleInkDustNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift index 7ab97d7886..8d50ea4156 100644 --- a/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsImageComponent/Sources/StarsImageComponent.swift @@ -2,12 +2,14 @@ import Foundation import UIKit import Display import SwiftSignalKit +import Postbox import TelegramCore import ComponentFlow import TelegramPresentationData import PhotoResources import AvatarNode import AccountContext +import InvisibleInkDustNode final class StarsParticlesView: UIView { private struct Particle { @@ -245,6 +247,7 @@ public final class StarsImageComponent: Component { public enum Subject: Equatable { case none case photo(TelegramMediaWebFile) + case extendedMedia(TelegramExtendedMedia) case transactionPeer(StarsContext.State.Transaction.Peer) } @@ -288,6 +291,7 @@ public final class StarsImageComponent: Component { private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? + private var dustNode: MediaDustNode? private let fetchDisposable = MetaDisposable() @@ -351,6 +355,39 @@ public final class StarsImageComponent: Component { imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + case let .extendedMedia(extendedMedia): + let imageNode: TransformImageNode + let dustNode: MediaDustNode + if let current = self.imageNode, let currentDust = self.dustNode { + imageNode = current + dustNode = currentDust + } else { + imageNode = TransformImageNode() + imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] + self.addSubview(imageNode.view) + self.imageNode = imageNode + + let media: TelegramMediaImage + switch extendedMedia { + case let .preview(_, immediateThumbnailData, _): + let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) + media = thumbnailMedia + case let .full(fullMedia): + media = fullMedia as! TelegramMediaImage + } + + imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true)) + + dustNode = MediaDustNode(enableAnimations: true) + self.addSubview(dustNode.view) + self.dustNode = dustNode + } + + imageNode.frame = imageFrame + imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 12.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() + + dustNode.frame = imageFrame + dustNode.update(size: imageFrame.size, color: .white, transition: .immediate) case let .transactionPeer(peer): if case let .peer(peer) = peer { let avatarNode: ImageNode diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD index 518c2783e2..9f1d4d8263 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ScrollComponent", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", + "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BundleIconComponent", "//submodules/Components/SolidRoundedButtonComponent", @@ -40,6 +41,10 @@ swift_library( "//submodules/AvatarNode", "//submodules/PhotoResources", "//submodules/TelegramUI/Components/Stars/StarsImageComponent", + "//submodules/PasswordSetupUI", + "//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController", + "//submodules/TelegramUI/Components/ListItemComponentAdaptor", + "//submodules/StatisticsUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift new file mode 100644 index 0000000000..8de9595897 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift @@ -0,0 +1,151 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import ComponentFlow +import AccountContext +import MultilineTextComponent +import TelegramPresentationData +import PresentationDataUtils +import SolidRoundedButtonComponent + +final class StarsOverviewItemComponent: Component { + let theme: PresentationTheme + let dateTimeFormat: PresentationDateTimeFormat + let title: String + let value: Int64 + let rate: Double + + init( + theme: PresentationTheme, + dateTimeFormat: PresentationDateTimeFormat, + title: String, + value: Int64, + rate: Double + ) { + self.theme = theme + self.dateTimeFormat = dateTimeFormat + self.title = title + self.value = value + self.rate = rate + } + + static func ==(lhs: StarsOverviewItemComponent, rhs: StarsOverviewItemComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.rate != rhs.rate { + return false + } + return true + } + + final class View: UIView { + private let icon = UIImageView() + private let value = ComponentView() + private let usdValue = ComponentView() + private let title = ComponentView() + + private var component: StarsOverviewItemComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.icon.image = UIImage(bundleImageName: "Premium/Stars/StarMedium") + + self.addSubview(self.icon) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StarsOverviewItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 16.0 + + var valueOffset: CGFloat = 0.0 + if let icon = self.icon.image { + self.icon.frame = CGRect(origin: CGPoint(x: sideInset - 1.0, y: 10.0), size: icon.size) + valueOffset += icon.size.width + } + + let valueString = presentationStringsFormattedNumber(Int32(component.value), component.dateTimeFormat.groupingSeparator) + let usdValueString = formatUsdValue(component.value, rate: component.rate) + + let valueSize = self.value.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: valueString, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let valueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + 2.0, y: 10.0), size: valueSize) + if let valueView = self.value.view { + if valueView.superview == nil { + self.addSubview(valueView) + } + valueView.frame = valueFrame + } + + let usdValueSize = self.usdValue.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "≈\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let usdValueFrame = CGRect(origin: CGPoint(x: sideInset + valueOffset + valueSize.width + 6.0, y: 14.0), size: usdValueSize) + if let usdValueView = self.usdValue.view { + if usdValueView.superview == nil { + self.addSubview(usdValueView) + } + usdValueView.frame = usdValueFrame + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: 32.0), size: titleSize) + titleView.frame = titleFrame + } + + return CGSize(width: availableSize.width, height: 59.0) + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsRevenueWithdrawalController.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsRevenueWithdrawalController.swift new file mode 100644 index 0000000000..2a0fe89b7d --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsRevenueWithdrawalController.swift @@ -0,0 +1,112 @@ +import Foundation +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import AccountContext +import PasswordSetupUI +import Markdown +import OwnershipTransferController + +func confirmStarsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + + var dismissImpl: (() -> Void)? + var proceedImpl: (() -> Void)? + + let disposable = MetaDisposable() + + let contentNode = ChannelOwnershipTransferAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Title, text: presentationData.strings.Monetization_Withdraw_EnterPassword_Text, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?() + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Monetization_Withdraw_EnterPassword_Done, action: { + proceedImpl?() + })]) + + contentNode.complete = { + proceedImpl?() + } + + let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode) + let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in + controller?.theme = AlertControllerTheme(presentationData: presentationData) + contentNode?.theme = presentationData.theme + }) + controller.dismissed = { _ in + presentationDataDisposable.dispose() + disposable.dispose() + } + dismissImpl = { [weak controller, weak contentNode] in + contentNode?.dismissInput() + controller?.dismissAnimated() + } + proceedImpl = { [weak contentNode] in + guard let contentNode = contentNode else { + return + } + contentNode.updateIsChecking(true) + + let signal = context.engine.peers.requestStarsRevenueWithdrawalUrl(peerId: peerId, amount: amount, password: contentNode.password) + disposable.set((signal |> deliverOnMainQueue).start(next: { url in + dismissImpl?() + completion(url) + }, error: { [weak contentNode] error in + var errorTextAndActions: (String, [TextAlertAction])? + switch error { + case .invalidPassword: + contentNode?.animateError() + case .limitExceeded: + errorTextAndActions = (presentationData.strings.TwoStepAuth_FloodError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + default: + errorTextAndActions = (presentationData.strings.Login_UnknownError, [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + } + contentNode?.updateIsChecking(false) + + if let (text, actions) = errorTextAndActions { + dismissImpl?() + present(textAlertController(context: context, title: nil, text: text, actions: actions), nil) + } + })) + } + + return controller +} + + +func starsRevenueWithdrawalController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: EnginePeer.Id, amount: Int64, initialError: RequestStarsRevenueWithdrawalError, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + let theme = AlertControllerTheme(presentationData: presentationData) + + var title: NSAttributedString? = NSAttributedString(string: presentationData.strings.OwnershipTransfer_SecurityCheck, font: Font.semibold(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.primaryColor, paragraphAlignment: .center) + + var text = presentationData.strings.Monetization_Withdraw_SecurityRequirements + let textFontSize = presentationData.listsFontSize.baseDisplaySize * 13.0 / 17.0 + + var actions: [TextAlertAction] = [] + switch initialError { + case .requestPassword: + return confirmStarsRevenueWithdrawalController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, amount: amount, present: present, completion: completion) + case .twoStepAuthTooFresh, .authSessionTooFresh: + text = text + presentationData.strings.Monetization_Withdraw_ComeBackLater + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + case .twoStepAuthMissing: + actions = [TextAlertAction(type: .genericAction, title: presentationData.strings.OwnershipTransfer_SetupTwoStepAuth, action: { + let controller = SetupTwoStepVerificationController(context: context, initialState: .automatic, stateUpdated: { update, shouldDismiss, controller in + if shouldDismiss { + controller.dismiss() + } + }) + present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})] + default: + title = nil + text = presentationData.strings.Login_UnknownError + actions = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})] + } + + let body = MarkdownAttributeSet(font: Font.regular(textFontSize), textColor: theme.primaryColor) + let bold = MarkdownAttributeSet(font: Font.semibold(textFontSize), textColor: theme.primaryColor) + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center) + + return richTextAlertController(context: context, title: title, text: attributedText, actions: actions) +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift index fd45cc9a5d..4e207ec526 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsStatisticsScreen.swift @@ -17,23 +17,29 @@ import ListSectionComponent import BundleIconComponent import TextFormat import UndoUI +import ListItemComponentAdaptor +import StatisticsUI +import ItemListUI final class StarsStatisticsScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext - let starsContext: StarsContext + let peerId: EnginePeer.Id + let revenueContext: StarsRevenueStatsContext let openTransaction: (StarsContext.State.Transaction) -> Void let buy: () -> Void init( context: AccountContext, - starsContext: StarsContext, + peerId: EnginePeer.Id, + revenueContext: StarsRevenueStatsContext, openTransaction: @escaping (StarsContext.State.Transaction) -> Void, buy: @escaping () -> Void ) { self.context = context - self.starsContext = starsContext + self.peerId = peerId + self.revenueContext = revenueContext self.openTransaction = openTransaction self.buy = buy } @@ -42,7 +48,10 @@ final class StarsStatisticsScreenComponent: Component { if lhs.context !== rhs.context { return false } - if lhs.starsContext !== rhs.starsContext { + if lhs.peerId != rhs.peerId { + return false + } + if lhs.revenueContext !== rhs.revenueContext { return false } return true @@ -103,7 +112,7 @@ final class StarsStatisticsScreenComponent: Component { private var ignoreScrolling: Bool = false private var stateDisposable: Disposable? - private var starsState: StarsContext.State? + private var starsState: StarsRevenueStats? private var previousBalance: Int64? @@ -236,22 +245,22 @@ final class StarsStatisticsScreenComponent: Component { var balanceUpdated = false if let starsState = self.starsState { - if let previousBalance, starsState.balance != previousBalance { + if let previousBalance = self.previousBalance, starsState.balances.availableBalance != previousBalance { balanceUpdated = true } - self.previousBalance = starsState.balance + self.previousBalance = starsState.balances.availableBalance } let environment = environment[ViewControllerComponentContainer.Environment.self].value let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } if self.stateDisposable == nil { - self.stateDisposable = (component.starsContext.state + self.stateDisposable = (component.revenueContext.state |> deliverOnMainQueue).start(next: { [weak self] state in guard let self else { return } - self.starsState = state + self.starsState = state.stats if !self.isUpdating { self.state?.updated() @@ -291,12 +300,12 @@ final class StarsStatisticsScreenComponent: Component { contentHeight += environment.navigationHeight contentHeight += 31.0 - + let titleSize = self.titleView.update( transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "Stars Balance", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Stars_BotRevenue_Title, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -314,39 +323,71 @@ final class StarsStatisticsScreenComponent: Component { transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleSize)) } + if let revenueGraph = starsState?.revenueGraph { + let chartSize = self.chartView.update( + transition: .immediate, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stars_BotRevenue_Revenue_Title.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: environment.theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( + itemGenerator: StatsGraphItem(presentationData: ItemListPresentationData(presentationData), graph: revenueGraph, type: .currency, conversionRate: starsState?.usdRate ?? 0.0, sectionId: 0, style: .blocks), + params: ListViewItemLayoutParams(width: availableSize.width - sideInsets, leftInset: 0.0, rightInset: 0.0, availableHeight: 10000.0, isStandalone: true) + ))), + ], + displaySeparators: false + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInsets, height: availableSize.height) + ) + let chartFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - chartSize.width) / 2.0), y: contentHeight), size: chartSize) + if let chartView = self.chartView.view { + if chartView.superview == nil { + self.scrollView.addSubview(chartView) + } + transition.setFrame(view: chartView, frame: chartFrame) + } + contentHeight += chartSize.height + contentHeight += 44.0 + } + let proceedsSize = self.proceedsView.update( transition: .immediate, component: AnyComponent(ListSectionComponent( theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Proceeds Overview".uppercased(), + string: environment.strings.Stars_BotRevenue_Proceeds_Title.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), maximumNumberOfLines: 0 )), footer: nil, - items: [AnyComponentWithIdentity(id: 0, component: AnyComponent( - VStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarMedium", tintColor: nil))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0), environment.dateTimeFormat.groupingSeparator), font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))))), - AnyComponentWithIdentity(id: 2, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: formatUsdValue(self.starsState?.balance ?? 0, rate: 0.2), font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))), - ], spacing: 3.0))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Available Balance", font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))) - ], alignment: .left, spacing: 2.0) - )), - AnyComponentWithIdentity(id: 1, component: AnyComponent( - VStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(HStack([ - AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(name: "Premium/Stars/StarMedium", tintColor: nil))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(Int32(self.starsState?.balance ?? 0) * 3, environment.dateTimeFormat.groupingSeparator), font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor))))), - AnyComponentWithIdentity(id: 2, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: formatUsdValue((self.starsState?.balance ?? 0) * 3, rate: 0.2), font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))), - ], spacing: 3.0))), - AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Total Lifetime Proceeds", font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor))))) - ], alignment: .left, spacing: 2.0) - ))], + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent( + theme: environment.theme, + dateTimeFormat: environment.dateTimeFormat, + title: environment.strings.Stars_BotRevenue_Proceeds_Available, + value: starsState?.balances.availableBalance ?? 0, + rate: starsState?.usdRate ?? 0.0 + ))), + AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent( + theme: environment.theme, + dateTimeFormat: environment.dateTimeFormat, + title: environment.strings.Stars_BotRevenue_Proceeds_Total, + value: starsState?.balances.overallRevenue ?? 0, + rate: starsState?.usdRate ?? 0.0 + ))) + ], displaySeparators: false )), environment: {}, @@ -359,7 +400,6 @@ final class StarsStatisticsScreenComponent: Component { } transition.setFrame(view: proceedsView, frame: proceedsFrame) } - contentHeight += proceedsSize.height contentHeight += 44.0 @@ -369,7 +409,7 @@ final class StarsStatisticsScreenComponent: Component { return (TelegramTextAttributes.URL, contents) }) - let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("You can withdraw Stars using Fragment, or use Stars to advertise your bot. [Learn More >]()", attributes: termsMarkdownAttributes, textAlignment: .natural + let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_BotRevenue_Withdraw_Info, attributes: termsMarkdownAttributes, textAlignment: .natural )) if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme { self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) @@ -384,7 +424,7 @@ final class StarsStatisticsScreenComponent: Component { theme: environment.theme, header: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Available Balance".uppercased(), + string: environment.strings.Stars_BotRevenue_Withdraw_Balance.uppercased(), font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -399,9 +439,9 @@ final class StarsStatisticsScreenComponent: Component { theme: environment.theme, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat, - count: self.starsState?.balance ?? 0, + count: self.starsState?.balances.availableBalance ?? 0, rate: 0.2, - actionTitle: "Withdraw via Fragment", + actionTitle: environment.strings.Stars_BotRevenue_Withdraw_Withdraw, actionAvailable: true, buy: { [weak self] in guard let self, let component = self.component else { @@ -426,28 +466,31 @@ final class StarsStatisticsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - let initialTransactions = self.starsState?.transactions ?? [] var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] - if !initialTransactions.isEmpty { - let allTransactionsContext: StarsTransactionsContext - if let current = self.allTransactionsContext { - allTransactionsContext = current - } else { - allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .all) + let allTransactionsContext: StarsTransactionsContext + if let current = self.allTransactionsContext { + allTransactionsContext = current + } else { + allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .peer(component.peerId), mode: .all) + component.revenueContext.setUpdated { [weak self] in + if let self, let allTransactionsContext = self.allTransactionsContext { + allTransactionsContext.reload() + } } - - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "all", - title: environment.strings.Stars_Intro_AllTransactions, - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - transactionsContext: allTransactionsContext, - action: { transaction in - component.openTransaction(transaction) - } - )) - )) + self.allTransactionsContext = allTransactionsContext } + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: environment.strings.Stars_Intro_AllTransactions, + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + transactionsContext: allTransactionsContext, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) var panelTransition = transition if balanceUpdated { @@ -528,17 +571,20 @@ final class StarsStatisticsScreenComponent: Component { public final class StarsStatisticsScreen: ViewControllerComponentContainer { private let context: AccountContext - private let starsContext: StarsContext + private let peerId: EnginePeer.Id + private let revenueContext: StarsRevenueStatsContext - public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) { + public init(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) { self.context = context - self.starsContext = starsContext + self.peerId = peerId + self.revenueContext = revenueContext var withdrawImpl: (() -> Void)? var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? super.init(context: context, component: StarsStatisticsScreenComponent( context: context, - starsContext: starsContext, + peerId: peerId, + revenueContext: revenueContext, openTransaction: { transaction in openTransactionImpl?(transaction) }, @@ -558,12 +604,47 @@ public final class StarsStatisticsScreen: ViewControllerComponentContainer { } withdrawImpl = { [weak self] in - guard let _ = self else { + guard let self else { return } + + let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability() + |> deliverOnMainQueue).start(error: { [weak self] error in + guard let self else { + return + } + switch error { + case .requestPassword: + let _ = (revenueContext.state + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] state in + guard let self, let stats = state.stats else { + return + } + let controller = StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: { [weak self] amount in + guard let self else { + return + } + let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: peerId, amount: amount, present: { [weak self] c, a in + self?.present(c, in: .window(.root)) + }, completion: { url in + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) + }) + self.present(controller, in: .window(.root)) + }) + self.push(controller) + }) + default: + let controller = starsRevenueWithdrawalController(context: context, peerId: peerId, amount: 0, initialError: error, present: { [weak self] c, a in + self?.present(c, in: .window(.root)) + }, completion: { _ in + + }) + self.present(controller, in: .window(.root)) + } + }) } - - self.starsContext.load(force: false) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift index a3e2450ba2..d36bfdf6e4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -1205,24 +1205,3 @@ private final class TransactionCellComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - -private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { - return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setFillColor(backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) - - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setStrokeColor(foregroundColor.cgColor) - - context.move(to: CGPoint(x: 10.0, y: 10.0)) - context.addLine(to: CGPoint(x: 20.0, y: 20.0)) - context.strokePath() - - context.move(to: CGPoint(x: 20.0, y: 10.0)) - context.addLine(to: CGPoint(x: 10.0, y: 20.0)) - context.strokePath() - }) -} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 64744a6353..9cc7ed0496 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -275,7 +275,7 @@ final class StarsTransactionsScreenComponent: Component { var balanceUpdated = false if let starsState = self.starsState { - if let previousBalance, starsState.balance != previousBalance { + if let previousBalance = self.previousBalance, starsState.balance != previousBalance { balanceUpdated = true } self.previousBalance = starsState.balance @@ -555,6 +555,7 @@ final class StarsTransactionsScreenComponent: Component { allTransactionsContext = current } else { allTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .all) + self.allTransactionsContext = allTransactionsContext } let incomingTransactionsContext: StarsTransactionsContext @@ -562,6 +563,7 @@ final class StarsTransactionsScreenComponent: Component { incomingTransactionsContext = current } else { incomingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .incoming) + self.incomingTransactionsContext = incomingTransactionsContext } let outgoingTransactionsContext: StarsTransactionsContext @@ -569,6 +571,7 @@ final class StarsTransactionsScreenComponent: Component { outgoingTransactionsContext = current } else { outgoingTransactionsContext = component.context.engine.payments.peerStarsTransactionsContext(subject: .starsContext(component.starsContext), mode: .outgoing) + self.outgoingTransactionsContext = outgoingTransactionsContext } panelItems.append(StarsTransactionsPanelContainerComponent.Item( diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsWithdrawScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsWithdrawScreen.swift new file mode 100644 index 0000000000..eea5e3f5b4 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsWithdrawScreen.swift @@ -0,0 +1,721 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import SwiftSignalKit +import Postbox +import TelegramCore +import Markdown +import TextFormat +import TelegramPresentationData +import ViewControllerComponent +import SheetComponent +import BalancedTextComponent +import MultilineTextComponent +import BundleIconComponent +import ButtonComponent +import ItemListUI +import AccountContext +import PresentationDataUtils +import ListSectionComponent +import TelegramStringFormatting + +private let amountTag = GenericComponentViewTag() + +private final class SheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let mode: StarsWithdrawScreen.Mode + let dismiss: () -> Void + + init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode, + dismiss: @escaping () -> Void + ) { + self.context = context + self.mode = mode + self.dismiss = dismiss + } + + static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + return true + } + + static var body: Body { + let background = Child(RoundedRectangle.self) + let closeButton = Child(Button.self) + let title = Child(Text.self) + let urlSection = Child(ListSectionComponent.self) + let button = Child(ButtonComponent.self) + let balanceTitle = Child(MultilineTextComponent.self) + let balanceValue = Child(MultilineTextComponent.self) + let balanceIcon = Child(BundleIconComponent.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let component = context.component + let state = context.state + + let theme = environment.theme.withModalBlocksBackground() + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let sideInset: CGFloat = 16.0 + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) + + let background = background.update( + component: RoundedRectangle(color: theme.list.blocksBackgroundColor, cornerRadius: 8.0), + availableSize: CGSize(width: context.availableSize.width, height: 1000.0), + transition: .immediate + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + closeImage = image + } else { + closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! + state.cachedCloseImage = (closeImage, theme) + } + let closeButton = closeButton.update( + component: Button( + content: AnyComponent(Image(image: closeImage)), + action: { + component.dismiss() + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + ) + + let titleString: String + let amountTitle: String + let amountPlaceholder: String + + let minAmount: Int64? + let maxAmount: Int64? + + switch component.mode { + case let .withdraw(status): + titleString = environment.strings.Stars_Withdraw_Title + amountTitle = environment.strings.Stars_Withdraw_AmountTitle + amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder + + let configuration = StarsWithdrawConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) + minAmount = configuration.minWithdrawAmount + maxAmount = status.balances.availableBalance + case .paidMedia: + titleString = environment.strings.Stars_PaidContent_Title + amountTitle = environment.strings.Stars_PaidContent_AmountTitle + amountPlaceholder = environment.strings.Stars_PaidContent_AmountPlaceholder + + minAmount = 1 + maxAmount = nil + } + + let title = title.update( + component: Text(text: titleString, font: Font.bold(17.0), color: theme.list.itemPrimaryTextColor), + availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + title.size.height / 2.0)) + ) + contentSize.height += title.size.height + contentSize.height += 40.0 + + if case let .withdraw(starsState) = component.mode { + let balanceTitle = balanceTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.Stars_Transfer_Balance, + font: Font.regular(14.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: .immediate + ) + let balanceValue = balanceValue.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(starsState.balances.availableBalance), environment.dateTimeFormat.groupingSeparator), + font: Font.semibold(16.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: .immediate + ) + let balanceIcon = balanceIcon.update( + component: BundleIconComponent(name: "Premium/Stars/StarSmall", tintColor: nil), + availableSize: context.availableSize, + transition: .immediate + ) + + let topBalanceOriginY = 11.0 + context.add(balanceTitle + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceTitle.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height / 2.0)) + ) + context.add(balanceIcon + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 1.0 + UIScreenPixel)) + ) + context.add(balanceValue + .position(CGPoint(x: 16.0 + environment.safeInsets.left + balanceIcon.size.width + 3.0 + balanceValue.size.width / 2.0, y: topBalanceOriginY + balanceTitle.size.height + balanceValue.size.height / 2.0 + 2.0 - UIScreenPixel)) + ) + } + + let amountFooter: AnyComponent? + if case .paidMedia = component.mode { + let amountFont = Font.regular(13.0) + let amountTextColor = theme.list.freeTextColor + let amountMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), bold: MarkdownAttributeSet(font: amountFont, textColor: amountTextColor), link: MarkdownAttributeSet(font: amountFont, textColor: theme.list.itemAccentColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let amountInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_PaidContent_AmountInfo, attributes: amountMarkdownAttributes, textAlignment: .natural)) + if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme) + } + if let range = amountInfoString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + amountInfoString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: amountInfoString.string)) + } + amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0 + )) + } else { + amountFooter = nil + } + + let urlSection = urlSection.update( + component: ListSectionComponent( + theme: theme, + header: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: amountTitle.uppercased(), + font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), + textColor: theme.list.freeTextColor + )), + maximumNumberOfLines: 0 + )), + footer: amountFooter, + items: [ + AnyComponentWithIdentity( + id: "amount", + component: AnyComponent( + AmountFieldComponent( + textColor: theme.list.itemPrimaryTextColor, + placeholderColor: theme.list.itemPlaceholderTextColor, + value: state.amount, + minValue: minAmount, + maxValue: maxAmount, + placeholderText: amountPlaceholder, + amountUpdated: { [weak state] amount in + state?.amount = amount + state?.updated() + }, + tag: amountTag + ) + ) + ) + ] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(urlSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + urlSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += urlSection.size.height + contentSize.height += 32.0 + + let buttonString: String + if case .paidMedia = component.mode { + buttonString = environment.strings.Stars_PaidContent_Create + } else if let amount = state.amount { + buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(amount)" + } else { + buttonString = environment.strings.Stars_Withdraw_Withdraw + } + + if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { + state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) + } + + let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { + buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) + buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) + } + + let controller = environment.controller + let button = button.update( + component: ButtonComponent( + background: ButtonComponent.Background( + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), + cornerRadius: 10.0 + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) + ), + isEnabled: true, + displaysProgress: false, + action: { [weak state] in + if let controller = controller() as? StarsWithdrawScreen, let amount = state?.amount { + controller.completion(amount) + controller.dismissAnimated() + } + } + ), + availableSize: CGSize(width: 361.0, height: 50), + transition: .immediate + ) + context.add(button + .clipsToBounds(true) + .cornerRadius(10.0) + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + button.size.height / 2.0)) + ) + contentSize.height += button.size.height + contentSize.height += 30.0 + + contentSize.height += max(environment.inputHeight, environment.safeInsets.bottom) + + return contentSize + } + } + + final class State: ComponentState { + private let context: AccountContext + + fileprivate var amount: Int64? + + var cachedCloseImage: (UIImage, PresentationTheme)? + var cachedStarImage: (UIImage, PresentationTheme)? + var cachedChevronImage: (UIImage, PresentationTheme)? + + init( + context: AccountContext, + amount: Int64? + ) { + self.context = context + self.amount = amount + + super.init() + } + } + + func makeState() -> State { + var amount: Int64? + if case let .withdraw(stats) = mode { + amount = stats.balances.availableBalance + } + return State(context: self.context, amount: amount) + } +} + +private final class StarsWithdrawSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + private let context: AccountContext + private let mode: StarsWithdrawScreen.Mode + + init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode + ) { + self.context = context + self.mode = mode + } + + static func ==(lhs: StarsWithdrawSheetComponent, rhs: StarsWithdrawSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.mode != rhs.mode { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent<(EnvironmentType)>.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(SheetContent( + context: context.component.context, + mode: context.component.mode, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } + )), + backgroundColor: .blur(.light), + followContentSizeChanges: false, + clipsContent: true, + isScrollEnabled: false, + animateOut: animateOut + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + return context.availableSize + } + } +} + +public final class StarsWithdrawScreen: ViewControllerComponentContainer { + public enum Mode: Equatable { + case withdraw(StarsRevenueStats) + case paidMedia + } + + private let context: AccountContext + fileprivate let completion: (Int64) -> Void + + public init( + context: AccountContext, + mode: StarsWithdrawScreen.Mode, + completion: @escaping (Int64) -> Void + ) { + self.context = context + self.completion = completion + + super.init( + context: context, + component: StarsWithdrawSheetComponent( + context: context, + mode: mode + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let view = self.node.hostView.findTaggedView(tag: amountTag) as? AmountFieldComponent.View { + Queue.mainQueue().after(0.01) { + view.activateInput() + view.selectAll() + } + } + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} + +private final class AmountFieldComponent: Component { + typealias EnvironmentType = Empty + + let textColor: UIColor + let placeholderColor: UIColor + let value: Int64? + let minValue: Int64? + let maxValue: Int64? + let placeholderText: String + let amountUpdated: (Int64?) -> Void + let tag: AnyObject? + + init( + textColor: UIColor, + placeholderColor: UIColor, + value: Int64?, + minValue: Int64?, + maxValue: Int64?, + placeholderText: String, + amountUpdated: @escaping (Int64?) -> Void, + tag: AnyObject? = nil + ) { + self.textColor = textColor + self.placeholderColor = placeholderColor + self.value = value + self.minValue = minValue + self.maxValue = maxValue + self.placeholderText = placeholderText + self.amountUpdated = amountUpdated + self.tag = tag + } + + static func ==(lhs: AmountFieldComponent, rhs: AmountFieldComponent) -> Bool { + if lhs.textColor != rhs.textColor { + return false + } + if lhs.placeholderColor != rhs.placeholderColor { + return false + } + if lhs.value != rhs.value { + return false + } + if lhs.minValue != rhs.minValue { + return false + } + if lhs.maxValue != rhs.maxValue { + return false + } + if lhs.placeholderText != rhs.placeholderText { + return false + } + return true + } + + final class View: UIView, UITextFieldDelegate, ComponentTaggedView { + public func matches(tag: Any) -> Bool { + if let component = self.component, let componentTag = component.tag { + let tag = tag as AnyObject + if componentTag === tag { + return true + } + } + return false + } + + private let placeholderView: ComponentView + private let iconView: UIImageView + private let textField: TextFieldNodeView + + private var component: AmountFieldComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.placeholderView = ComponentView() + self.textField = TextFieldNodeView(frame: .zero) + + self.iconView = UIImageView(image: UIImage(bundleImageName: "Premium/Stars/StarLarge")) + + super.init(frame: frame) + + self.textField.delegate = self + self.textField.addTarget(self, action: #selector(self.textChanged(_:)), for: .editingChanged) + + self.addSubview(self.textField) + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func textChanged(_ sender: Any) { + let text = self.textField.text ?? "" + let amount: Int64? + if !text.isEmpty, let value = Int64(text) { + amount = value + } else { + amount = nil + } + self.component?.amountUpdated(amount) + self.placeholderView.view?.isHidden = !text.isEmpty + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string) + + if let component = self.component { + let amount: Int64? + if !newText.isEmpty, let value = Int64(newText) { + amount = value + } else { + amount = nil + } + + if let amount, let maxAmount = component.maxValue, amount > maxAmount { + textField.text = "\(maxAmount)" + + textField.layer.addShakeAnimation() + let hapticFeedback = HapticFeedback() + hapticFeedback.error() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: { + let _ = hapticFeedback + }) + + return false + } + } + return true + } + + func activateInput() { + self.textField.becomeFirstResponder() + } + + func selectAll() { + self.textField.selectAll(nil) + } + + func update(component: AmountFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.textField.textColor = component.textColor + if let value = component.value { + self.textField.text = "\(value)" + } else { + self.textField.text = "" + } + self.textField.font = Font.regular(17.0) + + self.textField.keyboardType = .numberPad + self.textField.returnKeyType = .done + self.textField.autocorrectionType = .no + self.textField.autocapitalizationType = .none + + self.component = component + self.state = state + + let size = CGSize(width: availableSize.width, height: 44.0) + + var leftInset: CGFloat = 15.0 + if let icon = self.iconView.image { + leftInset += icon.size.width + 6.0 + self.iconView.frame = CGRect(origin: CGPoint(x: 15.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size) + } + + let placeholderSize = self.placeholderView.update( + transition: .easeInOut(duration: 0.2), + component: AnyComponent( + Text( + text: component.placeholderText, + font: Font.regular(17.0), + color: component.placeholderColor + ) + ), + environment: {}, + containerSize: availableSize + ) + + if let placeholderComponentView = self.placeholderView.view { + if placeholderComponentView.superview == nil { + self.insertSubview(placeholderComponentView, at: 0) + } + + placeholderComponentView.frame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((size.height - placeholderSize.height) / 2.0) + 1.0 - UIScreenPixel), size: placeholderSize) + + placeholderComponentView.isHidden = !(self.textField.text ?? "").isEmpty + } + + self.textField.frame = CGRect(x: leftInset, y: 0.0, width: size.width - 30.0, height: 44.0) + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} + +private struct StarsWithdrawConfiguration { + static var defaultValue: StarsWithdrawConfiguration { + return StarsWithdrawConfiguration(minWithdrawAmount: nil) + } + + let minWithdrawAmount: Int64? + + fileprivate init(minWithdrawAmount: Int64?) { + self.minWithdrawAmount = minWithdrawAmount + } + + static func with(appConfiguration: AppConfiguration) -> StarsWithdrawConfiguration { + if let data = appConfiguration.data, let minWithdrawAmount = data["stars_revenue_withdrawal_min"] as? Double { + return StarsWithdrawConfiguration(minWithdrawAmount: Int64(minWithdrawAmount)) + } else { + return .defaultValue + } + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 3d45440c82..62d2a302ef 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -227,28 +227,31 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) ) - if let peer = state.peer { - let subject: StarsImageComponent.Subject + let subject: StarsImageComponent.Subject + if let extendedMedia = component.invoice.extendedMedia { + subject = .extendedMedia(extendedMedia) + } else if let peer = state.peer { if let photo = component.invoice.photo { subject = .photo(photo) } else { subject = .transactionPeer(.peer(peer)) } - let star = star.update( - component: StarsImageComponent( - context: component.context, - subject: subject, - theme: theme, - diameter: 90.0 - ), - availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), - transition: context.transition - ) - - context.add(star - .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0)) - ) + } else { + subject = .none } + let star = star.update( + component: StarsImageComponent( + context: component.context, + subject: subject, + theme: theme, + diameter: 90.0 + ), + availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), + transition: context.transition + ) + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 27.0)) + ) let closeImage: UIImage if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { @@ -295,14 +298,23 @@ private final class SheetContent: CombinedComponent { }) let amount = component.invoice.totalAmount + let infoText: String + if let _ = component.invoice.extendedMedia { + infoText = strings.Stars_Transfer_UnlockInfo( + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } else { + infoText = strings.Stars_Transfer_Info( + component.invoice.title, + state.peer?.compactDisplayTitle ?? "", + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string + } + let text = text.update( component: BalancedTextComponent( text: .markdown( - text: strings.Stars_Transfer_Info( - component.invoice.title, - state.peer?.compactDisplayTitle ?? "", - strings.Stars_Transfer_Info_Stars(Int32(amount)) - ).string, + text: infoText, attributes: markdownAttributes ), horizontalAlignment: .center, diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index db16c8152d..cafaed742f 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -532,7 +532,7 @@ public class StickerPickerScreen: ViewController { self.containerView.addSubview(self.hostView) if controller.hasInteractiveStickers { - self.storyStickersContentView = StoryStickersContentView(frame: .zero) + self.storyStickersContentView = StoryStickersContentView(isPremium: context.isPremium) self.storyStickersContentView?.locationAction = { [weak self] in self?.controller?.presentLocationPicker() } @@ -543,7 +543,14 @@ public class StickerPickerScreen: ViewController { self?.controller?.addReaction() } self.storyStickersContentView?.linkAction = { [weak self] in - self?.controller?.addLink() + guard let self, let controller = self.controller else { + return + } + if controller.context.isPremium { + controller.addLink() + } else { + self.presentLinkPremiumSuggestion() + } } } @@ -799,7 +806,7 @@ public class StickerPickerScreen: ViewController { controller.present(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { [weak controller] action in if case .info = action, let controller { let premiumController = context.sharedContext.makePremiumIntroController(context: context, source: .savedGifs, forceDark: controller.forceDark, dismissed: nil) - controller.push(premiumController) + controller.pushController(premiumController) return true } return false @@ -1633,6 +1640,32 @@ public class StickerPickerScreen: ViewController { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } } + + func presentLinkPremiumSuggestion() { + guard let controller = self.controller else { + return + } + let tooltipController = UndoOverlayController( + presentationData: self.presentationData, + content: .linkCopied( + text: self.presentationData.strings.Story_Editor_TooltipLinkPremium + ), + elevatedLayout: true, + position: .top, + animateInAsReplacement: false, action: { [weak controller] action in + if case .info = action, let controller { + let _ = controller.completion(nil) + controller.dismiss(animated: true) + + let premiumController = controller.context.sharedContext.makePremiumIntroController(context: controller.context, source: .storiesLinks, forceDark: controller.forceDark, dismissed: nil) + controller.pushController(premiumController) + return true + } + return false + } + ) + controller.present(tooltipController, in: .window(.root)) + } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { guard let controller = self.controller else { @@ -2440,7 +2473,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 { + if spacing < context.component.minSpacing || currentGroup.count == 2 { groups.append(currentGroup) currentGroup = [] } @@ -2493,19 +2526,29 @@ final class ItemStack: CombinedComponent { } } - final class StoryStickersContentView: UIView, EmojiCustomContentView { let tintContainerView = UIView() private let container = ComponentView() + private let isPremium: Bool + var locationAction: () -> Void = {} var audioAction: () -> Void = {} var reactionAction: () -> Void = {} var linkAction: () -> Void = {} + init(isPremium: Bool) { + self.isPremium = isPremium + + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize { - //TODO:localize let padding: CGFloat = 22.0 let size = self.container.update( transition: transition, @@ -2521,7 +2564,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { component: AnyComponent( InteractiveStickerButtonContent( theme: theme, - title: "LINK", + title: strings.MediaEditor_AddLink, iconName: "Premium/Link", useOpaqueTheme: useOpaqueTheme, tintContainerView: self.tintContainerView diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json new file mode 100644 index 0000000000..9a5e75bd65 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "albumoff_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf new file mode 100644 index 0000000000..3cd6c8100e Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf differ diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json new file mode 100644 index 0000000000..a1f89c5d89 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cash.circle_24.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf new file mode 100644 index 0000000000..65dfc29832 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf differ diff --git a/submodules/TelegramUI/Sources/AccountContext.swift b/submodules/TelegramUI/Sources/AccountContext.swift index a96aa6768b..57272623f8 100644 --- a/submodules/TelegramUI/Sources/AccountContext.swift +++ b/submodules/TelegramUI/Sources/AccountContext.swift @@ -295,7 +295,7 @@ public final class AccountContextImpl: AccountContext { self.themeUpdateManager = ThemeUpdateManagerImpl(sharedContext: sharedContext, account: account) self.inAppPurchaseManager = InAppPurchaseManager(engine: self.engine) - self.starsContext = self.engine.payments.peerStarsContext(peerId: account.peerId) + self.starsContext = self.engine.payments.peerStarsContext() } else { self.prefetchManager = nil self.wallpaperUploadManager = nil diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 70c122ace2..7bf7134d64 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -126,6 +126,8 @@ import ChatMediaInputStickerGridItem import AdsInfoScreen import MessageUI import PhoneNumberFormat +import OwnershipTransferController +import OldChannelsController public enum ChatControllerPeekActions { case standard @@ -870,6 +872,42 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } + + #if DEBUG + if message.text == "#", let telegramImage = message.media.first(where: { $0 is TelegramMediaImage }) as? TelegramMediaImage { + let invoice = TelegramMediaInvoice(title: "", description: "", photo: nil, receiptMessageId: nil, currency: "XTR", totalAmount: 100, startParam: "", extendedMedia: .preview(dimensions: telegramImage.representations.first?.dimensions ?? PixelDimensions(width: 1, height: 1), immediateThumbnailData: telegramImage.immediateThumbnailData, videoDuration: nil), flags: [], version: 0) + + let inputData = Promise() + inputData.set(.single( + BotCheckoutController.InputData( + form: BotPaymentForm(id: 123, canSaveCredentials: false, passwordMissing: false, invoice: BotPaymentInvoice(isTest: false, requestedFields: [], currency: "XTR", prices: [BotPaymentPrice(label: "", amount: 100)], tip: nil, termsInfo: nil), paymentBotId: message.id.peerId, providerId: nil, url: nil, nativeProvider: nil, savedInfo: nil, savedCredentials: [], additionalPaymentMethods: []), + validatedFormInfo: nil, + botPeer: nil + ))) + if invoice.currency == "XTR", let starsContext = strongSelf.context.starsContext { + let starsInputData = combineLatest( + inputData.get(), + starsContext.state + ) + |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in + if let data, let state { + return (state, data.form, data.botPeer) + } else { + return nil + } + } + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let strongSelf = self else { + return + } + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(message.id), inputData: starsInputData, completion: { _ in }) + strongSelf.push(controller) + }) + } + return true + } + #endif + if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia { switch extendedMedia { case .preview: diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index c5f11b6048..d7cdbd02da 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -19,6 +19,7 @@ import MapResourceToAvatarSizes import LegacyMediaPickerUI import TextFormat import AvatarEditorScreen +import OldChannelsController private struct CreateChannelArguments { let context: AccountContext diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index d1108c444d..3532b0b827 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -32,6 +32,7 @@ import AsyncDisplayKit import TextFormat import AvatarEditorScreen import SendInviteLinkScreen +import OldChannelsController private struct CreateGroupArguments { let context: AccountContext diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index dded619dee..867d19bfeb 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2054,6 +2054,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .storiesSuggestedReactions case .storiesHigherQuality: mappedSource = .storiesHigherQuality + case .storiesLinks: + mappedSource = .storiesLinks case let .channelBoost(peerId): mappedSource = .channelBoost(peerId) case .nameColor: @@ -2638,8 +2640,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionScreen(context: context, subject: .receipt(receipt), action: {}) } - public func makeStarsStatisticsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController { - return StarsStatisticsScreen(context: context, starsContext: starsContext) + public func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController { + return StarsStatisticsScreen(context: context, peerId: peerId, revenueContext: revenueContext) + } + + public func makeStarsAmountScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsWithdrawScreen(context: context, mode: .paidMedia, completion: completion) } } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 52dc6e7292..67f47dff20 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -407,7 +407,7 @@ public final class ChatTextInputTextCustomEmojiAttribute: NSObject, Codable { public enum Custom: Codable { case topic(id: Int64, info: EngineMessageHistoryThread.Info) case nameColors([UInt32]) - case stars + case stars(tinted: Bool) } public let interactivelySelectedFromPackId: ItemCollectionId? diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 98769d41da..275ed4cd06 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -1934,6 +1934,16 @@ public final class WebAppController: ViewController, AttachmentContainable { let attachMenuBot = attachMenuBots.first(where: { $0.peer.id == botId && !$0.flags.contains(.notActivated) }) +// items.append(.action(ContextMenuActionItem(text: "Minimize", icon: { theme in +// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/HideArchive"), color: theme.contextMenu.primaryColor) +// }, action: { [weak self] c, _ in +// c?.dismiss(completion: nil) +// +// if let self, let parentController = self.parentController(), let navigationController = self.getNavigationController() { +// navigationController.minimizeViewController(parentController, animated: true) +// } +// }))) + if hasSettings { items.append(.action(ContextMenuActionItem(text: presentationData.strings.WebApp_Settings, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Settings"), color: theme.contextMenu.primaryColor) @@ -2183,5 +2193,9 @@ public func standaloneWebAppController( controller.willDismiss = willDismiss controller.didDismiss = didDismiss controller.getSourceRect = getSourceRect + controller.title = params.botName + controller.shouldMinimizeOnSwipe = { + return false + } return controller }