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(isIntro: false, isVisible: true, hasIdleAnimations: true), 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.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(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, 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(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.View.Tag()) as? SheetComponent.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 public init(id: IdType, title: String, component: AnyComponent) { 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() 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, transition: Transition) -> 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, transition: Transition) -> 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, transition: Transition) -> 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, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }