mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Various improvements
This commit is contained in:
parent
2150d65f78
commit
1ae20bd685
94 changed files with 3242 additions and 534 deletions
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public enum PremiumIntroSource {
|
|||
case storiesExpirationDurations
|
||||
case storiesSuggestedReactions
|
||||
case storiesHigherQuality
|
||||
case storiesLinks
|
||||
case channelBoost(EnginePeer.Id)
|
||||
case nameColor
|
||||
case similarChannels
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import FullScreenEffectView
|
|||
import PeerInfoStoryGridScreen
|
||||
import ArchiveInfoScreen
|
||||
import BirthdayPickerScreen
|
||||
import OldChannelsController
|
||||
|
||||
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
|
||||
let controller: ViewController
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
620
submodules/DrawingUI/Sources/DrawingLinkEntityView.swift
Normal file
620
submodules/DrawingUI/Sources/DrawingLinkEntityView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import Emoji
|
|||
import LocalizedPeerData
|
||||
import Markdown
|
||||
import SendInviteLinkScreen
|
||||
import OwnershipTransferController
|
||||
import OldChannelsController
|
||||
|
||||
private let rankMaxLength: Int32 = 16
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import AccountContext
|
|||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import ItemListAvatarAndNameInfoItem
|
||||
import OldChannelsController
|
||||
|
||||
private final class ChannelBannedMemberControllerArguments {
|
||||
let context: AccountContext
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import ItemListPeerItem
|
|||
import ItemListPeerActionItem
|
||||
import ChatListFilterSettingsHeaderItem
|
||||
import UndoUI
|
||||
import OldChannelsController
|
||||
|
||||
private final class ChannelDiscussionGroupSetupControllerArguments {
|
||||
let context: AccountContext
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import ItemListPeerActionItem
|
|||
import Markdown
|
||||
import UndoUI
|
||||
import Postbox
|
||||
import OldChannelsController
|
||||
|
||||
private final class ChannelPermissionsControllerArguments {
|
||||
let context: AccountContext
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import TelegramPresentationData
|
|||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import OldChannelsController
|
||||
|
||||
private final class GroupPreHistorySetupArguments {
|
||||
let toggle: (Bool) -> Void
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ swift_library(
|
|||
"//submodules/Components/HierarchyTrackingLayer",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/WebsiteType",
|
||||
"//submodules/UrlEscaping",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/Settings/PeerSelectionScreen",
|
||||
"//submodules/ConfettiEffect",
|
||||
"//submodules/ContactsPeerItem",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ swift_library(
|
|||
"//submodules/PhotoResources",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/InvisibleInkDustNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "albumoff_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/GroupingOff.imageset/albumoff_24.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "cash.circle_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Media Grid/Paid.imageset/cash.circle_24.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import MapResourceToAvatarSizes
|
|||
import LegacyMediaPickerUI
|
||||
import TextFormat
|
||||
import AvatarEditorScreen
|
||||
import OldChannelsController
|
||||
|
||||
private struct CreateChannelArguments {
|
||||
let context: AccountContext
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import AsyncDisplayKit
|
|||
import TextFormat
|
||||
import AvatarEditorScreen
|
||||
import SendInviteLinkScreen
|
||||
import OldChannelsController
|
||||
|
||||
private struct CreateGroupArguments {
|
||||
let context: AccountContext
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue