mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
2370 lines
113 KiB
Swift
2370 lines
113 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import ComponentFlow
|
|
import ViewControllerComponent
|
|
import SheetComponent
|
|
import MultilineTextComponent
|
|
import MultilineTextWithEntitiesComponent
|
|
import BundleIconComponent
|
|
import ButtonComponent
|
|
import Markdown
|
|
import BalancedTextComponent
|
|
import AvatarNode
|
|
import TextFormat
|
|
import TelegramStringFormatting
|
|
import StarsAvatarComponent
|
|
import EmojiTextAttachmentView
|
|
import UndoUI
|
|
import ConfettiEffect
|
|
import PlainButtonComponent
|
|
import CheckComponent
|
|
import TooltipUI
|
|
|
|
private let modelButtonTag = GenericComponentViewTag()
|
|
private let backdropButtonTag = GenericComponentViewTag()
|
|
private let symbolButtonTag = GenericComponentViewTag()
|
|
|
|
private final class GiftViewSheetContent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let subject: GiftViewScreen.Subject
|
|
let cancel: (Bool) -> Void
|
|
let openPeer: (EnginePeer) -> Void
|
|
let updateSavedToProfile: (Bool) -> Void
|
|
let convertToStars: () -> Void
|
|
let openStarsIntro: () -> Void
|
|
let sendGift: (EnginePeer.Id) -> Void
|
|
let openMyGifts: () -> Void
|
|
let transferGift: () -> Void
|
|
let showAttributeInfo: (Any) -> Void
|
|
let getController: () -> ViewController?
|
|
|
|
init(
|
|
context: AccountContext,
|
|
subject: GiftViewScreen.Subject,
|
|
cancel: @escaping (Bool) -> Void,
|
|
openPeer: @escaping (EnginePeer) -> Void,
|
|
updateSavedToProfile: @escaping (Bool) -> Void,
|
|
convertToStars: @escaping () -> Void,
|
|
openStarsIntro: @escaping () -> Void,
|
|
sendGift: @escaping (EnginePeer.Id) -> Void,
|
|
openMyGifts: @escaping () -> Void,
|
|
transferGift: @escaping () -> Void,
|
|
showAttributeInfo: @escaping (Any) -> Void,
|
|
getController: @escaping () -> ViewController?
|
|
) {
|
|
self.context = context
|
|
self.subject = subject
|
|
self.cancel = cancel
|
|
self.openPeer = openPeer
|
|
self.updateSavedToProfile = updateSavedToProfile
|
|
self.convertToStars = convertToStars
|
|
self.openStarsIntro = openStarsIntro
|
|
self.sendGift = sendGift
|
|
self.openMyGifts = openMyGifts
|
|
self.transferGift = transferGift
|
|
self.showAttributeInfo = showAttributeInfo
|
|
self.getController = getController
|
|
}
|
|
|
|
static func ==(lhs: GiftViewSheetContent, rhs: GiftViewSheetContent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
private let context: AccountContext
|
|
private let getController: () -> ViewController?
|
|
|
|
private var disposable: Disposable?
|
|
var initialized = false
|
|
|
|
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
|
|
var starGiftsMap: [Int64: StarGift.Gift] = [:]
|
|
|
|
var cachedCircleImage: UIImage?
|
|
var cachedStarImage: (UIImage, PresentationTheme)?
|
|
var cachedCloseImage: (UIImage, PresentationTheme)?
|
|
var cachedOverlayCloseImage: UIImage?
|
|
|
|
var cachedChevronImage: (UIImage, PresentationTheme)?
|
|
var cachedSmallChevronImage: (UIImage, PresentationTheme)?
|
|
|
|
var inProgress = false
|
|
|
|
var inUpgrade = false
|
|
var isUpgradedMock = false
|
|
|
|
var keepName = false
|
|
|
|
var mockFiles: [TelegramMediaFile] = []
|
|
var mockIconFiles: [TelegramMediaFile] = []
|
|
var upgradedMockId: Int = 0
|
|
var upgradedMockBackgroundColor: UIColor = .white
|
|
var upgradedMockIcon: TelegramMediaFile?
|
|
|
|
init(context: AccountContext, subject: GiftViewScreen.Subject, getController: @escaping () -> ViewController?) {
|
|
self.context = context
|
|
self.getController = getController
|
|
|
|
super.init()
|
|
|
|
if let arguments = subject.arguments {
|
|
var peerIds: [EnginePeer.Id] = [arguments.peerId, context.account.peerId]
|
|
if let fromPeerId = arguments.fromPeerId, !peerIds.contains(fromPeerId) {
|
|
peerIds.append(fromPeerId)
|
|
}
|
|
self.disposable = combineLatest(queue: Queue.mainQueue(),
|
|
context.engine.data.get(EngineDataMap(
|
|
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
|
|
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
|
}
|
|
)),
|
|
.single(nil) |> then(context.engine.payments.cachedStarGifts())
|
|
).startStrict(next: { [weak self] peers, starGifts in
|
|
if let strongSelf = self {
|
|
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
|
|
for peerId in peerIds {
|
|
if let maybePeer = peers[peerId], let peer = maybePeer {
|
|
peersMap[peerId] = peer
|
|
}
|
|
}
|
|
strongSelf.peerMap = peersMap
|
|
|
|
var starGiftsMap: [Int64: StarGift.Gift] = [:]
|
|
if let starGifts {
|
|
for gift in starGifts {
|
|
if case let .generic(gift) = gift {
|
|
starGiftsMap[gift.id] = gift
|
|
}
|
|
}
|
|
}
|
|
strongSelf.starGiftsMap = starGiftsMap
|
|
|
|
strongSelf.initialized = true
|
|
|
|
strongSelf.updated(transition: .immediate)
|
|
}
|
|
})
|
|
}
|
|
|
|
#if DEBUG
|
|
let _ = combineLatest(
|
|
queue: Queue.mainQueue(),
|
|
self.context.engine.stickers.loadedStickerPack(reference: .name("RingColors"), forceActualized: false),
|
|
self.context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false)
|
|
|> map { result -> [TelegramMediaFile] in
|
|
switch result {
|
|
case let .result(_, items, _):
|
|
return items.map(\.file)
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|> take(1)
|
|
).start(next: { [weak self] result, icons in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if case let .result(_, items, _) = result {
|
|
self.mockFiles = items.map { $0.file }
|
|
self.mockIconFiles = icons
|
|
self.updated()
|
|
}
|
|
})
|
|
#endif
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
func doMockUpgrade() {
|
|
self.inUpgrade = false
|
|
self.isUpgradedMock = true
|
|
self.upgradedMockId = Int.random(in: 0 ..< self.mockFiles.count)
|
|
self.upgradedMockBackgroundColor = self.context.peerNameColors.profileColors.values.randomElement()?.main ?? .white
|
|
self.upgradedMockIcon = self.mockIconFiles.randomElement()
|
|
self.updated(transition: .spring(duration: 0.4))
|
|
|
|
if let controller = self.getController() {
|
|
controller.view.addSubview(ConfettiView(frame: controller.view.bounds))
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State(context: self.context, subject: self.subject, getController: self.getController)
|
|
}
|
|
|
|
static var body: Body {
|
|
let closeButton = Child(Button.self)
|
|
let animation = Child(GiftCompositionComponent.self)
|
|
let title = Child(MultilineTextComponent.self)
|
|
let description = Child(MultilineTextComponent.self)
|
|
let hiddenText = Child(MultilineTextComponent.self)
|
|
let table = Child(TableComponent.self)
|
|
let additionalText = Child(MultilineTextComponent.self)
|
|
let button = Child(ButtonComponent.self)
|
|
|
|
let upgradeTitle = Child(MultilineTextComponent.self)
|
|
let upgradeDescription = Child(BalancedTextComponent.self)
|
|
let upgradePerks = Child(List<Empty>.self)
|
|
let upgradeKeepName = Child(PlainButtonComponent.self)
|
|
|
|
let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: [])
|
|
|
|
return { context in
|
|
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
|
|
|
let component = context.component
|
|
let theme = environment.theme
|
|
let strings = environment.strings
|
|
let dateTimeFormat = environment.dateTimeFormat
|
|
|
|
let state = context.state
|
|
|
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
|
|
|
let closeImage: UIImage
|
|
let closeOverlayImage: 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)
|
|
}
|
|
if let image = state.cachedOverlayCloseImage {
|
|
closeOverlayImage = image
|
|
} else {
|
|
closeOverlayImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.1), foregroundColor: .white)!
|
|
state.cachedOverlayCloseImage = closeOverlayImage
|
|
}
|
|
|
|
let closeButton = closeButton.update(
|
|
component: Button(
|
|
content: AnyComponent(Image(image: state.inUpgrade || state.isUpgradedMock ? closeOverlayImage : closeImage)),
|
|
action: { [weak component] in
|
|
component?.cancel(true)
|
|
}
|
|
),
|
|
availableSize: CGSize(width: 30.0, height: 30.0),
|
|
transition: .immediate
|
|
)
|
|
|
|
var titleString: String
|
|
var animationFile: TelegramMediaFile?
|
|
let stars: Int64
|
|
let convertStars: Int64?
|
|
let text: String?
|
|
let entities: [MessageTextEntity]?
|
|
let limitTotal: Int32?
|
|
var incoming = false
|
|
var savedToProfile = false
|
|
var converted = false
|
|
var giftId: Int64 = 0
|
|
var date: Int32?
|
|
var soldOut = false
|
|
var nameHidden = false
|
|
var canUpgrade = false
|
|
if case let .soldOutGift(gift) = component.subject {
|
|
animationFile = gift.file
|
|
stars = gift.price
|
|
text = nil
|
|
entities = nil
|
|
limitTotal = gift.availability?.total
|
|
convertStars = nil
|
|
soldOut = true
|
|
titleString = strings.Gift_View_UnavailableTitle
|
|
} else if let arguments = component.subject.arguments, case let .generic(gift) = arguments.gift {
|
|
animationFile = gift.file
|
|
stars = gift.price
|
|
text = arguments.text
|
|
entities = arguments.entities
|
|
limitTotal = gift.availability?.total
|
|
convertStars = arguments.convertStars
|
|
incoming = arguments.incoming || arguments.peerId == component.context.account.peerId
|
|
savedToProfile = arguments.savedToProfile
|
|
converted = arguments.converted
|
|
giftId = gift.id
|
|
date = arguments.date
|
|
titleString = incoming ? strings.Gift_View_ReceivedTitle : strings.Gift_View_Title
|
|
nameHidden = arguments.nameHidden
|
|
canUpgrade = arguments.canUpgrade
|
|
} else {
|
|
animationFile = nil
|
|
stars = 0
|
|
text = nil
|
|
entities = nil
|
|
limitTotal = nil
|
|
convertStars = nil
|
|
titleString = ""
|
|
}
|
|
|
|
#if DEBUG
|
|
if state.mockFiles.count > 1 {
|
|
animationFile = state.mockFiles[1]
|
|
}
|
|
#endif
|
|
|
|
var originY: CGFloat = 0.0
|
|
if let animationFile {
|
|
let animationHeight: CGFloat
|
|
let animationSubject: GiftCompositionComponent.Subject
|
|
if state.isUpgradedMock {
|
|
animationHeight = 240.0
|
|
animationSubject = .unique(state.mockFiles[state.upgradedMockId], state.upgradedMockBackgroundColor, state.upgradedMockIcon)
|
|
} else if state.inUpgrade {
|
|
animationHeight = 258.0
|
|
animationSubject = .preview(state.mockFiles, state.mockIconFiles)
|
|
} else {
|
|
animationHeight = 210.0
|
|
if !state.mockFiles.isEmpty {
|
|
animationSubject = .generic(state.mockFiles[1])
|
|
} else {
|
|
animationSubject = .generic(animationFile)
|
|
}
|
|
}
|
|
let animation = animation.update(
|
|
component: GiftCompositionComponent(
|
|
context: component.context,
|
|
theme: environment.theme,
|
|
subject: animationSubject
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width, height: animationHeight),
|
|
transition: .immediate
|
|
)
|
|
context.add(animation
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: animationHeight / 2.0))
|
|
)
|
|
originY += animationHeight
|
|
}
|
|
|
|
if state.inUpgrade {
|
|
let upgradeTitle = upgradeTitle.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.Gift_Upgrade_Title,
|
|
font: Font.bold(20.0),
|
|
textColor: .white,
|
|
paragraphAlignment: .center
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 1
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(upgradeTitle
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: 191.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
|
|
let upgradeDescription = upgradeDescription.update(
|
|
component: BalancedTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: environment.strings.Gift_Upgrade_Description,
|
|
font: Font.regular(13.0),
|
|
textColor: UIColor.white.withAlphaComponent(0.6),
|
|
paragraphAlignment: .center
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 5,
|
|
lineSpacing: 0.2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(upgradeDescription
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: 208.0 + upgradeDescription.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
originY += 24.0
|
|
|
|
let textColor = theme.actionSheet.primaryTextColor
|
|
let secondaryTextColor = theme.actionSheet.secondaryTextColor
|
|
let linkColor = theme.actionSheet.controlAccentColor
|
|
|
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "unique",
|
|
component: AnyComponent(ParagraphComponent(
|
|
title: strings.Gift_Upgrade_Unique_Title,
|
|
titleColor: textColor,
|
|
text: strings.Gift_Upgrade_Unique_Description,
|
|
textColor: secondaryTextColor,
|
|
accentColor: linkColor,
|
|
iconName: "Premium/Collectible/Unique",
|
|
iconColor: linkColor
|
|
))
|
|
)
|
|
)
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "transferable",
|
|
component: AnyComponent(ParagraphComponent(
|
|
title: strings.Gift_Upgrade_Transferable_Title,
|
|
titleColor: textColor,
|
|
text: strings.Gift_Upgrade_Transferable_Description,
|
|
textColor: secondaryTextColor,
|
|
accentColor: linkColor,
|
|
iconName: "Premium/Collectible/Transferable",
|
|
iconColor: linkColor
|
|
))
|
|
)
|
|
)
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: "tradable",
|
|
component: AnyComponent(ParagraphComponent(
|
|
title: strings.Gift_Upgrade_Tradable_Title,
|
|
titleColor: textColor,
|
|
text: strings.Gift_Upgrade_Tradable_Description,
|
|
textColor: secondaryTextColor,
|
|
accentColor: linkColor,
|
|
iconName: "Premium/Collectible/Tradable",
|
|
iconColor: linkColor,
|
|
badge: strings.Gift_Upgrade_Soon
|
|
))
|
|
)
|
|
)
|
|
|
|
let perksSideInset = sideInset + 12.0
|
|
let upgradePerks = upgradePerks.update(
|
|
component: List(items),
|
|
availableSize: CGSize(width: context.availableSize.width - perksSideInset * 2.0, height: 10000.0),
|
|
transition: context.transition
|
|
)
|
|
context.add(upgradePerks
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + upgradePerks.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
originY += upgradePerks.size.height
|
|
originY += 16.0
|
|
|
|
let checkTheme = CheckComponent.Theme(
|
|
backgroundColor: theme.list.itemCheckColors.fillColor,
|
|
strokeColor: theme.list.itemCheckColors.foregroundColor,
|
|
borderColor: theme.list.itemCheckColors.strokeColor,
|
|
overlayBorder: false,
|
|
hasInset: false,
|
|
hasShadow: false
|
|
)
|
|
let upgradeKeepName = upgradeKeepName.update(
|
|
component: PlainButtonComponent(
|
|
content: AnyComponent(HStack([
|
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(CheckComponent(
|
|
theme: checkTheme,
|
|
size: CGSize(width: 18.0, height: 18.0),
|
|
selected: state.keepName
|
|
))),
|
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: strings.Gift_Upgrade_AddName, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor))
|
|
)))
|
|
],
|
|
spacing: 10.0
|
|
)),
|
|
effectAlignment: .center,
|
|
action: { [weak state] in
|
|
guard let state else {
|
|
return
|
|
}
|
|
state.keepName = !state.keepName
|
|
state.updated(transition: .easeInOut(duration: 0.2))
|
|
},
|
|
animateAlpha: false,
|
|
animateScale: false
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 1000.0),
|
|
transition: context.transition
|
|
)
|
|
context.add(upgradeKeepName
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + upgradeKeepName.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
originY += upgradeKeepName.size.height
|
|
originY += 18.0
|
|
} else {
|
|
var descriptionText: String
|
|
if state.isUpgradedMock {
|
|
//TODO:localize
|
|
titleString = "Ring"
|
|
descriptionText = "\(strings.Gift_Unique_Collectible) #175"
|
|
} else if soldOut {
|
|
descriptionText = strings.Gift_View_UnavailableDescription
|
|
} else if incoming {
|
|
if let convertStars {
|
|
if !converted {
|
|
descriptionText = strings.Gift_View_KeepUpgradeOrConvertDescription(strings.Gift_View_KeepOrConvertDescription_Stars(Int32(convertStars))).string
|
|
} else {
|
|
descriptionText = strings.Gift_View_ConvertedDescription(strings.Gift_View_ConvertedDescription_Stars(Int32(convertStars))).string
|
|
}
|
|
} else {
|
|
descriptionText = strings.Gift_View_BotDescription
|
|
}
|
|
} else if let peerId = component.subject.arguments?.peerId, let peer = state.peerMap[peerId] {
|
|
if case .message = component.subject, let convertStars {
|
|
descriptionText = strings.Gift_View_OtherDescription(peer.compactDisplayTitle, strings.Gift_View_OtherDescription_Stars(Int32(convertStars))).string
|
|
} else {
|
|
descriptionText = ""
|
|
}
|
|
} else {
|
|
descriptionText = ""
|
|
}
|
|
if let spaceRegex {
|
|
let nsRange = NSRange(descriptionText.startIndex..., in: descriptionText)
|
|
let matches = spaceRegex.matches(in: descriptionText, options: [], range: nsRange)
|
|
var modifiedString = descriptionText
|
|
|
|
for match in matches.reversed() {
|
|
let matchRange = Range(match.range, in: descriptionText)!
|
|
let matchedSubstring = String(descriptionText[matchRange])
|
|
let replacedSubstring = matchedSubstring.replacingOccurrences(of: " ", with: "\u{00A0}")
|
|
modifiedString.replaceSubrange(matchRange, with: replacedSubstring)
|
|
}
|
|
descriptionText = modifiedString
|
|
}
|
|
|
|
let title = title.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: titleString,
|
|
font: state.isUpgradedMock ? Font.bold(20.0) : Font.bold(25.0),
|
|
textColor: state.isUpgradedMock ? .white : theme.actionSheet.primaryTextColor,
|
|
paragraphAlignment: .center
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 1
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(title
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: state.isUpgradedMock ? 190.0 : 177.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
|
|
// originY += 32.0
|
|
// if soldOut {
|
|
// originY -= 12.0
|
|
// }
|
|
|
|
let linkColor = theme.actionSheet.controlAccentColor
|
|
if !descriptionText.isEmpty {
|
|
if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme {
|
|
state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme)
|
|
}
|
|
|
|
let textFont: UIFont
|
|
let textColor: UIColor
|
|
if state.isUpgradedMock {
|
|
textFont = Font.regular(13.0)
|
|
textColor = UIColor.white.withAlphaComponent(0.6)
|
|
} else {
|
|
textFont = soldOut ? Font.medium(15.0) : Font.regular(15.0)
|
|
textColor = soldOut ? theme.list.itemDestructiveColor : theme.list.itemPrimaryTextColor
|
|
}
|
|
|
|
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
|
return (TelegramTextAttributes.URL, contents)
|
|
})
|
|
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
|
|
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 {
|
|
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
|
|
}
|
|
let description = description.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(attributedString),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 5,
|
|
lineSpacing: 0.2,
|
|
highlightColor: linkColor.withAlphaComponent(0.1),
|
|
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
tapAction: { _, _ in
|
|
component.openStarsIntro()
|
|
}
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(description
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: 207.0 + description.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
|
|
if state.isUpgradedMock {
|
|
originY += 16.0
|
|
} else {
|
|
originY += description.size.height + 21.0
|
|
if soldOut {
|
|
originY -= 7.0
|
|
}
|
|
}
|
|
} else {
|
|
originY += 21.0
|
|
}
|
|
|
|
if nameHidden && incoming && !state.isUpgradedMock {
|
|
let textFont = Font.regular(13.0)
|
|
let textColor = theme.list.itemSecondaryTextColor
|
|
|
|
let hiddenText = hiddenText.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: text != nil ? strings.Gift_View_NameAndMessageHidden : strings.Gift_View_NameHidden, font: textFont, textColor: textColor)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 2,
|
|
lineSpacing: 0.2
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(hiddenText
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY))
|
|
)
|
|
|
|
originY += hiddenText.size.height
|
|
originY += 11.0
|
|
}
|
|
|
|
let tableFont = Font.regular(15.0)
|
|
let tableBoldFont = Font.semibold(15.0)
|
|
let tableItalicFont = Font.italic(15.0)
|
|
let tableBoldItalicFont = Font.semiboldItalic(15.0)
|
|
let tableMonospaceFont = Font.monospace(15.0)
|
|
|
|
let tableTextColor = theme.list.itemPrimaryTextColor
|
|
let tableLinkColor = theme.list.itemAccentColor
|
|
var tableItems: [TableComponent.Item] = []
|
|
|
|
if !soldOut {
|
|
if state.isUpgradedMock {
|
|
if let peer = state.peerMap[component.context.account.peerId] {
|
|
let ownerComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(
|
|
PeerCellComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
strings: strings,
|
|
peer: peer
|
|
)
|
|
),
|
|
action: {
|
|
component.openPeer(peer)
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: strings.Gift_Unique_Transfer,
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
component.transferGift()
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
tableItems.append(.init(
|
|
id: "owner",
|
|
title: strings.Gift_Unique_Owner,
|
|
component: ownerComponent
|
|
))
|
|
}
|
|
} else if let peerId = component.subject.arguments?.fromPeerId, let peer = state.peerMap[peerId] {
|
|
let fromComponent: AnyComponent<Empty>
|
|
if incoming {
|
|
fromComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(
|
|
PeerCellComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
strings: strings,
|
|
peer: peer
|
|
)
|
|
),
|
|
action: {
|
|
component.openPeer(peer)
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: strings.Gift_View_Send,
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
component.sendGift(peerId)
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
} else {
|
|
fromComponent = AnyComponent(Button(
|
|
content: AnyComponent(
|
|
PeerCellComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
strings: strings,
|
|
peer: peer
|
|
)
|
|
),
|
|
action: {
|
|
component.openPeer(peer)
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
))
|
|
}
|
|
tableItems.append(.init(
|
|
id: "from",
|
|
title: strings.Gift_View_From,
|
|
component: fromComponent
|
|
))
|
|
} else {
|
|
tableItems.append(.init(
|
|
id: "from_anon",
|
|
title: strings.Gift_View_From,
|
|
component: AnyComponent(
|
|
PeerCellComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
strings: strings,
|
|
peer: nil
|
|
)
|
|
)
|
|
))
|
|
}
|
|
}
|
|
|
|
if state.isUpgradedMock {
|
|
let showAttributeInfo = component.showAttributeInfo
|
|
let modelComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Silver Glamour", font: tableFont, textColor: tableTextColor))))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: "2%",
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
showAttributeInfo(modelButtonTag)
|
|
}
|
|
).tagged(modelButtonTag))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
|
|
tableItems.append(.init(
|
|
id: "model",
|
|
title: strings.Gift_Unique_Model,
|
|
component: modelComponent
|
|
))
|
|
|
|
let circleImage: UIImage
|
|
if let image = state.cachedCircleImage {
|
|
circleImage = image
|
|
} else {
|
|
circleImage = generateFilledCircleImage(diameter: 14.0, color: .white)!.withRenderingMode(.alwaysTemplate)
|
|
state.cachedCircleImage = circleImage
|
|
}
|
|
|
|
let backdropComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(Image(image: circleImage, tintColor: state.upgradedMockBackgroundColor, size: CGSize(width: 14.0, height: 14.0)))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Green", font: tableFont, textColor: tableTextColor))))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(2),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: "5%",
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
showAttributeInfo(backdropButtonTag)
|
|
}
|
|
).tagged(backdropButtonTag))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
tableItems.append(.init(
|
|
id: "backdrop",
|
|
title: strings.Gift_Unique_Backdrop,
|
|
component: backdropComponent
|
|
))
|
|
|
|
let symbolComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Monkey", font: tableFont, textColor: tableTextColor))))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: "2%",
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
showAttributeInfo(symbolButtonTag)
|
|
}
|
|
).tagged(symbolButtonTag))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
tableItems.append(.init(
|
|
id: "symbol",
|
|
title: strings.Gift_Unique_Symbol,
|
|
component: symbolComponent
|
|
))
|
|
|
|
tableItems.append(.init(
|
|
id: "availability",
|
|
title: strings.Gift_Unique_Availability,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Unique_Issued("354/985").string, font: tableFont, textColor: tableTextColor)))
|
|
)
|
|
))
|
|
} else {
|
|
if case let .soldOutGift(gift) = component.subject, let soldOut = gift.soldOut {
|
|
tableItems.append(.init(
|
|
id: "firstDate",
|
|
title: strings.Gift_View_FirstSale,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.firstSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
|
|
)
|
|
))
|
|
|
|
tableItems.append(.init(
|
|
id: "lastDate",
|
|
title: strings.Gift_View_LastSale,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: soldOut.lastSale, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
|
|
)
|
|
))
|
|
} else if let date {
|
|
tableItems.append(.init(
|
|
id: "date",
|
|
title: strings.Gift_View_Date,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
|
|
)
|
|
))
|
|
}
|
|
|
|
let valueString = "⭐️\(presentationStringsFormattedNumber(abs(Int32(stars)), dateTimeFormat.groupingSeparator))"
|
|
let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor)
|
|
let range = (valueAttributedString.string as NSString).range(of: "⭐️")
|
|
if range.location != NSNotFound {
|
|
valueAttributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: false)), range: range)
|
|
valueAttributedString.addAttribute(.baselineOffset, value: 1.0, range: range)
|
|
}
|
|
|
|
let valueComponent: AnyComponent<Empty>
|
|
if let convertStars, incoming && !converted {
|
|
valueComponent = AnyComponent(
|
|
HStack([
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: theme.list.mediaPlaceholderColor,
|
|
text: .plain(valueAttributedString),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
),
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: strings.Gift_View_Sale(strings.Gift_View_Sale_Stars(Int32(convertStars))).string,
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: {
|
|
component.convertToStars()
|
|
}
|
|
))
|
|
)
|
|
], spacing: 4.0)
|
|
)
|
|
} else {
|
|
valueComponent = AnyComponent(MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: theme.list.mediaPlaceholderColor,
|
|
text: .plain(valueAttributedString),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
}
|
|
|
|
tableItems.append(.init(
|
|
id: "value",
|
|
title: strings.Gift_View_Value,
|
|
component: valueComponent,
|
|
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
|
|
))
|
|
|
|
if let limitTotal {
|
|
var remains: Int32 = 0
|
|
if let gift = state.starGiftsMap[giftId], let availability = gift.availability {
|
|
remains = availability.remains
|
|
}
|
|
let remainsString = presentationStringsFormattedNumber(remains, environment.dateTimeFormat.groupingSeparator)
|
|
let totalString = presentationStringsFormattedNumber(limitTotal, environment.dateTimeFormat.groupingSeparator)
|
|
tableItems.append(.init(
|
|
id: "availability",
|
|
title: strings.Gift_View_Availability,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Availability_NewOf("\(remainsString)", "\(totalString)").string, font: tableFont, textColor: tableTextColor)))
|
|
)
|
|
))
|
|
}
|
|
|
|
if !soldOut && canUpgrade {
|
|
//TODO:localize
|
|
var items: [AnyComponentWithIdentity<Empty>] = []
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(0),
|
|
component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_View_Status_NonUnique, font: tableFont, textColor: tableTextColor))))
|
|
)
|
|
)
|
|
if incoming {
|
|
items.append(
|
|
AnyComponentWithIdentity(
|
|
id: AnyHashable(1),
|
|
component: AnyComponent(Button(
|
|
content: AnyComponent(ButtonContentComponent(
|
|
context: component.context,
|
|
text: strings.Gift_View_Status_Upgrade,
|
|
color: theme.list.itemAccentColor
|
|
)),
|
|
action: { [weak state] in
|
|
state?.inUpgrade = true
|
|
state?.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
))
|
|
)
|
|
)
|
|
}
|
|
tableItems.append(.init(
|
|
id: "status",
|
|
title: strings.Gift_View_Status,
|
|
component: AnyComponent(
|
|
HStack(items, spacing: 4.0)
|
|
),
|
|
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)
|
|
))
|
|
}
|
|
|
|
if let text {
|
|
let attributedText = stringWithAppliedEntities(text, entities: entities ?? [], baseColor: tableTextColor, linkColor: tableLinkColor, baseFont: tableFont, linkFont: tableFont, boldFont: tableBoldFont, italicFont: tableItalicFont, boldItalicFont: tableBoldItalicFont, fixedFont: tableMonospaceFont, blockQuoteFont: tableFont, message: nil)
|
|
|
|
tableItems.append(.init(
|
|
id: "text",
|
|
title: nil,
|
|
component: AnyComponent(
|
|
MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: theme.list.mediaPlaceholderColor,
|
|
text: .plain(attributedText),
|
|
maximumNumberOfLines: 0,
|
|
handleSpoilers: true
|
|
)
|
|
)
|
|
))
|
|
}
|
|
}
|
|
|
|
let table = table.update(
|
|
component: TableComponent(
|
|
theme: environment.theme,
|
|
items: tableItems
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(table
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
originY += table.size.height + 23.0
|
|
|
|
if incoming && !converted {
|
|
if state.cachedSmallChevronImage == nil || state.cachedSmallChevronImage?.1 !== environment.theme {
|
|
state.cachedSmallChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: linkColor)!, theme)
|
|
}
|
|
let descriptionText = savedToProfile ? strings.Gift_View_DisplayedInfoHide : strings.Gift_View_HiddenInfo
|
|
|
|
let textFont = Font.regular(13.0)
|
|
let textColor = theme.list.itemSecondaryTextColor
|
|
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: textFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
|
|
return (TelegramTextAttributes.URL, contents)
|
|
})
|
|
let attributedString = parseMarkdownIntoAttributedString(descriptionText, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
|
|
if let range = attributedString.string.range(of: ">"), let chevronImage = state.cachedSmallChevronImage?.0 {
|
|
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
|
|
}
|
|
|
|
originY -= 5.0
|
|
let additionalText = additionalText.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(attributedString),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 5,
|
|
lineSpacing: 0.2,
|
|
highlightColor: linkColor.withAlphaComponent(0.1),
|
|
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
tapAction: { _, _ in
|
|
component.updateSavedToProfile(false)
|
|
Queue.mainQueue().after(1.0, {
|
|
component.cancel(false)
|
|
})
|
|
}
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
context.add(additionalText
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additionalText.size.height / 2.0))
|
|
.appear(.default(alpha: true))
|
|
.disappear(.default(alpha: true))
|
|
)
|
|
originY += additionalText.size.height
|
|
originY += 16.0
|
|
}
|
|
}
|
|
|
|
let buttonChild: _UpdatedChildComponent
|
|
if state.inUpgrade {
|
|
if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme {
|
|
state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme)
|
|
}
|
|
|
|
//TODO:localize
|
|
let amountString = "25"
|
|
let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Gift_Upgrade_Upgrade) # \(amountString)", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, 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: environment.theme.list.itemCheckColors.foregroundColor, range: NSRange(range, in: buttonAttributedString.string))
|
|
buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string))
|
|
}
|
|
buttonChild = 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: state.inProgress,
|
|
action: { [weak state] in
|
|
state?.doMockUpgrade()
|
|
}),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
|
transition: context.transition
|
|
)
|
|
} else if incoming && !converted && !savedToProfile {
|
|
buttonChild = 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(NSAttributedString(string: savedToProfile ? strings.Gift_View_Hide : strings.Gift_View_Display, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
|
),
|
|
isEnabled: true,
|
|
displaysProgress: state.inProgress,
|
|
action: {
|
|
component.updateSavedToProfile(!savedToProfile)
|
|
}),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
|
transition: context.transition
|
|
)
|
|
} else {
|
|
buttonChild = 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(NSAttributedString(string: strings.Common_OK, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center))))
|
|
),
|
|
isEnabled: true,
|
|
displaysProgress: state.inProgress,
|
|
action: {
|
|
component.cancel(true)
|
|
}),
|
|
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
|
transition: context.transition
|
|
)
|
|
}
|
|
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: buttonChild.size)
|
|
context.add(buttonChild
|
|
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
|
|
.cornerRadius(10.0)
|
|
)
|
|
originY += buttonChild.size.height
|
|
originY += 7.0
|
|
|
|
context.add(closeButton
|
|
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0))
|
|
)
|
|
|
|
let contentSize = CGSize(width: context.availableSize.width, height: originY + 5.0 + environment.safeInsets.bottom)
|
|
|
|
return contentSize
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class GiftViewSheetComponent: CombinedComponent {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let subject: GiftViewScreen.Subject
|
|
let openPeer: (EnginePeer) -> Void
|
|
let updateSavedToProfile: (Bool) -> Void
|
|
let convertToStars: () -> Void
|
|
let openStarsIntro: () -> Void
|
|
let sendGift: (EnginePeer.Id) -> Void
|
|
let openMyGifts: () -> Void
|
|
let transferGift: () -> Void
|
|
let showAttributeInfo: (Any) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
subject: GiftViewScreen.Subject,
|
|
openPeer: @escaping (EnginePeer) -> Void,
|
|
updateSavedToProfile: @escaping (Bool) -> Void,
|
|
convertToStars: @escaping () -> Void,
|
|
openStarsIntro: @escaping () -> Void,
|
|
sendGift: @escaping (EnginePeer.Id) -> Void,
|
|
openMyGifts: @escaping () -> Void,
|
|
transferGift: @escaping () -> Void,
|
|
showAttributeInfo: @escaping (Any) -> Void
|
|
) {
|
|
self.context = context
|
|
self.subject = subject
|
|
self.openPeer = openPeer
|
|
self.updateSavedToProfile = updateSavedToProfile
|
|
self.convertToStars = convertToStars
|
|
self.openStarsIntro = openStarsIntro
|
|
self.sendGift = sendGift
|
|
self.openMyGifts = openMyGifts
|
|
self.transferGift = transferGift
|
|
self.showAttributeInfo = showAttributeInfo
|
|
}
|
|
|
|
static func ==(lhs: GiftViewSheetComponent, rhs: GiftViewSheetComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.subject != rhs.subject {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let sheet = Child(SheetComponent<EnvironmentType>.self)
|
|
let animateOut = StoredActionSlot(Action<Void>.self)
|
|
|
|
let sheetExternalState = SheetComponent<EnvironmentType>.ExternalState()
|
|
|
|
return { context in
|
|
let environment = context.environment[EnvironmentType.self]
|
|
let controller = environment.controller
|
|
|
|
let sheet = sheet.update(
|
|
component: SheetComponent<EnvironmentType>(
|
|
content: AnyComponent<EnvironmentType>(GiftViewSheetContent(
|
|
context: context.component.context,
|
|
subject: context.component.subject,
|
|
cancel: { animate in
|
|
if animate {
|
|
if let controller = controller() as? GiftViewScreen {
|
|
controller.dismissAllTooltips()
|
|
animateOut.invoke(Action { [weak controller] _ in
|
|
controller?.dismiss(completion: nil)
|
|
})
|
|
}
|
|
} else if let controller = controller() {
|
|
controller.dismiss(animated: false, completion: nil)
|
|
}
|
|
},
|
|
openPeer: context.component.openPeer,
|
|
updateSavedToProfile: context.component.updateSavedToProfile,
|
|
convertToStars: context.component.convertToStars,
|
|
openStarsIntro: context.component.openStarsIntro,
|
|
sendGift: context.component.sendGift,
|
|
openMyGifts: context.component.openMyGifts,
|
|
transferGift: context.component.transferGift,
|
|
showAttributeInfo: context.component.showAttributeInfo,
|
|
getController: controller
|
|
)),
|
|
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
|
|
followContentSizeChanges: true,
|
|
clipsContent: true,
|
|
externalState: sheetExternalState,
|
|
animateOut: animateOut,
|
|
onPan: {
|
|
if let controller = controller() as? GiftViewScreen {
|
|
controller.dismissAllTooltips()
|
|
}
|
|
}
|
|
),
|
|
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 {
|
|
if let controller = controller() as? GiftViewScreen {
|
|
controller.dismissAllTooltips()
|
|
animateOut.invoke(Action { _ in
|
|
controller.dismiss(completion: nil)
|
|
})
|
|
}
|
|
} else {
|
|
if let controller = controller() as? GiftViewScreen {
|
|
controller.dismissAllTooltips()
|
|
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))
|
|
)
|
|
|
|
if let controller = controller(), !controller.automaticallyControlPresentationContextLayout {
|
|
let layout = ContainerViewLayout(
|
|
size: context.availableSize,
|
|
metrics: environment.metrics,
|
|
deviceMetrics: environment.deviceMetrics,
|
|
intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0),
|
|
safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: environment.safeInsets.right),
|
|
additionalInsets: .zero,
|
|
statusBarHeight: environment.statusBarHeight,
|
|
inputHeight: nil,
|
|
inputHeightIsInteractivellyChanging: false,
|
|
inVoiceOver: false
|
|
)
|
|
controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition)
|
|
}
|
|
|
|
return context.availableSize
|
|
}
|
|
}
|
|
}
|
|
|
|
public class GiftViewScreen: ViewControllerComponentContainer {
|
|
public enum Subject: Equatable {
|
|
case message(EngineMessage)
|
|
case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift)
|
|
case soldOutGift(StarGift.Gift)
|
|
|
|
var arguments: (peerId: EnginePeer.Id, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, canUpgrade: Bool)? {
|
|
switch self {
|
|
case let .message(message):
|
|
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, _, _) = action.action {
|
|
return (message.id.peerId, message.author?.id, message.author?.compactDisplayTitle, message.id, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, converted, canUpgrade: false)
|
|
}
|
|
case let .profileGift(peerId, gift):
|
|
return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, gift.messageId, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, false, gift.canUpgrade)
|
|
case .soldOutGift:
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
public var disposed: () -> Void = {}
|
|
|
|
private let hapticFeedback = HapticFeedback()
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
subject: GiftViewScreen.Subject,
|
|
forceDark: Bool = false,
|
|
updateSavedToProfile: ((Bool) -> Void)? = nil,
|
|
convertToStars: (() -> Void)? = nil
|
|
) {
|
|
self.context = context
|
|
|
|
var openPeerImpl: ((EnginePeer) -> Void)?
|
|
var updateSavedToProfileImpl: ((Bool) -> Void)?
|
|
var convertToStarsImpl: (() -> Void)?
|
|
var openStarsIntroImpl: (() -> Void)?
|
|
var sendGiftImpl: ((EnginePeer.Id) -> Void)?
|
|
var openMyGiftsImpl: (() -> Void)?
|
|
var transferGiftImpl: (() -> Void)?
|
|
var showAttributeInfoImpl: ((Any) -> Void)?
|
|
|
|
super.init(
|
|
context: context,
|
|
component: GiftViewSheetComponent(
|
|
context: context,
|
|
subject: subject,
|
|
openPeer: { peerId in
|
|
openPeerImpl?(peerId)
|
|
},
|
|
updateSavedToProfile: { added in
|
|
updateSavedToProfileImpl?(added)
|
|
},
|
|
convertToStars: {
|
|
convertToStarsImpl?()
|
|
},
|
|
openStarsIntro: {
|
|
openStarsIntroImpl?()
|
|
},
|
|
sendGift: { peerId in
|
|
sendGiftImpl?(peerId)
|
|
},
|
|
openMyGifts: {
|
|
openMyGiftsImpl?()
|
|
},
|
|
transferGift: {
|
|
transferGiftImpl?()
|
|
},
|
|
showAttributeInfo: { tag in
|
|
showAttributeInfoImpl?(tag)
|
|
}
|
|
),
|
|
navigationBarAppearance: .none,
|
|
statusBarStyle: .ignore,
|
|
theme: forceDark ? .dark : .default
|
|
)
|
|
|
|
self.navigationPresentation = .flatModal
|
|
self.automaticallyControlPresentationContextLayout = false
|
|
|
|
openPeerImpl = { [weak self] peer in
|
|
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
self.dismissAllTooltips()
|
|
|
|
let _ = (context.engine.data.get(
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
|
)
|
|
|> deliverOnMainQueue).start(next: { peer in
|
|
guard let peer else {
|
|
return
|
|
}
|
|
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
|
|
})
|
|
}
|
|
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
updateSavedToProfileImpl = { [weak self] added in
|
|
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId else {
|
|
return
|
|
}
|
|
if let updateSavedToProfile {
|
|
updateSavedToProfile(added)
|
|
} else {
|
|
let _ = (context.engine.payments.updateStarGiftAddedToProfile(messageId: messageId, added: added)
|
|
|> deliverOnMainQueue).startStandalone()
|
|
}
|
|
|
|
self.dismissAnimated()
|
|
|
|
let text = added ? presentationData.strings.Gift_Displayed_NewText : presentationData.strings.Gift_Hidden_NewText
|
|
if let navigationController = self.navigationController as? NavigationController {
|
|
Queue.mainQueue().after(0.5) {
|
|
if let lastController = navigationController.viewControllers.last as? ViewController, case let .generic(gift) = arguments.gift {
|
|
let resultController = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .sticker(context: context, file: gift.file, loop: false, title: nil, text: text, undoText: updateSavedToProfile == nil ? presentationData.strings.Gift_Displayed_View : nil, customAction: nil),
|
|
elevatedLayout: lastController is ChatController,
|
|
action: { [weak navigationController] action in
|
|
if case .undo = action, let navigationController {
|
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|
|
guard let peer, let navigationController else {
|
|
return
|
|
}
|
|
if let controller = context.sharedContext.makePeerInfoController(
|
|
context: context,
|
|
updatedPresentationData: nil,
|
|
peer: peer._asPeer(),
|
|
mode: .myProfileGifts,
|
|
avatarInitiallyExpanded: false,
|
|
fromChat: false,
|
|
requestsContext: nil
|
|
) {
|
|
navigationController.pushViewController(controller, animated: true)
|
|
}
|
|
})
|
|
}
|
|
return true
|
|
}
|
|
)
|
|
lastController.present(resultController, in: .window(.root))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
convertToStarsImpl = { [weak self] in
|
|
guard let self, let arguments = subject.arguments, let messageId = arguments.messageId, let fromPeerName = arguments.fromPeerName, let convertStars = arguments.convertStars, let navigationController = self.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
|
|
let configuration = GiftConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
|
let starsConvertMaxDate = arguments.date + configuration.convertToStarsPeriod
|
|
|
|
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
|
|
|
if currentTime > starsConvertMaxDate {
|
|
let days: Int32 = Int32(ceil(Float(configuration.convertToStarsPeriod) / 86400.0))
|
|
let controller = textAlertController(
|
|
context: self.context,
|
|
title: presentationData.strings.Gift_Convert_Title,
|
|
text: presentationData.strings.Gift_Convert_Period_Unavailable_Text(presentationData.strings.Gift_Convert_Period_Unavailable_Days(days)).string,
|
|
actions: [
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})
|
|
],
|
|
parseMarkdown: true
|
|
)
|
|
self.present(controller, in: .window(.root))
|
|
} else {
|
|
let delta = starsConvertMaxDate - currentTime
|
|
let days: Int32 = Int32(ceil(Float(delta) / 86400.0))
|
|
|
|
let text = presentationData.strings.Gift_Convert_Period_Text(fromPeerName, presentationData.strings.Gift_Convert_Period_Stars(Int32(convertStars)), presentationData.strings.Gift_Convert_Period_Days(days)).string
|
|
let controller = textAlertController(
|
|
context: self.context,
|
|
title: presentationData.strings.Gift_Convert_Title,
|
|
text: text,
|
|
actions: [
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak navigationController] in
|
|
if let convertToStars {
|
|
convertToStars()
|
|
} else {
|
|
let _ = (context.engine.payments.convertStarGift(messageId: messageId)
|
|
|> deliverOnMainQueue).startStandalone()
|
|
}
|
|
self?.dismissAnimated()
|
|
|
|
if let navigationController {
|
|
Queue.mainQueue().after(0.5) {
|
|
if let starsContext = context.starsContext {
|
|
navigationController.pushViewController(context.sharedContext.makeStarsTransactionsScreen(context: context, starsContext: starsContext), animated: true)
|
|
}
|
|
|
|
if let lastController = navigationController.viewControllers.last as? ViewController {
|
|
let resultController = UndoOverlayController(
|
|
presentationData: presentationData,
|
|
content: .universal(
|
|
animation: "StarsBuy",
|
|
scale: 0.066,
|
|
colors: [:],
|
|
title: presentationData.strings.Gift_Convert_Success_Title,
|
|
text: presentationData.strings.Gift_Convert_Success_Text(presentationData.strings.Gift_Convert_Success_Text_Stars(Int32(convertStars))).string,
|
|
customUndoText: nil,
|
|
timeout: nil
|
|
),
|
|
elevatedLayout: lastController is ChatController,
|
|
action: { _ in return true}
|
|
)
|
|
lastController.present(resultController, in: .window(.root))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
],
|
|
parseMarkdown: true
|
|
)
|
|
self.present(controller, in: .window(.root))
|
|
}
|
|
}
|
|
openStarsIntroImpl = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let introController = context.sharedContext.makeStarsIntroScreen(context: context)
|
|
self.push(introController)
|
|
}
|
|
sendGiftImpl = { [weak self] peerId in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let _ = (context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|
|
|> filter { !$0.isEmpty }
|
|
|> deliverOnMainQueue).start(next: { giftOptions in
|
|
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
|
|
let controller = context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false)
|
|
self.push(controller)
|
|
})
|
|
}
|
|
openMyGiftsImpl = { [weak self] in
|
|
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|
|
guard let peer, let navigationController else {
|
|
return
|
|
}
|
|
if let controller = context.sharedContext.makePeerInfoController(
|
|
context: context,
|
|
updatedPresentationData: nil,
|
|
peer: peer._asPeer(),
|
|
mode: .myProfileGifts,
|
|
avatarInitiallyExpanded: false,
|
|
fromChat: false,
|
|
requestsContext: nil
|
|
) {
|
|
navigationController.pushViewController(controller, animated: true)
|
|
}
|
|
})
|
|
}
|
|
|
|
transferGiftImpl = { [weak self] in
|
|
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
let _ = (context.account.stateManager.contactBirthdays
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { birthdays in
|
|
let controller = context.sharedContext.makePremiumGiftController(context: context, source: .settings(birthdays), transfer: true, completion: nil)
|
|
navigationController.pushViewController(controller)
|
|
})
|
|
}
|
|
|
|
showAttributeInfoImpl = { [weak self] tag in
|
|
guard let self else {
|
|
return
|
|
}
|
|
guard let sourceView = self.node.hostView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: self.view) else {
|
|
return
|
|
}
|
|
|
|
let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize())
|
|
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: presentationData.strings.Gift_Unique_AttributeDescription("5%").string), location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
|
|
return .ignore
|
|
})
|
|
self.present(controller, in: .current)
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.disposed()
|
|
}
|
|
|
|
public override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.view.disablesInteractiveModalDismiss = true
|
|
}
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
self.dismissAllTooltips()
|
|
}
|
|
|
|
public func dismissAnimated() {
|
|
self.dismissAllTooltips()
|
|
|
|
if let view = self.node.hostView.findTaggedView(tag: SheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? SheetComponent<ViewControllerComponentContainer.Environment>.View {
|
|
view.dismissAnimated()
|
|
}
|
|
}
|
|
|
|
fileprivate func dismissAllTooltips() {
|
|
// self.window?.forEachController({ controller in
|
|
// if let controller = controller as? UndoOverlayController {
|
|
// controller.dismiss()
|
|
// }
|
|
// })
|
|
// self.forEachController({ controller in
|
|
// if let controller = controller as? UndoOverlayController {
|
|
// controller.dismiss()
|
|
// }
|
|
// return true
|
|
// })
|
|
}
|
|
}
|
|
|
|
private final class TableComponent: CombinedComponent {
|
|
class Item: Equatable {
|
|
public let id: AnyHashable
|
|
public let title: String?
|
|
public let component: AnyComponent<Empty>
|
|
public let insets: UIEdgeInsets?
|
|
|
|
public init<IdType: Hashable>(id: IdType, title: String?, component: AnyComponent<Empty>, insets: UIEdgeInsets? = nil) {
|
|
self.id = AnyHashable(id)
|
|
self.title = title
|
|
self.component = component
|
|
self.insets = insets
|
|
}
|
|
|
|
public static func == (lhs: Item, rhs: Item) -> Bool {
|
|
if lhs.id != rhs.id {
|
|
return false
|
|
}
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.component != rhs.component {
|
|
return false
|
|
}
|
|
if lhs.insets != rhs.insets {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private let theme: PresentationTheme
|
|
private let items: [Item]
|
|
|
|
public init(theme: PresentationTheme, items: [Item]) {
|
|
self.theme = theme
|
|
self.items = items
|
|
}
|
|
|
|
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.items != rhs.items {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class State: ComponentState {
|
|
var cachedBorderImage: (UIImage, PresentationTheme)?
|
|
}
|
|
|
|
func makeState() -> State {
|
|
return State()
|
|
}
|
|
|
|
public static var body: Body {
|
|
let leftColumnBackground = Child(Rectangle.self)
|
|
let verticalBorder = Child(Rectangle.self)
|
|
let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
|
let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
|
let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
|
let outerBorder = Child(Image.self)
|
|
|
|
return { context in
|
|
let verticalPadding: CGFloat = 11.0
|
|
let horizontalPadding: CGFloat = 12.0
|
|
let borderWidth: CGFloat = 1.0
|
|
|
|
let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor
|
|
let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6)
|
|
|
|
var leftColumnWidth: CGFloat = 0.0
|
|
|
|
var updatedTitleChildren: [Int: _UpdatedChildComponent] = [:]
|
|
var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = []
|
|
var updatedBorderChildren: [_UpdatedChildComponent] = []
|
|
|
|
var i = 0
|
|
for item in context.component.items {
|
|
guard let title = item.title else {
|
|
i += 1
|
|
continue
|
|
}
|
|
let titleChild = titleChildren[item.id].update(
|
|
component: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
|
|
)),
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
updatedTitleChildren[i] = titleChild
|
|
|
|
if titleChild.size.width > leftColumnWidth {
|
|
leftColumnWidth = titleChild.size.width
|
|
}
|
|
i += 1
|
|
}
|
|
|
|
leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0)
|
|
let rightColumnWidth = context.availableSize.width - leftColumnWidth
|
|
|
|
i = 0
|
|
var rowHeights: [Int: CGFloat] = [:]
|
|
var totalHeight: CGFloat = 0.0
|
|
var innerTotalHeight: CGFloat = 0.0
|
|
|
|
for item in context.component.items {
|
|
let insets: UIEdgeInsets
|
|
if let customInsets = item.insets {
|
|
insets = customInsets
|
|
} else {
|
|
insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding)
|
|
}
|
|
|
|
var titleHeight: CGFloat = 0.0
|
|
if let titleChild = updatedTitleChildren[i] {
|
|
titleHeight = titleChild.size.height
|
|
}
|
|
|
|
let availableValueWidth: CGFloat
|
|
if titleHeight > 0.0 {
|
|
availableValueWidth = rightColumnWidth
|
|
} else {
|
|
availableValueWidth = context.availableSize.width
|
|
}
|
|
|
|
let valueChild = valueChildren[item.id].update(
|
|
component: item.component,
|
|
availableSize: CGSize(width: availableValueWidth - insets.left - insets.right, height: context.availableSize.height),
|
|
transition: context.transition
|
|
)
|
|
updatedValueChildren.append((valueChild, insets))
|
|
|
|
let rowHeight = max(40.0, max(titleHeight, valueChild.size.height) + verticalPadding * 2.0)
|
|
rowHeights[i] = rowHeight
|
|
totalHeight += rowHeight
|
|
if titleHeight > 0.0 {
|
|
innerTotalHeight += rowHeight
|
|
}
|
|
|
|
if i < context.component.items.count - 1 {
|
|
let borderChild = borderChildren[item.id].update(
|
|
component: AnyComponent(Rectangle(color: borderColor)),
|
|
availableSize: CGSize(width: context.availableSize.width, height: borderWidth),
|
|
transition: context.transition
|
|
)
|
|
updatedBorderChildren.append(borderChild)
|
|
}
|
|
|
|
i += 1
|
|
}
|
|
|
|
let leftColumnBackground = leftColumnBackground.update(
|
|
component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor),
|
|
availableSize: CGSize(width: leftColumnWidth, height: innerTotalHeight),
|
|
transition: context.transition
|
|
)
|
|
context.add(
|
|
leftColumnBackground
|
|
.position(CGPoint(x: leftColumnWidth / 2.0, y: innerTotalHeight / 2.0))
|
|
)
|
|
|
|
let borderImage: UIImage
|
|
if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme {
|
|
borderImage = currentImage
|
|
} else {
|
|
let borderRadius: CGFloat = 5.0
|
|
borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
context.setFillColor(backgroundColor.cgColor)
|
|
context.fill(bounds)
|
|
|
|
let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil)
|
|
context.setBlendMode(.clear)
|
|
context.addPath(path)
|
|
context.fillPath()
|
|
|
|
context.setBlendMode(.normal)
|
|
context.setStrokeColor(borderColor.cgColor)
|
|
context.setLineWidth(borderWidth)
|
|
context.addPath(path)
|
|
context.strokePath()
|
|
})!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5)
|
|
context.state.cachedBorderImage = (borderImage, context.component.theme)
|
|
}
|
|
|
|
let outerBorder = outerBorder.update(
|
|
component: Image(image: borderImage),
|
|
availableSize: CGSize(width: context.availableSize.width, height: totalHeight),
|
|
transition: context.transition
|
|
)
|
|
context.add(outerBorder
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0))
|
|
)
|
|
|
|
let verticalBorder = verticalBorder.update(
|
|
component: Rectangle(color: borderColor),
|
|
availableSize: CGSize(width: borderWidth, height: innerTotalHeight),
|
|
transition: context.transition
|
|
)
|
|
context.add(
|
|
verticalBorder
|
|
.position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: innerTotalHeight / 2.0))
|
|
)
|
|
|
|
i = 0
|
|
var originY: CGFloat = 0.0
|
|
for (valueChild, valueInsets) in updatedValueChildren {
|
|
let rowHeight = rowHeights[i] ?? 0.0
|
|
|
|
let valueFrame: CGRect
|
|
if let titleChild = updatedTitleChildren[i] {
|
|
let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size)
|
|
context.add(titleChild
|
|
.position(titleFrame.center)
|
|
)
|
|
valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size)
|
|
} else {
|
|
valueFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: valueChild.size)
|
|
}
|
|
|
|
context.add(valueChild
|
|
.position(valueFrame.center)
|
|
)
|
|
|
|
if i < updatedBorderChildren.count {
|
|
let borderChild = updatedBorderChildren[i]
|
|
context.add(borderChild
|
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0))
|
|
)
|
|
}
|
|
|
|
originY += rowHeight
|
|
i += 1
|
|
}
|
|
|
|
return CGSize(width: context.availableSize.width, height: totalHeight)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class PeerCellComponent: Component {
|
|
let context: AccountContext
|
|
let theme: PresentationTheme
|
|
let strings: PresentationStrings
|
|
let peer: EnginePeer?
|
|
|
|
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EnginePeer?) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.strings = strings
|
|
self.peer = peer
|
|
}
|
|
|
|
static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.theme !== rhs.theme {
|
|
return false
|
|
}
|
|
if lhs.strings !== rhs.strings {
|
|
return false
|
|
}
|
|
if lhs.peer != rhs.peer {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class View: UIView {
|
|
private let avatarNode: AvatarNode
|
|
private let text = ComponentView<Empty>()
|
|
|
|
private var component: PeerCellComponent?
|
|
private weak var state: EmptyComponentState?
|
|
|
|
override init(frame: CGRect) {
|
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 8.0))
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubnode(self.avatarNode)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let avatarSize = CGSize(width: 22.0, height: 22.0)
|
|
let spacing: CGFloat = 6.0
|
|
|
|
let peerName: String
|
|
let avatarOverride: AvatarNodeImageOverride?
|
|
if let peerValue = component.peer {
|
|
peerName = peerValue.compactDisplayTitle
|
|
avatarOverride = nil
|
|
} else {
|
|
peerName = component.strings.Gift_View_HiddenName
|
|
avatarOverride = .anonymousSavedMessagesIcon(isColored: true)
|
|
}
|
|
|
|
let avatarNaturalSize = CGSize(width: 40.0, height: 40.0)
|
|
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer, overrideImage: avatarOverride)
|
|
self.avatarNode.bounds = CGRect(origin: .zero, size: avatarNaturalSize)
|
|
|
|
let textSize = self.text.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: peerName, font: Font.regular(15.0), textColor: component.peer != nil ? component.theme.list.itemAccentColor : component.theme.list.itemPrimaryTextColor, paragraphAlignment: .left))
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height)
|
|
)
|
|
|
|
let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height)
|
|
|
|
let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize)
|
|
self.avatarNode.frame = avatarFrame
|
|
|
|
if let view = self.text.view {
|
|
if view.superview == nil {
|
|
self.addSubview(view)
|
|
}
|
|
let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
|
|
transition.setFrame(view: view, frame: textFrame)
|
|
}
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
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()
|
|
})
|
|
}
|
|
|
|
private final class ButtonContentComponent: Component {
|
|
let context: AccountContext
|
|
let text: String
|
|
let color: UIColor
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
text: String,
|
|
color: UIColor
|
|
) {
|
|
self.context = context
|
|
self.text = text
|
|
self.color = color
|
|
}
|
|
|
|
public static func ==(lhs: ButtonContentComponent, rhs: ButtonContentComponent) -> Bool {
|
|
if lhs.context !== rhs.context {
|
|
return false
|
|
}
|
|
if lhs.text != rhs.text {
|
|
return false
|
|
}
|
|
if lhs.color != rhs.color {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: ButtonContentComponent?
|
|
private weak var componentState: EmptyComponentState?
|
|
|
|
private let backgroundLayer = SimpleLayer()
|
|
private let title = ComponentView<Empty>()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
self.layer.addSublayer(self.backgroundLayer)
|
|
self.backgroundLayer.masksToBounds = true
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(component: ButtonContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.componentState = state
|
|
|
|
let attributedText = NSAttributedString(string: component.text, font: Font.regular(11.0), textColor: component.color)
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextWithEntitiesComponent(
|
|
context: component.context,
|
|
animationCache: component.context.animationCache,
|
|
animationRenderer: component.context.animationRenderer,
|
|
placeholderColor: .white,
|
|
text: .plain(attributedText)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
|
|
let padding: CGFloat = 6.0
|
|
let size = CGSize(width: titleSize.width + padding * 2.0, height: 18.0)
|
|
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
}
|
|
|
|
let backgroundColor = component.color.withAlphaComponent(0.1)
|
|
self.backgroundLayer.backgroundColor = backgroundColor.cgColor
|
|
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
|
|
self.backgroundLayer.cornerRadius = size.height / 2.0
|
|
|
|
return size
|
|
}
|
|
}
|
|
|
|
public func makeView() -> View {
|
|
return View(frame: CGRect())
|
|
}
|
|
|
|
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
|
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
|
}
|
|
}
|
|
|
|
private struct GiftConfiguration {
|
|
static var defaultValue: GiftConfiguration {
|
|
return GiftConfiguration(convertToStarsPeriod: 90 * 86400)
|
|
}
|
|
|
|
let convertToStarsPeriod: Int32
|
|
|
|
fileprivate init(convertToStarsPeriod: Int32) {
|
|
self.convertToStarsPeriod = convertToStarsPeriod
|
|
}
|
|
|
|
static func with(appConfiguration: AppConfiguration) -> GiftConfiguration {
|
|
if let data = appConfiguration.data {
|
|
var convertToStarsPeriod: Int32?
|
|
if let value = data["stargifts_convert_period_max"] as? Double {
|
|
convertToStarsPeriod = Int32(value)
|
|
}
|
|
return GiftConfiguration(convertToStarsPeriod: convertToStarsPeriod ?? GiftConfiguration.defaultValue.convertToStarsPeriod)
|
|
} else {
|
|
return .defaultValue
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ParagraphComponent: CombinedComponent {
|
|
let title: String
|
|
let titleColor: UIColor
|
|
let text: String
|
|
let textColor: UIColor
|
|
let accentColor: UIColor
|
|
let iconName: String
|
|
let iconColor: UIColor
|
|
let badge: String?
|
|
let action: () -> Void
|
|
|
|
public init(
|
|
title: String,
|
|
titleColor: UIColor,
|
|
text: String,
|
|
textColor: UIColor,
|
|
accentColor: UIColor,
|
|
iconName: String,
|
|
iconColor: UIColor,
|
|
badge: String? = nil,
|
|
action: @escaping () -> Void = {}
|
|
) {
|
|
self.title = title
|
|
self.titleColor = titleColor
|
|
self.text = text
|
|
self.textColor = textColor
|
|
self.accentColor = accentColor
|
|
self.iconName = iconName
|
|
self.iconColor = iconColor
|
|
self.badge = badge
|
|
self.action = action
|
|
}
|
|
|
|
static func ==(lhs: ParagraphComponent, rhs: ParagraphComponent) -> Bool {
|
|
if lhs.title != rhs.title {
|
|
return false
|
|
}
|
|
if lhs.titleColor != rhs.titleColor {
|
|
return false
|
|
}
|
|
if lhs.text != rhs.text {
|
|
return false
|
|
}
|
|
if lhs.textColor != rhs.textColor {
|
|
return false
|
|
}
|
|
if lhs.accentColor != rhs.accentColor {
|
|
return false
|
|
}
|
|
if lhs.iconName != rhs.iconName {
|
|
return false
|
|
}
|
|
if lhs.iconColor != rhs.iconColor {
|
|
return false
|
|
}
|
|
if lhs.badge != rhs.badge {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static var body: Body {
|
|
let title = Child(MultilineTextComponent.self)
|
|
let text = Child(MultilineTextComponent.self)
|
|
let icon = Child(BundleIconComponent.self)
|
|
let badgeBackground = Child(RoundedRectangle.self)
|
|
let badgeText = Child(MultilineTextComponent.self)
|
|
|
|
return { context in
|
|
let component = context.component
|
|
|
|
let leftInset: CGFloat = 32.0
|
|
let rightInset: CGFloat = 24.0
|
|
let textSideInset: CGFloat = leftInset + 8.0
|
|
let spacing: CGFloat = 5.0
|
|
|
|
let textTopInset: CGFloat = 9.0
|
|
|
|
let title = title.update(
|
|
component: MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: component.title,
|
|
font: Font.semibold(15.0),
|
|
textColor: component.titleColor,
|
|
paragraphAlignment: .natural
|
|
)),
|
|
horizontalAlignment: .center,
|
|
maximumNumberOfLines: 1
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: CGFloat.greatestFiniteMagnitude),
|
|
transition: .immediate
|
|
)
|
|
|
|
let textFont = Font.regular(15.0)
|
|
let boldTextFont = Font.semibold(15.0)
|
|
let textColor = component.textColor
|
|
let accentColor = component.accentColor
|
|
let markdownAttributes = MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: textFont, textColor: textColor),
|
|
bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor),
|
|
link: MarkdownAttributeSet(font: textFont, textColor: accentColor),
|
|
linkAttribute: { contents in
|
|
return (TelegramTextAttributes.URL, contents)
|
|
}
|
|
)
|
|
|
|
let text = text.update(
|
|
component: MultilineTextComponent(
|
|
text: .markdown(text: component.text, attributes: markdownAttributes),
|
|
horizontalAlignment: .natural,
|
|
maximumNumberOfLines: 0,
|
|
lineSpacing: 0.2,
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
tapAction: { _, _ in
|
|
component.action()
|
|
}
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width - leftInset - rightInset, height: context.availableSize.height),
|
|
transition: .immediate
|
|
)
|
|
|
|
let icon = icon.update(
|
|
component: BundleIconComponent(
|
|
name: component.iconName,
|
|
tintColor: component.iconColor
|
|
),
|
|
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
|
transition: .immediate
|
|
)
|
|
|
|
context.add(title
|
|
.position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0))
|
|
)
|
|
|
|
if let badge = component.badge {
|
|
let badgeText = badgeText.update(
|
|
component: MultilineTextComponent(text: .plain(NSAttributedString(string: badge, font: Font.semibold(11.0), textColor: .white))),
|
|
availableSize: context.availableSize,
|
|
transition: context.transition
|
|
)
|
|
|
|
let badgeWidth = badgeText.size.width + 7.0
|
|
let badgeBackground = badgeBackground.update(
|
|
component: RoundedRectangle(
|
|
color: component.accentColor,
|
|
cornerRadius: 5.0),
|
|
availableSize: CGSize(width: badgeWidth, height: 16.0),
|
|
transition: context.transition
|
|
)
|
|
|
|
context.add(badgeBackground
|
|
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
|
|
)
|
|
|
|
context.add(badgeText
|
|
.position(CGPoint(x: textSideInset + title.size.width + badgeWidth / 2.0 + 5.0, y: textTopInset + title.size.height / 2.0))
|
|
)
|
|
}
|
|
|
|
context.add(text
|
|
.position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0))
|
|
)
|
|
|
|
context.add(icon
|
|
.position(CGPoint(x: 15.0, y: textTopInset + 18.0))
|
|
)
|
|
|
|
return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 20.0)
|
|
}
|
|
}
|
|
}
|