2025-02-22 19:17:36 +04:00

1142 lines
57 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import SolidRoundedButtonComponent
import PresentationDataUtils
import Markdown
import UndoUI
import AnimatedAvatarSetNode
import AvatarNode
import TelegramStringFormatting
private final class SendInviteLinkScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peer: EnginePeer
let link: String?
let peers: [TelegramForbiddenInvitePeer]
let peerPresences: [EnginePeer.Id: EnginePeer.Presence]
init(
context: AccountContext,
peer: EnginePeer,
link: String?,
peers: [TelegramForbiddenInvitePeer],
peerPresences: [EnginePeer.Id: EnginePeer.Presence]
) {
self.context = context
self.peer = peer
self.link = link
self.peers = peers
self.peerPresences = peerPresences
}
static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.link != rhs.link {
return false
}
if lhs.peers != rhs.peers {
return false
}
if lhs.peerPresences != rhs.peerPresences {
return false
}
return true
}
private struct ItemLayout: Equatable {
var containerSize: CGSize
var containerInset: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.bottomInset = bottomInset
self.topInset = topInset
}
}
private final class ScrollView: UIScrollView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event)
}
}
final class View: UIView, UIScrollViewDelegate {
private let dimView: UIView
private let backgroundLayer: SimpleLayer
private let navigationBarContainer: SparseContainerView
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private var avatarsNode: AnimatedAvatarSetNode?
private let avatarsContext = AnimatedAvatarSetContext()
private var premiumTitle: ComponentView<Empty>?
private var premiumText: ComponentView<Empty>?
private var premiumButton: ComponentView<Empty>?
private var premiumSeparatorLeft: SimpleLayer?
private var premiumSeparatorRight: SimpleLayer?
private var premiumSeparatorText: ComponentView<Empty>?
private let leftButton = ComponentView<Empty>()
private var title: ComponentView<Empty>?
private var descriptionText: ComponentView<Empty>?
private var actionButton: ComponentView<Empty>?
private let itemContainerView: UIView
private var items: [AnyHashable: ComponentView<Empty>] = [:]
private var selectedItems = Set<EnginePeer.Id>()
private let bottomOverscrollLimit: CGFloat
private var ignoreScrolling: Bool = false
private var component: SendInviteLinkScreenComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
private var itemLayout: ItemLayout?
private var topOffsetDistance: CGFloat?
override init(frame: CGRect) {
self.bottomOverscrollLimit = 200.0
self.dimView = UIView()
self.backgroundLayer = SimpleLayer()
self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
self.backgroundLayer.cornerRadius = 10.0
self.navigationBarContainer = SparseContainerView()
self.scrollView = ScrollView()
self.scrollContentClippingView = SparseContainerView()
self.scrollContentClippingView.clipsToBounds = true
self.scrollContentView = UIView()
self.itemContainerView = UIView()
self.itemContainerView.clipsToBounds = true
self.itemContainerView.layer.cornerRadius = 10.0
super.init(frame: frame)
self.addSubview(self.dimView)
self.layer.addSublayer(self.backgroundLayer)
self.addSubview(self.navigationBarContainer)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.alwaysBounceVertical = true
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollContentClippingView)
self.scrollContentClippingView.addSubview(self.scrollView)
self.scrollView.addSubview(self.scrollContentView)
self.scrollContentView.addSubview(self.itemContainerView)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let itemLayout = self.itemLayout, let topOffsetDistance = self.topOffsetDistance else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
if topOffset < topOffsetDistance {
targetContentOffset.pointee.y = scrollView.contentOffset.y
scrollView.setContentOffset(CGPoint(x: 0.0, y: itemLayout.topInset), animated: true)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if !self.backgroundLayer.frame.contains(point) {
return self.dimView
}
if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) {
return result
}
let result = super.hitTest(point, with: event)
return result
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
controller.dismiss()
}
}
private func updateScrolling(transition: ComponentTransition) {
guard let environment = self.environment, let controller = environment.controller(), let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
self.topOffsetDistance = topOffsetDistance
var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition)
}
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let actionButtonView = self.actionButton?.view {
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
}
func animateOut(completion: @escaping () -> Void) {
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let actionButtonView = self.actionButton?.view {
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
}
}
func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
let themeUpdated = self.environment?.theme !== environment.theme
let resetScrolling = self.scrollView.bounds.width != availableSize.width
let sideInset: CGFloat = 16.0
if self.component == nil {
for peer in component.peers {
if component.link != nil && !peer.premiumRequiredToContact {
self.selectedItems.insert(peer.peer.id)
}
}
}
self.component = component
self.state = state
self.environment = environment
let premiumRestrictedUsers = component.peers.filter { peer in
return peer.canInviteWithPremium
}
var hasInviteLink = true
if premiumRestrictedUsers.count == component.peers.count && component.link == nil {
hasInviteLink = false
} else if component.link != nil && !premiumRestrictedUsers.isEmpty && component.peers.allSatisfy({ $0.premiumRequiredToContact }) {
hasInviteLink = false
}
if themeUpdated {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
var locations: [NSNumber] = []
var colors: [CGColor] = []
let numStops = 6
for i in 0 ..< numStops {
let step = CGFloat(i) / CGFloat(numStops - 1)
locations.append(step as NSNumber)
colors.append(environment.theme.list.blocksBackgroundColor.withAlphaComponent(1.0 - step * step).cgColor)
}
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
var contentHeight: CGFloat = 0.0
contentHeight += 102.0
let avatarsNode: AnimatedAvatarSetNode
if let current = self.avatarsNode {
avatarsNode = current
} else {
avatarsNode = AnimatedAvatarSetNode()
self.avatarsNode = avatarsNode
self.scrollContentView.addSubview(avatarsNode.view)
}
let avatarPeers: [EnginePeer]
if !premiumRestrictedUsers.isEmpty {
avatarPeers = premiumRestrictedUsers.map(\.peer)
} else {
avatarPeers = component.peers.map(\.peer)
}
let avatarsContent = self.avatarsContext.update(peers: avatarPeers.count <= 3 ? avatarPeers : Array(avatarPeers.prefix(upTo: 3)), animated: false)
let avatarsSize = avatarsNode.update(
context: component.context,
content: avatarsContent,
itemSize: CGSize(width: 60.0, height: 60.0),
customSpacing: 30.0,
font: avatarPlaceholderFont(size: 28.0),
animated: false,
synchronousLoad: true
)
let avatarsFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - avatarsSize.width) * 0.5), y: 26.0), size: avatarsSize)
transition.setFrame(view: avatarsNode.view, frame: avatarsFrame)
let leftButtonSize = self.leftButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)),
action: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
}
).minSize(CGSize(width: 44.0, height: 56.0))),
environment: {},
containerSize: CGSize(width: 120.0, height: 100.0)
)
let leftButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 0.0), size: leftButtonSize)
if let leftButtonView = self.leftButton.view {
if leftButtonView.superview == nil {
self.navigationBarContainer.addSubview(leftButtonView)
}
transition.setFrame(view: leftButtonView, frame: leftButtonFrame)
}
if !premiumRestrictedUsers.isEmpty {
var premiumItemsTransition = transition
let premiumTitle: ComponentView<Empty>
if let current = self.premiumTitle {
premiumTitle = current
} else {
premiumTitle = ComponentView()
self.premiumTitle = premiumTitle
premiumItemsTransition = premiumItemsTransition.withAnimation(.none)
}
let premiumText: ComponentView<Empty>
if let current = self.premiumText {
premiumText = current
} else {
premiumText = ComponentView()
self.premiumText = premiumText
}
let premiumButton: ComponentView<Empty>
if let current = self.premiumButton {
premiumButton = current
} else {
premiumButton = ComponentView()
self.premiumButton = premiumButton
}
let premiumTitleSize = premiumTitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_TitleUpgradeToPremium, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
)
let premiumTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumTitleSize.width) * 0.5), y: contentHeight), size: premiumTitleSize)
if let premiumTitleView = premiumTitle.view {
if premiumTitleView.superview == nil {
self.scrollContentView.addSubview(premiumTitleView)
}
transition.setFrame(view: premiumTitleView, frame: premiumTitleFrame)
}
contentHeight += premiumTitleSize.height
contentHeight += 8.0
let text: String
if premiumRestrictedUsers.count == 1 {
if case let .channel(channel) = component.peer, case .broadcast = channel.info {
text = environment.strings.SendInviteLink_ChannelTextContactsAndPremiumOneUser(premiumRestrictedUsers[0].peer.compactDisplayTitle).string
} else {
text = environment.strings.SendInviteLink_TextContactsAndPremiumOneUser(premiumRestrictedUsers[0].peer.compactDisplayTitle).string
}
} else {
let extraCount = premiumRestrictedUsers.count - 3
var peersTextArray: [String] = []
for i in 0 ..< min(3, premiumRestrictedUsers.count) {
peersTextArray.append("**\(premiumRestrictedUsers[i].peer.compactDisplayTitle)**")
}
var peersText = ""
if #available(iOS 13.0, *) {
let listFormatter = ListFormatter()
listFormatter.locale = localeWithStrings(environment.strings)
if let value = listFormatter.string(from: peersTextArray) {
peersText = value
}
}
if peersText.isEmpty {
for i in 0 ..< peersTextArray.count {
if i != 0 {
peersText.append(", ")
}
peersText.append(peersTextArray[i])
}
}
if extraCount >= 1 {
if case let .channel(channel) = component.peer, case .broadcast = channel.info {
text = environment.strings.SendInviteLink_ChannelTextContactsAndPremiumMultipleUsers(Int32(extraCount)).replacingOccurrences(of: "{user_list}", with: peersText)
} else {
text = environment.strings.SendInviteLink_TextContactsAndPremiumMultipleUsers(Int32(extraCount)).replacingOccurrences(of: "{user_list}", with: peersText)
}
} else {
if case let .channel(channel) = component.peer, case .broadcast = channel.info {
text = environment.strings.SendInviteLink_ChannelTextContactsAndPremiumOneUser(peersText).string
} else {
text = environment.strings.SendInviteLink_TextContactsAndPremiumOneUser(peersText).string
}
}
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let premiumTextSize = premiumText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .markdown(text: text, attributes: MarkdownAttributes(
body: body,
bold: bold,
link: body,
linkAttribute: { _ in nil }
)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
)
let premiumTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumTextSize.width) * 0.5), y: contentHeight), size: premiumTextSize)
if let premiumTextView = premiumText.view {
if premiumTextView.superview == nil {
self.scrollContentView.addSubview(premiumTextView)
}
transition.setFrame(view: premiumTextView, frame: premiumTextFrame)
}
contentHeight += premiumTextSize.height
contentHeight += 22.0
let premiumButtonTitle = environment.strings.SendInviteLink_SubscribeToPremiumButton
let premiumButtonSize = premiumButton.update(
transition: transition,
component: AnyComponent(SolidRoundedButtonComponent(
title: premiumButtonTitle,
badge: nil,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: .black,
backgroundColors: [
UIColor(rgb: 0x0077ff),
UIColor(rgb: 0x6b93ff),
UIColor(rgb: 0x8878ff),
UIColor(rgb: 0xe46ace)
],
foregroundColor: .white
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 11.0,
gloss: false,
animationName: nil,
iconPosition: .right,
iconSpacing: 4.0,
action: { [weak self] in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
let navigationController = controller.navigationController as? NavigationController
controller.dismiss()
let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .settings, forceDark: false, dismissed: nil)
navigationController?.pushViewController(premiumController)
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let premiumButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: premiumButtonSize)
if let premiumButtonView = premiumButton.view {
if premiumButtonView.superview == nil {
self.scrollContentView.addSubview(premiumButtonView)
}
transition.setFrame(view: premiumButtonView, frame: premiumButtonFrame)
}
contentHeight += premiumButtonSize.height
if hasInviteLink {
let premiumSeparatorText: ComponentView<Empty>
if let current = self.premiumSeparatorText {
premiumSeparatorText = current
} else {
premiumSeparatorText = ComponentView()
self.premiumSeparatorText = premiumSeparatorText
}
let premiumSeparatorLeft: SimpleLayer
if let current = self.premiumSeparatorLeft {
premiumSeparatorLeft = current
} else {
premiumSeparatorLeft = SimpleLayer()
self.premiumSeparatorLeft = premiumSeparatorLeft
self.scrollContentView.layer.addSublayer(premiumSeparatorLeft)
}
let premiumSeparatorRight: SimpleLayer
if let current = self.premiumSeparatorRight {
premiumSeparatorRight = current
} else {
premiumSeparatorRight = SimpleLayer()
self.premiumSeparatorRight = premiumSeparatorRight
self.scrollContentView.layer.addSublayer(premiumSeparatorRight)
}
premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor
contentHeight += 19.0
let premiumSeparatorTextSize = premiumSeparatorText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: environment.strings.SendInviteLink_PremiumOrSendSectionSeparator, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
)
let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize)
if let premiumSeparatorTextView = premiumSeparatorText.view {
if premiumSeparatorTextView.superview == nil {
self.scrollContentView.addSubview(premiumSeparatorTextView)
}
transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame)
}
let separatorWidth: CGFloat = 72.0
let separatorSpacing: CGFloat = 10.0
transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel)))
contentHeight += 31.0
} else {
if let premiumSeparatorLeft = self.premiumSeparatorLeft {
self.premiumSeparatorLeft = nil
premiumSeparatorLeft.removeFromSuperlayer()
}
if let premiumSeparatorRight = self.premiumSeparatorRight {
self.premiumSeparatorRight = nil
premiumSeparatorRight.removeFromSuperlayer()
}
if let premiumSeparatorText = self.premiumSeparatorText {
self.premiumSeparatorText = nil
premiumSeparatorText.view?.removeFromSuperview()
}
contentHeight += 14.0
}
} else {
if let premiumTitle = self.premiumTitle {
self.premiumTitle = nil
premiumTitle.view?.removeFromSuperview()
}
if let premiumText = self.premiumText {
self.premiumText = nil
premiumText.view?.removeFromSuperview()
}
if let premiumButton = self.premiumButton {
self.premiumButton = nil
premiumButton.view?.removeFromSuperview()
}
}
let containerInset: CGFloat = environment.statusBarHeight + 10.0
var initialContentHeight = contentHeight
let clippingY: CGFloat
if hasInviteLink {
let title: ComponentView<Empty>
if let current = self.title {
title = current
} else {
title = ComponentView()
self.title = title
}
let descriptionText: ComponentView<Empty>
if let current = self.descriptionText {
descriptionText = current
} else {
descriptionText = ComponentView()
self.descriptionText = descriptionText
}
let actionButton: ComponentView<Empty>
if let current = self.actionButton {
actionButton = current
} else {
actionButton = ComponentView()
self.actionButton = actionButton
}
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize)
if let titleView = title.view {
if titleView.superview == nil {
self.scrollContentView.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
contentHeight += titleSize.height
contentHeight += 8.0
let text: String
if !premiumRestrictedUsers.isEmpty {
if component.link != nil {
text = environment.strings.SendInviteLink_TextSendInviteLink
} else {
if component.peers.count == 1 {
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
} else {
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
}
}
} else {
if component.link != nil {
if component.peers.count == 1 {
text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
} else {
text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count))
}
} else {
if component.peers.count == 1 {
text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)).string
} else {
text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count))
}
}
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor)
let descriptionTextSize = descriptionText.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .markdown(text: text, attributes: MarkdownAttributes(
body: body,
bold: bold,
link: body,
linkAttribute: { _ in nil }
)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0)
)
let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize)
if let descriptionTextView = descriptionText.view {
if descriptionTextView.superview == nil {
self.scrollContentView.addSubview(descriptionTextView)
}
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
}
contentHeight += descriptionTextFrame.height
contentHeight += 22.0
initialContentHeight = contentHeight
var singleItemHeight: CGFloat = 0.0
var itemsHeight: CGFloat = 0.0
var validIds: [AnyHashable] = []
for i in 0 ..< component.peers.count {
let peer = component.peers[i]
for _ in 0 ..< 1 {
//let id: AnyHashable = AnyHashable("\(peer.id)_\(j)")
let id = AnyHashable(peer.peer.id)
validIds.append(id)
let item: ComponentView<Empty>
var itemTransition = transition
if let current = self.items[id] {
item = current
} else {
itemTransition = .immediate
item = ComponentView()
self.items[id] = item
}
let itemSubtitle: PeerListItemComponent.Subtitle
let canBeSelected = component.link != nil && !peer.premiumRequiredToContact
if peer.premiumRequiredToContact {
itemSubtitle = .text(text: environment.strings.SendInviteLink_StatusAvailableToPremiumOnly, icon: .lock)
} else {
itemSubtitle = .presence(component.peerPresences[peer.peer.id])
}
let itemSize = item.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
strings: environment.strings,
sideInset: 0.0,
title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
subtitle: itemSubtitle,
peer: peer.peer,
selectionState: !canBeSelected ? .none : .editing(isSelected: self.selectedItems.contains(peer.peer.id)),
hasNext: i != component.peers.count - 1,
action: { [weak self] peer in
guard let self else {
return
}
if !canBeSelected {
return
}
if self.selectedItems.contains(peer.id) {
self.selectedItems.remove(peer.id)
} else {
self.selectedItems.insert(peer.id)
}
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize)
if let itemView = item.view {
if itemView.superview == nil {
self.itemContainerView.addSubview(itemView)
}
itemTransition.setFrame(view: itemView, frame: itemFrame)
}
itemsHeight += itemSize.height
singleItemHeight = itemSize.height
}
}
var removeIds: [AnyHashable] = []
for (id, item) in self.items {
if !validIds.contains(id) {
removeIds.append(id)
item.view?.removeFromSuperview()
}
}
for id in removeIds {
self.items.removeValue(forKey: id)
}
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight)))
initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5))
contentHeight += itemsHeight
contentHeight += 24.0
initialContentHeight += 24.0
let actionButtonTitle: String
if component.link != nil {
actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite
} else {
actionButtonTitle = environment.strings.SendInviteLink_ActionClose
}
let actionButtonSize = actionButton.update(
transition: transition,
component: AnyComponent(SolidRoundedButtonComponent(
title: actionButtonTitle,
badge: (self.selectedItems.isEmpty || component.link == nil) ? nil : "\(self.selectedItems.count)",
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 11.0,
gloss: false,
animationName: nil,
iconPosition: .right,
iconSpacing: 4.0,
action: { [weak self] in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
if self.selectedItems.isEmpty {
controller.dismiss()
} else if let link = component.link {
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) }
let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start()
let text: String
if selectedPeers.count == 1 {
text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
} else if selectedPeers.count == 2 {
text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string
} else {
text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].peer.displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
controller.dismiss()
} else {
controller.dismiss()
}
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height
let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
if let actionButtonView = actionButton.view {
if actionButtonView.superview == nil {
self.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
contentHeight += bottomPanelHeight
initialContentHeight += bottomPanelHeight
clippingY = actionButtonFrame.minY - 24.0
} else {
if let title = self.title {
self.title = nil
title.view?.removeFromSuperview()
}
if let descriptionText = self.descriptionText {
self.descriptionText = nil
descriptionText.view?.removeFromSuperview()
}
if let actionButton = self.actionButton {
self.actionButton = nil
actionButton.view?.removeFromSuperview()
}
initialContentHeight += environment.safeInsets.bottom
clippingY = availableSize.height
}
let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight)
let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 10.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
self.ignoreScrolling = true
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height)))
let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
if resetScrolling {
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)
}
self.ignoreScrolling = false
self.updateScrolling(transition: transition)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class SendInviteLinkScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let link: String?
private let peers: [TelegramForbiddenInvitePeer]
private var isDismissed: Bool = false
private var presenceDisposable: Disposable?
public init(context: AccountContext, peer: EnginePeer, link: String?, peers: [TelegramForbiddenInvitePeer]) {
self.context = context
var link = link
if link == nil, let addressName = peer.addressName {
link = "https://t.me/\(addressName)"
}
#if DEBUG
var peers = peers
if !"".isEmpty {
enum TestConfiguration: CaseIterable {
case singlePeerNoPremiumLink
case singlePeerPremiumLink
case singlePeerNoPremiumNoLink
case singlePeerPremiumNoLink
case somePeersNoPremiumLink
case somePeersOnePremiumLink
case somePeersAllPremiumLink
case somePeersNoPremiumNoLink
case somePeersOnePremiumNoLink
case somePeersAllPremiumNoLink
case morePeersNoPremiumLink
case morePeersOnePremiumLink
case morePeersAllPremiumLink
case morePeersNoPremiumNoLink
case morePeersOnePremiumNoLink
case morePeersAllPremiumNoLink
}
var nextPeerId: Int64 = 1
let makePeer: (Bool, Bool) -> TelegramForbiddenInvitePeer = { canInviteWithPremium, premiumRequiredToContact in
guard case let .user(user) = peers[0].peer else {
preconditionFailure()
}
let id = nextPeerId
nextPeerId += 1
return TelegramForbiddenInvitePeer(
peer: .user(TelegramUser(
id: EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: EnginePeer.Id.Id._internalFromInt64Value(id)),
accessHash: user.accessHash,
firstName: user.firstName,
lastName: user.lastName,
username: user.username,
phone: user.phone,
photo: user.photo,
botInfo: user.botInfo,
restrictionInfo: user.restrictionInfo,
flags: user.flags,
emojiStatus: user.emojiStatus,
usernames: user.usernames,
storiesHidden: user.storiesHidden,
nameColor: user.nameColor,
backgroundEmojiId: user.backgroundEmojiId,
profileColor: user.profileColor,
profileBackgroundEmojiId: user.profileBackgroundEmojiId,
subscriberCount: user.subscriberCount,
verificationIconFileId: user.verificationIconFileId
)),
canInviteWithPremium: canInviteWithPremium,
premiumRequiredToContact: premiumRequiredToContact
)
}
let caseIndex = 9
let configuration = TestConfiguration.allCases[caseIndex]
do {
switch configuration {
case .singlePeerNoPremiumLink:
peers = [makePeer(false, false)]
link = "abcd"
case .singlePeerPremiumLink:
peers = [makePeer(true, false)]
link = "abcd"
case .singlePeerNoPremiumNoLink:
peers = [makePeer(false, false)]
link = nil
case .singlePeerPremiumNoLink:
peers = [makePeer(true, false)]
link = nil
case .somePeersNoPremiumLink:
peers = (0 ..< 3).map { _ in makePeer(false, false) }
link = "abcd"
case .somePeersOnePremiumLink:
peers = [
makePeer(false, false),
makePeer(true, true),
makePeer(false, false)
]
link = "abcd"
case .somePeersAllPremiumLink:
peers = (0 ..< 3).map { _ in makePeer(true, false) }
link = "abcd"
case .somePeersNoPremiumNoLink:
peers = (0 ..< 3).map { _ in makePeer(false, false) }
link = nil
case .somePeersOnePremiumNoLink:
peers = [
makePeer(false, false),
makePeer(true, false),
makePeer(false, false)
]
link = nil
case .somePeersAllPremiumNoLink:
peers = (0 ..< 3).map { _ in makePeer(true, false) }
link = nil
case .morePeersNoPremiumLink:
preconditionFailure()
case .morePeersOnePremiumLink:
preconditionFailure()
case .morePeersAllPremiumLink:
preconditionFailure()
case .morePeersNoPremiumNoLink:
preconditionFailure()
case .morePeersOnePremiumNoLink:
preconditionFailure()
case .morePeersAllPremiumNoLink:
preconditionFailure()
}
}
}
#endif
self.link = link
self.peers = peers
super.init(context: context, component: SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: [:]), navigationBarAppearance: .none)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.presenceDisposable = (context.engine.data.subscribe(EngineDataMap(
peers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.Presence.init(id:))
))
|> deliverOnMainQueue).start(next: { [weak self] presences in
guard let self else {
return
}
var parsedPresences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for (id, presence) in presences {
if let presence {
parsedPresences[id] = presence
}
}
self.updateComponent(component: AnyComponent(SendInviteLinkScreenComponent(context: context, peer: peer, link: link, peers: peers, peerPresences: parsedPresences)), transition: .immediate)
})
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presenceDisposable?.dispose()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View {
componentView.animateIn()
}
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
if let componentView = self.node.hostView.componentView as? SendInviteLinkScreenComponent.View {
componentView.animateOut(completion: { [weak self] in
completion?()
self?.dismiss(animated: false)
})
} else {
self.dismiss(animated: false)
}
}
}
}