Telegram-iOS/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift
2026-05-05 14:57:06 +02:00

1268 lines
59 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import TelegramPresentationData
import AccountContext
import TelegramCore
import ResizableSheetComponent
import PresentationDataUtils
import Markdown
import UndoUI
import AnimatedAvatarSetNode
import AvatarNode
import TelegramStringFormatting
import ChatMessagePaymentAlertController
import ResizableSheetComponent
import ButtonComponent
import BundleIconComponent
import GlassBarButtonComponent
import MultilineTextComponent
import SolidRoundedButtonComponent
private func sendInviteLinkHasInviteSection(subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer]) -> Bool {
let premiumRestrictedUsers = peers.filter { peer in
return peer.canInviteWithPremium
}
switch subject {
case let .chat(_, link):
if premiumRestrictedUsers.count == peers.count && link == nil {
return false
} else if link != nil && !premiumRestrictedUsers.isEmpty && peers.allSatisfy({ $0.premiumRequiredToContact }) {
return false
} else {
return true
}
case .groupCall:
return true
}
}
private final class SendInviteLinkContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: SendInviteLinkScreenSubject
let peers: [TelegramForbiddenInvitePeer]
let peerPresences: [EnginePeer.Id: EnginePeer.Presence]
let selectedItems: Set<EnginePeer.Id>
let theme: PresentationTheme
let toggleSelection: (EnginePeer.Id) -> Void
let openPremium: () -> Void
init(
context: AccountContext,
subject: SendInviteLinkScreenSubject,
peers: [TelegramForbiddenInvitePeer],
peerPresences: [EnginePeer.Id: EnginePeer.Presence],
selectedItems: Set<EnginePeer.Id>,
theme: PresentationTheme,
toggleSelection: @escaping (EnginePeer.Id) -> Void,
openPremium: @escaping () -> Void
) {
self.context = context
self.subject = subject
self.peers = peers
self.peerPresences = peerPresences
self.selectedItems = selectedItems
self.theme = theme
self.toggleSelection = toggleSelection
self.openPremium = openPremium
}
static func ==(lhs: SendInviteLinkContentComponent, rhs: SendInviteLinkContentComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peers != rhs.peers {
return false
}
if lhs.peerPresences != rhs.peerPresences {
return false
}
if lhs.selectedItems != rhs.selectedItems {
return false
}
if lhs.theme !== rhs.theme {
return false
}
return true
}
final class View: 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 var title: ComponentView<Empty>?
private var descriptionText: ComponentView<Empty>?
private let itemContainerView: UIView
private var items: [AnyHashable: ComponentView<Empty>] = [:]
private var component: SendInviteLinkContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.itemContainerView = UIView()
self.itemContainerView.clipsToBounds = true
self.itemContainerView.layer.cornerRadius = 26.0
super.init(frame: frame)
self.addSubview(self.itemContainerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SendInviteLinkContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let environment = environment[EnvironmentType.self].value
let theme = component.theme
self.component = component
self.state = state
let sideInset: CGFloat = 16.0
self.itemContainerView.backgroundColor = theme.list.itemBlocksBackgroundColor
let premiumRestrictedUsers = component.peers.filter { peer in
return peer.canInviteWithPremium
}
let hasInviteSection = sendInviteLinkHasInviteSection(subject: component.subject, peers: component.peers)
var contentHeight: CGFloat = 0.0
contentHeight += 120.0
let avatarsNode: AnimatedAvatarSetNode
if let current = self.avatarsNode {
avatarsNode = current
} else {
avatarsNode = AnimatedAvatarSetNode()
self.avatarsNode = avatarsNode
self.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: 64.0, height: 64.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: 48.0), size: avatarsSize)
transition.setFrame(view: avatarsNode.view, frame: avatarsFrame)
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: theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 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.addSubview(premiumTitleView)
}
transition.setFrame(view: premiumTitleView, frame: premiumTitleFrame)
}
contentHeight += premiumTitleSize.height
contentHeight += 8.0
let text: String
switch component.subject {
case let .chat(peer, _):
if premiumRestrictedUsers.count == 1 {
if case let .channel(channel) = 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) = 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) = peer, case .broadcast = channel.info {
text = environment.strings.SendInviteLink_ChannelTextContactsAndPremiumOneUser(peersText).string
} else {
text = environment.strings.SendInviteLink_TextContactsAndPremiumOneUser(peersText).string
}
}
}
case .groupCall:
if premiumRestrictedUsers.count == 1 {
text = environment.strings.SendInviteLink_TextCallsRestrictedOneUser(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 {
text = environment.strings.SendInviteLink_TextCallsRestrictedMultipleUsers(Int32(extraCount)).replacingOccurrences(of: "{user_list}", with: peersText)
} else {
text = environment.strings.SendInviteLink_TextCallsRestrictedOneUser(peersText).string
}
}
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: 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.addSubview(premiumTextView)
}
transition.setFrame(view: premiumTextView, frame: premiumTextFrame)
}
contentHeight += premiumTextSize.height
contentHeight += 22.0
let premiumButtonSize = premiumButton.update(
transition: premiumItemsTransition,
component: AnyComponent(SolidRoundedButtonComponent(
title: environment.strings.SendInviteLink_SubscribeToPremiumButton,
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: {
component.openPremium()
}
)),
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.addSubview(premiumButtonView)
}
premiumItemsTransition.setFrame(view: premiumButtonView, frame: premiumButtonFrame)
}
contentHeight += premiumButtonSize.height
if hasInviteSection {
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.layer.addSublayer(premiumSeparatorLeft)
}
let premiumSeparatorRight: SimpleLayer
if let current = self.premiumSeparatorRight {
premiumSeparatorRight = current
} else {
premiumSeparatorRight = SimpleLayer()
self.premiumSeparatorRight = premiumSeparatorRight
self.layer.addSublayer(premiumSeparatorRight)
}
premiumSeparatorLeft.backgroundColor = theme.list.itemPlainSeparatorColor.cgColor
premiumSeparatorRight.backgroundColor = 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: theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 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.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()
}
}
if hasInviteSection {
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 titleText: String
switch component.subject {
case let .chat(_, link):
titleText = link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle
case .groupCall:
titleText = environment.strings.SendInviteLink_InviteTitle
}
let titleSize = title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: titleText, font: Font.semibold(24.0), textColor: theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 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.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: titleFrame)
}
contentHeight += titleSize.height
contentHeight += 8.0
let text: String
switch component.subject {
case let .chat(_, link):
if !premiumRestrictedUsers.isEmpty {
if 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 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))
}
case let .groupCall(groupCall):
switch groupCall {
case .create:
if component.peers.count == 1 {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
text = environment.strings.SendInviteLink_TextCallsRestrictedSendOneInviteLink(component.peers[0].peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
} else {
text = environment.strings.SendInviteLink_TextCallsRestrictedSendInviteLink
}
case .existing:
text = environment.strings.SendInviteLink_TextCallsRestrictedSendInviteLink
}
}
let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor)
let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: 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,
lineSpacing: 0.2
)),
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.addSubview(descriptionTextView)
}
transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame)
}
contentHeight += descriptionTextFrame.height
contentHeight += 22.0
var itemsHeight: CGFloat = 0.0
var validIds: [AnyHashable] = []
if case .chat = component.subject {
for i in 0 ..< component.peers.count {
let peer = component.peers[i]
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 canBeSelected: Bool
switch component.subject {
case let .chat(_, link):
canBeSelected = link != nil && !peer.premiumRequiredToContact
case .groupCall:
canBeSelected = true
}
let itemSubtitle: PeerListItemComponent.Subtitle
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: 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: component.selectedItems.contains(peer.peer.id)),
hasNext: i != component.peers.count - 1,
action: { peer in
if canBeSelected {
component.toggleSelection(peer.id)
}
}
)),
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
}
}
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)))
if itemsHeight != 0.0 {
contentHeight += itemsHeight
contentHeight += 24.0
} else {
contentHeight += 4.0
}
contentHeight += 84.0
} else {
if let title = self.title {
self.title = nil
title.view?.removeFromSuperview()
}
if let descriptionText = self.descriptionText {
self.descriptionText = nil
descriptionText.view?.removeFromSuperview()
}
for (_, item) in self.items {
item.view?.removeFromSuperview()
}
self.items.removeAll()
transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: 0.0)))
contentHeight += 24.0 + environment.safeInsets.bottom
}
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class SendInviteLinkActionButtonComponent: Component {
let theme: PresentationTheme
let title: String
let badge: Int?
let displaysProgress: Bool
let action: () -> Void
init(
theme: PresentationTheme,
title: String,
badge: Int?,
displaysProgress: Bool,
action: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.badge = badge
self.displaysProgress = displaysProgress
self.action = action
}
static func ==(lhs: SendInviteLinkActionButtonComponent, rhs: SendInviteLinkActionButtonComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.badge != rhs.badge {
return false
}
if lhs.displaysProgress != rhs.displaysProgress {
return false
}
return true
}
final class View: UIView {
private let button = ComponentView<Empty>()
private var component: SendInviteLinkActionButtonComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: SendInviteLinkActionButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let content: AnyComponentWithIdentity<Empty>
if let badge = component.badge {
content = AnyComponentWithIdentity(id: "badge-\(component.title)-\(badge)", component: AnyComponent(ButtonTextContentComponent(
text: component.title,
badge: badge,
textColor: component.theme.list.itemCheckColors.foregroundColor,
badgeBackground: component.theme.list.itemCheckColors.foregroundColor,
badgeForeground: component.theme.list.itemCheckColors.fillColor
)))
} else {
content = AnyComponentWithIdentity(id: "title-\(component.title)", component: AnyComponent(Text(
text: component.title,
font: Font.semibold(17.0),
color: component.theme.list.itemCheckColors.foregroundColor
)))
}
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
style: .glass,
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: content,
isEnabled: true,
displaysProgress: component.displaysProgress,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
}
)),
environment: {},
containerSize: availableSize
)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: CGRect(origin: .zero, size: buttonSize))
}
return buttonSize
}
}
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)
}
}
final class SendInviteLinkScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let subject: SendInviteLinkScreenSubject
let peers: [TelegramForbiddenInvitePeer]
let peerPresences: [EnginePeer.Id: EnginePeer.Presence]
let sendPaidMessageStars: [EnginePeer.Id: StarsAmount]
init(
context: AccountContext,
subject: SendInviteLinkScreenSubject,
peers: [TelegramForbiddenInvitePeer],
peerPresences: [EnginePeer.Id: EnginePeer.Presence],
sendPaidMessageStars: [EnginePeer.Id: StarsAmount]
) {
self.context = context
self.subject = subject
self.peers = peers
self.peerPresences = peerPresences
self.sendPaidMessageStars = sendPaidMessageStars
}
static func ==(lhs: SendInviteLinkScreenComponent, rhs: SendInviteLinkScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peers != rhs.peers {
return false
}
if lhs.peerPresences != rhs.peerPresences {
return false
}
if lhs.sendPaidMessageStars != rhs.sendPaidMessageStars {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(EnvironmentType, ResizableSheetComponentEnvironment)>()
private let animateOut = ActionSlot<Action<Void>>()
private var selectedItems = Set<EnginePeer.Id>()
private var didInitializeSelection = false
private var isInProgress = false
private var isDismissing = false
private var component: SendInviteLinkScreenComponent?
private weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var createCallDisposable: Disposable?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.createCallDisposable?.dispose()
}
private func dismiss(controller: @escaping () -> ViewController?, animated: Bool) {
guard !self.isDismissing else {
return
}
self.isDismissing = true
let performDismiss: () -> Void = {
if let controller = controller() as? SendInviteLinkScreen {
controller.completePendingDismiss()
controller.dismiss(animated: false)
} else {
controller()?.dismiss(animated: false)
}
}
if animated {
self.animateOut.invoke(Action { _ in
performDismiss()
})
} else {
performDismiss()
}
}
private func presentPaidMessageAlertIfNeeded(peers: [EngineRenderedPeer], requiresStars: [EnginePeer.Id: StarsAmount], completion: @escaping () -> Void) {
guard let component = self.component else {
completion()
return
}
var totalAmount: StarsAmount = .zero
for peer in peers {
if let amount = requiresStars[peer.peerId] {
totalAmount = totalAmount + amount
}
}
if totalAmount.value > 0 {
let controller = chatMessagePaymentAlertController(
context: component.context,
presentationData: component.context.sharedContext.currentPresentationData.with { $0 },
updatedPresentationData: nil,
peers: peers,
count: 1,
amount: totalAmount,
totalAmount: totalAmount,
hasCheck: false,
navigationController: self.environment?.controller()?.navigationController as? NavigationController,
completion: { _ in
completion()
}
)
self.environment?.controller()?.present(controller, in: .window(.root))
} else {
completion()
}
}
private func sendInviteLink(link: String, selectedPeers: [TelegramForbiddenInvitePeer], completion: (() -> Void)? = nil) {
guard let component = self.component, let environment = self.environment else {
return
}
self.presentPaidMessageAlertIfNeeded(
peers: selectedPeers.map { EngineRenderedPeer(peer: $0.peer) },
requiresStars: component.sendPaidMessageStars,
completion: { [weak self] in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
for peerId in Array(self.selectedItems) {
var messageAttributes: [EngineMessage.Attribute] = []
if let sendPaidMessageStars = component.sendPaidMessageStars[peerId] {
messageAttributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false))
}
let _ = enqueueMessages(account: component.context.account, peerId: peerId, messages: [.message(text: link, attributes: messageAttributes, inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone()
}
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))
completion?()
}
)
}
private func performMainAction() {
guard let component = self.component, let controller = self.environment?.controller() else {
return
}
let link: String?
switch component.subject {
case let .chat(_, linkValue):
link = linkValue
case let .groupCall(groupCall):
switch groupCall {
case .create:
self.isInProgress = true
self.state?.updated(transition: .immediate)
self.createCallDisposable = (component.context.engine.calls.createConferenceCall()
|> deliverOnMainQueue).startStrict(next: { [weak self] call in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
if self.selectedItems.isEmpty {
controller.dismiss()
} else {
let link = call.link
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) }
self.sendInviteLink(link: link, selectedPeers: selectedPeers, completion: { [weak self] in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
let navigationController = controller.navigationController as? NavigationController
let context = component.context
controller.dismiss(completion: { [weak navigationController] in
if let navigationController, let peer = selectedPeers.first?.peer {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: context,
chatLocation: .peer(peer)
))
}
})
})
}
})
return
case let .existing(linkValue):
link = linkValue
}
}
if self.selectedItems.isEmpty {
controller.dismiss()
} else if let link {
let selectedPeers = component.peers.filter { self.selectedItems.contains($0.peer.id) }
self.sendInviteLink(link: link, selectedPeers: selectedPeers, completion: { [weak self] in
self?.environment?.controller()?.dismiss()
})
} else {
controller.dismiss()
}
}
func update(component: SendInviteLinkScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
if !self.didInitializeSelection {
self.didInitializeSelection = true
for peer in component.peers {
switch component.subject {
case let .chat(_, link):
if link != nil && !peer.premiumRequiredToContact {
self.selectedItems.insert(peer.peer.id)
}
case .groupCall:
self.selectedItems.insert(peer.peer.id)
}
}
}
self.component = component
self.state = state
let environmentValue = environment[EnvironmentType.self].value
self.environment = environmentValue
let controller = environmentValue.controller
let theme = environmentValue.theme.withModalBlocksBackground()
let hasInviteSection = sendInviteLinkHasInviteSection(subject: component.subject, peers: component.peers)
let dismiss: (Bool) -> Void = { [weak self] animated in
self?.dismiss(controller: controller, animated: animated)
}
let actionTitle: String
let actionBadge: Int?
switch component.subject {
case let .chat(_, link):
if link != nil {
actionTitle = self.selectedItems.isEmpty ? environmentValue.strings.SendInviteLink_ActionSkip : environmentValue.strings.SendInviteLink_ActionInvite
} else {
actionTitle = environmentValue.strings.SendInviteLink_ActionClose
}
actionBadge = (self.selectedItems.isEmpty || link == nil) ? nil : self.selectedItems.count
case .groupCall:
actionTitle = environmentValue.strings.SendInviteLink_ActionInvite
actionBadge = nil
}
let bottomItem: AnyComponent<Empty>?
if hasInviteSection {
bottomItem = AnyComponent(SendInviteLinkActionButtonComponent(
theme: theme,
title: actionTitle,
badge: actionBadge,
displaysProgress: self.isInProgress,
action: { [weak self] in
self?.performMainAction()
}
))
} else {
bottomItem = nil
}
let sheetSize = self.sheet.update(
transition: transition,
component: AnyComponent(ResizableSheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(SendInviteLinkContentComponent(
context: component.context,
subject: component.subject,
peers: component.peers,
peerPresences: component.peerPresences,
selectedItems: self.selectedItems,
theme: theme,
toggleSelection: { [weak self] peerId in
guard let self else {
return
}
if self.selectedItems.contains(peerId) {
self.selectedItems.remove(peerId)
} else {
self.selectedItems.insert(peerId)
}
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.3, curve: .easeInOut)))
},
openPremium: { [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)
}
)),
titleItem: nil,
leftItem: AnyComponent(
GlassBarButtonComponent(
size: CGSize(width: 44.0, height: 44.0),
backgroundColor: nil,
isDark: theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: theme.chat.inputPanel.panelControlColor
)
)),
action: { _ in
dismiss(true)
}
)
),
rightItem: nil,
hasTopEdgeEffect: false,
bottomItem: bottomItem,
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
defaultHeight: 540.0,
animateOut: self.animateOut
)),
environment: {
environmentValue
ResizableSheetComponentEnvironment(
theme: theme,
statusBarHeight: environmentValue.statusBarHeight,
safeInsets: environmentValue.safeInsets,
inputHeight: 0.0,
metrics: environmentValue.metrics,
deviceMetrics: environmentValue.deviceMetrics,
isDisplaying: environmentValue.isVisible,
isCentered: environmentValue.metrics.widthClass == .regular,
screenSize: availableSize,
regularMetricsSize: nil,
dismiss: { animated in
dismiss(animated)
}
)
},
containerSize: availableSize
)
self.sheet.parentState = state
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class SendInviteLinkScreen: ViewControllerComponentContainer {
private var isDismissed: Bool = false
private var dismissCompletion: (() -> Void)?
private var presenceDisposable: Disposable?
public init(context: AccountContext, subject: SendInviteLinkScreenSubject, peers: [TelegramForbiddenInvitePeer], theme: PresentationTheme? = nil) {
super.init(
context: context,
component: SendInviteLinkScreenComponent(
context: context,
subject: subject,
peers: peers,
peerPresences: [:],
sendPaidMessageStars: [:]
),
navigationBarAppearance: .none,
theme: theme.flatMap { .custom($0) } ?? .default
)
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:))
),
EngineDataMap(
peers.map(\.peer.id).map(TelegramEngine.EngineData.Item.Peer.SendPaidMessageStars.init(id:))
)
)
|> deliverOnMainQueue).start(next: { [weak self] presences, sendPaidMessageStars in
guard let self else {
return
}
var parsedPresences: [EnginePeer.Id: EnginePeer.Presence] = [:]
for (id, presence) in presences {
if let presence {
parsedPresences[id] = presence
}
}
var parsedSendPaidMessageStars: [EnginePeer.Id: StarsAmount] = [:]
for (id, sendPaidMessageStars) in sendPaidMessageStars {
if let sendPaidMessageStars {
parsedSendPaidMessageStars[id] = sendPaidMessageStars
}
}
self.updateComponent(
component: AnyComponent(SendInviteLinkScreenComponent(
context: context,
subject: subject,
peers: peers,
peerPresences: parsedPresences,
sendPaidMessageStars: parsedSendPaidMessageStars
)),
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
}
func completePendingDismiss() {
let dismissCompletion = self.dismissCompletion
self.dismissCompletion = nil
dismissCompletion?()
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.isDismissed {
self.isDismissed = true
self.dismissCompletion = completion
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
view.dismissAnimated()
} else {
self.completePendingDismiss()
self.dismiss(animated: false)
}
}
}
}