Various improvements

This commit is contained in:
Ilya Laktyushin 2024-06-15 18:41:19 +04:00
parent 2150d65f78
commit 1ae20bd685
94 changed files with 3242 additions and 534 deletions

View file

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

View file

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

View file

@ -31,6 +31,7 @@ public enum PremiumIntroSource {
case storiesExpirationDurations
case storiesSuggestedReactions
case storiesHigherQuality
case storiesLinks
case channelBoost(EnginePeer.Id)
case nameColor
case similarChannels

View file

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

View file

@ -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<PresentationData, NoError>)? = 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)
}

View file

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

View file

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

View file

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

View file

@ -50,6 +50,7 @@ import FullScreenEffectView
import PeerInfoStoryGridScreen
import ArchiveInfoScreen
import BirthdayPickerScreen
import OldChannelsController
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController

View file

@ -374,7 +374,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: 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)

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String?, NoError> {
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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -92,6 +92,12 @@
- (void)setSpoiler:(bool)spoiler forItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)spoilersUpdatedSignal;
- (NSNumber *)priceForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)priceSignalForItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)priceSignalForIdentifier:(NSString *)identifier;
- (void)setPrice:(NSNumber *)price forItem:(NSObject<TGMediaEditableItem> *)item;
- (SSignal *)pricesUpdatedSignal;
- (UIImage *)paintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (UIImage *)stillPaintingImageForItem:(NSObject<TGMediaEditableItem> *)item;
- (bool)setPaintingData:(NSData *)data entitiesData:(NSData *)entitiesData image:(UIImage *)image stillImage:(UIImage *)stillImage forItem:(NSObject<TGMediaEditableItem> *)item dataUrl:(NSURL **)dataOutUrl entitiesDataUrl:(NSURL **)entitiesDataOutUrl imageUrl:(NSURL **)imageOutUrl forVideo:(bool)video;

View file

@ -64,6 +64,16 @@
@end
@interface TGMediaPriceUpdate : NSObject
@property (nonatomic, readonly, strong) id<TGMediaEditableItem> item;
@property (nonatomic, readonly, strong) NSNumber *price;
+ (instancetype)priceUpdateWithItem:(id<TGMediaEditableItem>)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<TGMediaEditableItem> *)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<TGMediaEditableItem> *)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<TGMediaEditableItem> *)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<TGMediaEditableItem>)item synchronous:(bool)synchronous
@ -1189,3 +1271,23 @@
}
@end
@implementation TGMediaPriceUpdate
+ (instancetype)priceUpdateWithItem:(id<TGMediaEditableItem>)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

View file

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

View file

@ -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<Int64?, NoError> { 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

View file

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

View file

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

View file

@ -14,6 +14,8 @@ import Emoji
import LocalizedPeerData
import Markdown
import SendInviteLinkScreen
import OwnershipTransferController
import OldChannelsController
private let rankMaxLength: Int32 = 16

View file

@ -13,6 +13,7 @@ import AccountContext
import AlertUI
import PresentationDataUtils
import ItemListAvatarAndNameInfoItem
import OldChannelsController
private final class ChannelBannedMemberControllerArguments {
let context: AccountContext

View file

@ -15,6 +15,7 @@ import ItemListPeerItem
import ItemListPeerActionItem
import ChatListFilterSettingsHeaderItem
import UndoUI
import OldChannelsController
private final class ChannelDiscussionGroupSetupControllerArguments {
let context: AccountContext

View file

@ -18,6 +18,7 @@ import ItemListPeerActionItem
import Markdown
import UndoUI
import Postbox
import OldChannelsController
private final class ChannelPermissionsControllerArguments {
let context: AccountContext

View file

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

View file

@ -8,6 +8,7 @@ import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import OldChannelsController
private final class GroupPreHistorySetupArguments {
let toggle: (Bool) -> Void

View file

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

View file

@ -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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (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()

View file

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

View file

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

View file

@ -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<PresentationData, NoError>)? = nil, peerId: EnginePeer.Id, present: @escaping (ViewController, Any?) -> Void, completion: @escaping (String) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }

View file

@ -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<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": [],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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..<count {
for i in 0 ..< count {
if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
let image = UIImage(cgImage: cgImage)
images.append(image)

View file

@ -343,7 +343,7 @@ public final class DrawingMessageRenderer {
}
public func render(completion: @escaping (Result) -> Void) {
Queue.mainQueue().after(0.1) {
Queue.mainQueue().after(0.066) {
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let defaultPresentationData = defaultPresentationData()

View file

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

View file

@ -62,6 +62,7 @@ swift_library(
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/WebsiteType",
"//submodules/UrlEscaping",
],
visibility = [
"//visibility:public",

View file

@ -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<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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()

View file

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

View file

@ -145,6 +145,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/PeerSelectionScreen",
"//submodules/ConfettiEffect",
"//submodules/ContactsPeerItem",
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
],
visibility = [
"//visibility:public",

View file

@ -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<StarsRevenueStatsContext?>(nil)
let starsRevenueStatsStatePromise = Promise<StarsRevenueStats?>(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
))
}
}

View file

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

View file

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

View file

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

View file

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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)

View file

@ -12,7 +12,6 @@ import AlertUI
import PresentationDataUtils
import PasswordSetupUI
import Markdown
import PeerInfoUI
private func commitOwnershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>, 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<PresentationData, NoError>)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController {
public func ownershipTransferController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, initialError: MessageActionCallbackError, present: @escaping (ViewController, Any?) -> Void, commit: @escaping (String) -> Signal<MessageActionCallbackResult, MessageActionCallbackError>, completion: @escaping (MessageActionCallbackResult) -> Void) -> ViewController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let theme = AlertControllerTheme(presentationData: presentationData)

View file

@ -21,6 +21,7 @@ swift_library(
"//submodules/PhotoResources",
"//submodules/AvatarNode",
"//submodules/AccountContext",
"//submodules/InvisibleInkDustNode",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

@ -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<Empty>()
private let usdValue = ComponentView<Empty>()
private let title = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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<PresentationData, NoError>)? = 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<PresentationData, NoError>)? = 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)
}

View file

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

View file

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

View file

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

View file

@ -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<Empty>?
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<Void>.self)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(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<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.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<Empty>
private let iconView: UIImageView
private let textField: TextFieldNodeView
private var component: AmountFieldComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.placeholderView = ComponentView<Empty>()
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<EnvironmentType>, 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<EnvironmentType>, 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
}
}
}

View file

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

View file

@ -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<ChildEnvironment: Equatable>: 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<ChildEnvironment: Equatable>: CombinedComponent {
}
}
final class StoryStickersContentView: UIView, EmojiCustomContentView {
let tintContainerView = UIView()
private let container = ComponentView<Empty>()
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

View file

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

View file

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

View file

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

View file

@ -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<BotCheckoutController.InputData?>()
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:

View file

@ -19,6 +19,7 @@ import MapResourceToAvatarSizes
import LegacyMediaPickerUI
import TextFormat
import AvatarEditorScreen
import OldChannelsController
private struct CreateChannelArguments {
let context: AccountContext

View file

@ -32,6 +32,7 @@ import AsyncDisplayKit
import TextFormat
import AvatarEditorScreen
import SendInviteLinkScreen
import OldChannelsController
private struct CreateGroupArguments {
let context: AccountContext

View file

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

View file

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

View file

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