Swiftgram/submodules/PremiumUI/Sources/PremiumGiftCodeScreen.swift
2024-11-27 17:37:21 +04:00

1206 lines
49 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import PresentationDataUtils
import ComponentFlow
import ViewControllerComponent
import SheetComponent
import MultilineTextComponent
import BundleIconComponent
import SolidRoundedButtonComponent
import Markdown
import BalancedTextComponent
import ConfettiEffect
import AvatarNode
import TextFormat
import TelegramStringFormatting
import UndoUI
import InvisibleInkDustNode
import PremiumStarComponent
private final class PremiumGiftCodeSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void
let cancel: (Bool) -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init(
context: AccountContext,
subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void,
cancel: @escaping (Bool) -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.action = action
self.cancel = cancel
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
}
static func ==(lhs: PremiumGiftCodeSheetContent, rhs: PremiumGiftCodeSheetContent) -> 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 var disposable: Disposable?
var initialized = false
var peerMap: [EnginePeer.Id: EnginePeer] = [:]
var cachedCloseImage: (UIImage, PresentationTheme)?
var inProgress = false
init(context: AccountContext, subject: PremiumGiftCodeScreen.Subject) {
self.context = context
super.init()
var peerIds: [EnginePeer.Id] = []
switch subject {
case let .giftCode(giftCode):
if let fromPeerId = giftCode.fromPeerId {
peerIds.append(fromPeerId)
}
if let toPeerId = giftCode.toPeerId {
peerIds.append(toPeerId)
}
case let .boost(channelId, boost):
peerIds.append(channelId)
if let peerId = boost.peer?.id {
peerIds.append(peerId)
}
}
self.disposable = (context.engine.data.get(
EngineDataMap(
peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in
return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
}
)
) |> deliverOnMainQueue).startStrict(next: { [weak self] peers 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
strongSelf.initialized = true
strongSelf.updated(transition: .immediate)
}
})
}
deinit {
self.disposable?.dispose()
}
}
func makeState() -> State {
return State(context: self.context, subject: self.subject)
}
static var body: Body {
let closeButton = Child(Button.self)
let title = Child(MultilineTextComponent.self)
let star = Child(PremiumStarComponent.self)
let description = Child(BalancedTextComponent.self)
let linkButton = Child(Button.self)
let table = Child(TableComponent.self)
let additional = Child(BalancedTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
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 accountContext = context.component.context
let state = context.state
let subject = component.subject
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let textSideInset: CGFloat = 32.0 + environment.safeInsets.left
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: { [weak component] in
component?.cancel(true)
}
),
availableSize: CGSize(width: 30.0, height: 30.0),
transition: .immediate
)
let titleText: String
let descriptionText: String
let additionalText: String
let buttonText: String
let link: String?
let date: Int32
let fromPeer: EnginePeer?
var toPeerId: EnginePeer.Id?
let toPeer: EnginePeer?
let months: Int32
var gloss = false
switch subject {
case let .giftCode(giftCode):
gloss = !giftCode.isUsed
if let usedDate = giftCode.usedDate {
let dateString = stringForMediumDate(timestamp: usedDate, strings: strings, dateTimeFormat: dateTimeFormat)
titleText = strings.GiftLink_UsedTitle
descriptionText = strings.GiftLink_UsedDescription
additionalText = strings.GiftLink_UsedFooter(dateString).string
buttonText = strings.Common_OK
} else {
titleText = strings.GiftLink_Title
descriptionText = strings.GiftLink_Description
additionalText = strings.GiftLink_Footer
buttonText = strings.GiftLink_UseLink
}
link = "https://t.me/giftcode/\(giftCode.slug)"
date = giftCode.date
if let fromPeerId = giftCode.fromPeerId {
fromPeer = state.peerMap[fromPeerId]
} else {
fromPeer = nil
}
toPeerId = giftCode.toPeerId
if let toPeerId = giftCode.toPeerId {
toPeer = state.peerMap[toPeerId]
} else {
toPeer = nil
}
months = giftCode.months
case let .boost(channelId, boost):
titleText = strings.GiftLink_Title
if let peer = boost.peer, !boost.flags.contains(.isUnclaimed) {
toPeer = boost.peer
if boost.slug == nil {
descriptionText = strings.GiftLink_PersonalDescription(peer.compactDisplayTitle).string
} else {
descriptionText = strings.GiftLink_PersonalUsedDescription(peer.compactDisplayTitle).string
}
} else {
toPeer = nil
descriptionText = strings.GiftLink_UnclaimedDescription
}
if boost.flags.contains(.isUnclaimed) || boost.slug == nil {
additionalText = strings.GiftLink_NotUsedFooter
} else {
additionalText = ""
}
buttonText = strings.Common_OK
if let slug = boost.slug {
link = "https://t.me/giftcode/\(slug)"
} else {
link = nil
}
date = boost.date
if boost.flags.contains(.isUnclaimed) {
toPeerId = nil
} else {
toPeerId = boost.peer?.id
}
fromPeer = state.peerMap[channelId]
months = Int32(round(Float(boost.expires - boost.date) / (86400.0 * 30.0)))
}
let title = title.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: titleText,
font: Font.semibold(17.0),
textColor: 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
)
let star = star.update(
component: PremiumStarComponent(
theme: theme,
isIntro: false,
isVisible: true,
hasIdleAnimations: true,
colors: [
UIColor(rgb: 0x6a94ff),
UIColor(rgb: 0x9472fd),
UIColor(rgb: 0xe26bd3)
]
),
availableSize: CGSize(width: context.availableSize.width, height: 200.0),
transition: .immediate
)
let textFont = Font.regular(15.0)
let boldTextFont = Font.semibold(15.0)
let textColor = theme.actionSheet.primaryTextColor
let linkColor = theme.actionSheet.controlAccentColor
let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in
return (TelegramTextAttributes.URL, contents)
})
let description = description.update(
component: BalancedTextComponent(
text: .markdown(text: descriptionText, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.2
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let linkButton = linkButton.update(
component: Button(
content: AnyComponent(
GiftLinkButtonContentComponent(theme: environment.theme, text: link)
),
action: {
if let link {
component.copyLink(link)
} else {
component.displayHiddenTooltip()
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: .immediate
)
let tableFont = Font.regular(15.0)
let tableTextColor = theme.list.itemPrimaryTextColor
let tableLinkColor = theme.list.itemAccentColor
var tableItems: [TableComponent.Item] = []
tableItems.append(.init(
id: "from",
title: strings.GiftLink_From,
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: fromPeer)),
action: {
if let peer = fromPeer, peer.id != accountContext.account.peerId {
component.openPeer(peer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
}
)
)
))
if let toPeer {
tableItems.append(.init(
id: "to",
title: strings.GiftLink_To,
component: AnyComponent(
Button(
content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)),
action: {
if toPeer.id != accountContext.account.peerId {
component.openPeer(toPeer)
Queue.mainQueue().after(1.0, {
component.cancel(false)
})
}
}
)
)
))
} else if toPeerId == nil {
tableItems.append(.init(
id: "to",
title: strings.GiftLink_To,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: strings.GiftLink_NoRecipient, font: tableFont, textColor: tableTextColor)))
)
))
}
let giftTitle = strings.GiftLink_TelegramPremium(months)
tableItems.append(.init(
id: "gift",
title: strings.GiftLink_Gift,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: giftTitle, font: tableFont, textColor: tableTextColor)))
)
))
if case let .giftCode(giftCode) = component.subject {
let giftReason: String
if giftCode.toPeerId == nil {
giftReason = strings.GiftLink_Reason_Unclaimed
} else {
giftReason = giftCode.isGiveaway ? strings.GiftLink_Reason_Giveaway : strings.GiftLink_Reason_Gift
}
tableItems.append(.init(
id: "reason",
title: strings.GiftLink_Reason,
component: AnyComponent(
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: giftCode.messageId != nil ? tableLinkColor : tableTextColor)))),
automaticHighlight: giftCode.messageId != nil,
action: {
if let messageId = giftCode.messageId {
component.openMessage(messageId)
Queue.mainQueue().after(1.0) {
component.cancel(false)
}
}
}
)
)
))
} else if case let .boost(_, boost) = component.subject {
if boost.flags.contains(.isUnclaimed) {
let giftReason = strings.GiftLink_Reason_Unclaimed
tableItems.append(.init(
id: "reason",
title: strings.GiftLink_Reason,
component: AnyComponent(
Button(
content: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: giftReason, font: tableFont, textColor: boost.giveawayMessageId != nil ? tableLinkColor : tableTextColor)))),
automaticHighlight: boost.giveawayMessageId != nil,
action: {
if let messageId = boost.giveawayMessageId {
component.openMessage(messageId)
Queue.mainQueue().after(1.0) {
component.cancel(false)
}
}
}
)
)
))
}
}
tableItems.append(.init(
id: "date",
title: strings.GiftLink_Date,
component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor)))
)
))
let table = table.update(
component: TableComponent(
theme: environment.theme,
items: tableItems
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude),
transition: .immediate
)
let additional = additional.update(
component: BalancedTextComponent(
text: .markdown(text: additionalText, attributes: markdownAttributes),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1,
highlightColor: linkColor.withAlphaComponent(0.2),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
} else {
return nil
}
},
tapAction: { attributes, _ in
if let link {
component.shareLink(link)
}
}
),
availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height),
transition: .immediate
)
let button = button.update(
component: SolidRoundedButtonComponent(
title: buttonText,
theme: SolidRoundedButtonComponent.Theme(theme: theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: gloss,
iconName: nil,
animationName: nil,
iconPosition: .left,
isLoading: state.inProgress,
action: { [weak state] in
if gloss {
component.action()
if let state {
state.inProgress = true
state.updated()
}
} else {
component.cancel(true)
}
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
context.add(title
.position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0))
)
context.add(star
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0))
)
var originY: CGFloat = 0.0
originY += star.size.height - 32.0
context.add(description
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0))
)
originY += description.size.height + 21.0
context.add(linkButton
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + linkButton.size.height / 2.0))
)
originY += linkButton.size.height + 16.0
context.add(table
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0))
)
originY += table.size.height + 23.0
context.add(additional
.position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0))
)
originY += additional.size.height + 23.0
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size)
context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
)
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: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom)
return contentSize
}
}
}
private final class PremiumGiftCodeSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: PremiumGiftCodeScreen.Subject
let action: () -> Void
let openPeer: (EnginePeer) -> Void
let openMessage: (EngineMessage.Id) -> Void
let copyLink: (String) -> Void
let shareLink: (String) -> Void
let displayHiddenTooltip: () -> Void
init(
context: AccountContext,
subject: PremiumGiftCodeScreen.Subject,
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void,
openMessage: @escaping (EngineMessage.Id) -> Void,
copyLink: @escaping (String) -> Void,
shareLink: @escaping (String) -> Void,
displayHiddenTooltip: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.action = action
self.openPeer = openPeer
self.openMessage = openMessage
self.copyLink = copyLink
self.shareLink = shareLink
self.displayHiddenTooltip = displayHiddenTooltip
}
static func ==(lhs: PremiumGiftCodeSheetComponent, rhs: PremiumGiftCodeSheetComponent) -> 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)
return { context in
let environment = context.environment[EnvironmentType.self]
let controller = environment.controller
let sheet = sheet.update(
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(PremiumGiftCodeSheetContent(
context: context.component.context,
subject: context.component.subject,
action: context.component.action,
cancel: { animate in
if animate {
animateOut.invoke(Action { _ in
if let controller = controller() {
controller.dismiss(completion: nil)
}
})
} else if let controller = controller() {
controller.dismiss(animated: false, completion: nil)
}
},
openPeer: context.component.openPeer,
openMessage: context.component.openMessage,
copyLink: context.component.copyLink,
shareLink: context.component.shareLink,
displayHiddenTooltip: context.component.displayHiddenTooltip
)),
backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor),
followContentSizeChanges: true,
clipsContent: true,
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 class PremiumGiftCodeScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case giftCode(PremiumGiftCodeInfo)
case boost(EnginePeer.Id, ChannelBoostersContext.State.Boost)
}
private let context: AccountContext
public var disposed: () -> Void = {}
private let hapticFeedback = HapticFeedback()
public init(
context: AccountContext,
subject: PremiumGiftCodeScreen.Subject,
forceDark: Bool = false,
action: @escaping () -> Void,
openPeer: @escaping (EnginePeer) -> Void = { _ in },
openMessage: @escaping (EngineMessage.Id) -> Void = { _ in },
shareLink: @escaping (String) -> Void = { _ in }
) {
self.context = context
var copyLinkImpl: ((String) -> Void)?
var displayHiddenTooltipImpl: (() -> Void)?
super.init(
context: context,
component: PremiumGiftCodeSheetComponent(
context: context,
subject: subject,
action: action,
openPeer: openPeer,
openMessage: openMessage,
copyLink: { link in
copyLinkImpl?(link)
},
shareLink: shareLink,
displayHiddenTooltip: {
displayHiddenTooltipImpl?()
}
),
navigationBarAppearance: .none,
statusBarStyle: .ignore,
theme: forceDark ? .dark : .default
)
self.navigationPresentation = .flatModal
copyLinkImpl = { [weak self] link in
UIPasteboard.general.string = link
guard let self else {
return
}
self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root))
}
displayHiddenTooltipImpl = { [weak self] in
guard let self else {
return
}
self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.GiftLink_LinkHidden, timeout: nil, customUndoText: nil), elevatedLayout: false, position: .top, action: { _ in return true }), in: .window(.root))
}
}
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() {
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
})
}
}
final class GiftLinkButtonContentComponent: CombinedComponent {
let theme: PresentationTheme
let text: String?
let isSeparateSection: Bool
init(
theme: PresentationTheme,
text: String?,
isSeparateSection: Bool = false
) {
self.theme = theme
self.text = text
self.isSeparateSection = isSeparateSection
}
static func ==(lhs: GiftLinkButtonContentComponent, rhs: GiftLinkButtonContentComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.isSeparateSection != rhs.isSeparateSection {
return false
}
return true
}
static var body: Body {
let background = Child(RoundedRectangle.self)
let text = Child(MultilineTextComponent.self)
let icon = Child(BundleIconComponent.self)
let dust = Child(DustComponent.self)
return { context in
let component = context.component
let sideInset: CGFloat = 38.0
let background = background.update(
component: RoundedRectangle(color: component.isSeparateSection ? component.theme.list.itemBlocksBackgroundColor : component.theme.list.itemInputField.backgroundColor, cornerRadius: 10.0),
availableSize: context.availableSize,
transition: context.transition
)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
if let _ = component.text {
let text = text.update(
component: MultilineTextComponent(
text: .plain(NSAttributedString(
string: (component.text ?? "").replacingOccurrences(of: "https://", with: ""),
font: Font.regular(17.0),
textColor: component.theme.list.itemPrimaryTextColor,
paragraphAlignment: .natural
)),
horizontalAlignment: .center,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: context.availableSize.width - sideInset - sideInset, height: CGFloat.greatestFiniteMagnitude),
transition: .immediate
)
let icon = icon.update(
component: BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: context.transition
)
context.add(icon
.position(CGPoint(x: context.availableSize.width - icon.size.width / 2.0 - 14.0, y: context.availableSize.height / 2.0))
)
context.add(text
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
} else {
let dust = dust.update(
component: DustComponent(color: component.theme.list.itemSecondaryTextColor),
availableSize: CGSize(width: context.availableSize.width * 0.8, height: context.availableSize.height * 0.54),
transition: context.transition
)
context.add(dust
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
)
}
return context.availableSize
}
}
}
private final class TableComponent: CombinedComponent {
class Item: Equatable {
public let id: AnyHashable
public let title: String
public let component: AnyComponent<Empty>
public init<IdType: Hashable>(id: IdType, title: String, component: AnyComponent<Empty>) {
self.id = AnyHashable(id)
self.title = title
self.component = component
}
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
}
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: [_UpdatedChildComponent] = []
var updatedValueChildren: [_UpdatedChildComponent] = []
var updatedBorderChildren: [_UpdatedChildComponent] = []
for item in context.component.items {
let titleChild = titleChildren[item.id].update(
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor))
)),
availableSize: context.availableSize,
transition: context.transition
)
updatedTitleChildren.append(titleChild)
if titleChild.size.width > leftColumnWidth {
leftColumnWidth = titleChild.size.width
}
}
leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0)
let rightColumnWidth = context.availableSize.width - leftColumnWidth
var i = 0
var rowHeights: [Int: CGFloat] = [:]
var totalHeight: CGFloat = 0.0
for item in context.component.items {
let titleChild = updatedTitleChildren[i]
let valueChild = valueChildren[item.id].update(
component: item.component,
availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height),
transition: context.transition
)
updatedValueChildren.append(valueChild)
let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0)
rowHeights[i] = rowHeight
totalHeight += 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: totalHeight),
transition: context.transition
)
context.add(
leftColumnBackground
.position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 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: totalHeight),
transition: context.transition
)
context.add(
verticalBorder
.position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0))
)
i = 0
var originY: CGFloat = 0.0
for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) {
let rowHeight = rowHeights[i] ?? 0.0
let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size)
let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size)
context.add(titleChild
.position(titleFrame.center)
)
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 textColor: UIColor
let peer: EnginePeer?
init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) {
self.context = context
self.textColor = textColor
self.peer = peer
}
static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.textColor !== rhs.textColor {
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: 13.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
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
let avatarSize = CGSize(width: 22.0, height: 22.0)
let spacing: CGFloat = 6.0
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left))
)
),
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 final class DustComponent: Component {
let color: UIColor
init(color: UIColor) {
self.color = color
}
static func ==(lhs: DustComponent, rhs: DustComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
final class View: UIView {
private let dustView = InvisibleInkDustView(textNode: nil, enableAnimations: true)
private var component: DustComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.dustView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: DustComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let rects: [CGRect] = [CGRect(origin: .zero, size: availableSize).insetBy(dx: 5.0, dy: 5.0)]
self.dustView.update(size: availableSize, color: component.color, textColor: component.color, rects: rects, wordRects: rects)
return availableSize
}
}
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)
}
}