mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-29 11:25:38 +00:00
1439 lines
70 KiB
Swift
1439 lines
70 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 Postbox
|
|
import MultilineTextComponent
|
|
import PresentationDataUtils
|
|
import ButtonComponent
|
|
import TokenListTextField
|
|
import AvatarNode
|
|
import LocalizedPeerData
|
|
import PeerListItemComponent
|
|
import LottieComponent
|
|
import TooltipUI
|
|
import Markdown
|
|
import TelegramStringFormatting
|
|
import ListSectionComponent
|
|
import ListActionItemComponent
|
|
import BundleIconComponent
|
|
import GlassBarButtonComponent
|
|
import EdgeEffect
|
|
import TextFormat
|
|
import ListItemSliderSelectorComponent
|
|
import CreateExternalMediaStreamScreen
|
|
|
|
final class LiveStreamSettingsScreenComponent: Component {
|
|
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
|
|
|
let context: AccountContext
|
|
let stateContext: LiveStreamSettingsScreen.StateContext
|
|
let editCategory: (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void
|
|
let editBlockedPeers: (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void
|
|
let completion: (LiveStreamSettingsScreen.Result) -> Void
|
|
|
|
init(
|
|
context: AccountContext,
|
|
stateContext: LiveStreamSettingsScreen.StateContext,
|
|
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void,
|
|
editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void,
|
|
completion: @escaping (LiveStreamSettingsScreen.Result) -> Void
|
|
) {
|
|
self.context = context
|
|
self.stateContext = stateContext
|
|
self.editCategory = editCategory
|
|
self.editBlockedPeers = editBlockedPeers
|
|
self.completion = completion
|
|
}
|
|
|
|
static func ==(lhs: LiveStreamSettingsScreenComponent, rhs: LiveStreamSettingsScreenComponent) -> Bool {
|
|
return true
|
|
}
|
|
|
|
private final class ScrollView: UIScrollView {
|
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
final class View: UIView, UIScrollViewDelegate {
|
|
private let scrollView: ScrollView
|
|
private let topEdgeEffectView: EdgeEffectView
|
|
private let bottomEdgeEffectView: EdgeEffectView
|
|
|
|
private let streamAsSection = ComponentView<Empty>()
|
|
private let privacySection = ComponentView<Empty>()
|
|
private let externalStreamSection = ComponentView<Empty>()
|
|
private let settingsSection = ComponentView<Empty>()
|
|
private let paidMessageSection = ComponentView<Empty>()
|
|
|
|
private let title = ComponentView<Empty>()
|
|
private let cancelButton = ComponentView<Empty>()
|
|
private let doneButton = ComponentView<Empty>()
|
|
private let actionButton = ComponentView<Empty>()
|
|
|
|
private var isUpdating: Bool = false
|
|
private var ignoreScrolling: Bool = false
|
|
|
|
private var component: LiveStreamSettingsScreenComponent?
|
|
private(set) weak var state: EmptyComponentState?
|
|
private var environment: EnvironmentType?
|
|
|
|
override init(frame: CGRect) {
|
|
self.scrollView = ScrollView()
|
|
self.scrollView.showsVerticalScrollIndicator = true
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.scrollsToTop = false
|
|
self.scrollView.delaysContentTouches = false
|
|
self.scrollView.canCancelContentTouches = true
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
self.scrollView.alwaysBounceVertical = true
|
|
|
|
self.topEdgeEffectView = EdgeEffectView()
|
|
self.bottomEdgeEffectView = EdgeEffectView()
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.scrollView.delegate = self
|
|
self.addSubview(self.scrollView)
|
|
|
|
self.addSubview(self.topEdgeEffectView)
|
|
self.addSubview(self.bottomEdgeEffectView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
if !self.ignoreScrolling {
|
|
self.updateScrolling(transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
self.endEditing(true)
|
|
}
|
|
|
|
private func updateScrolling(transition: ComponentTransition) {
|
|
|
|
}
|
|
|
|
fileprivate var credentialsPromise = Promise<GroupCallStreamCredentials>()
|
|
private func presentCreateExternalStream() {
|
|
guard let component = self.component, let controller = self.environment?.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
var dismissImpl: (() -> Void)?
|
|
let streamController = CreateExternalMediaStreamScreen(
|
|
context: component.context,
|
|
peerId: component.stateContext.sendAsPeerId ?? component.context.account.peerId,
|
|
credentialsPromise: self.credentialsPromise,
|
|
mode: .create(liveStream: true),
|
|
completion: { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.complete(rtmp: true)
|
|
dismissImpl?()
|
|
}
|
|
)
|
|
dismissImpl = { [weak controller] in
|
|
guard let controller, let navigationController = controller.navigationController as? NavigationController else {
|
|
return
|
|
}
|
|
var controllers = navigationController.viewControllers
|
|
controllers = controllers.filter { c in
|
|
return !(c is LiveStreamSettingsScreen || c is CreateExternalMediaStreamScreen)
|
|
}
|
|
navigationController.setViewControllers(controllers, animated: true)
|
|
}
|
|
controller.push(streamController)
|
|
}
|
|
|
|
private func presentStreamAsPeer() {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
let stateContext = ShareWithPeersScreen.StateContext(
|
|
context: component.context,
|
|
subject: .peers(peers: component.stateContext.stateValue?.sendAsPeers ?? [], peerId: component.stateContext.sendAsPeerId),
|
|
liveStream: true,
|
|
editing: false
|
|
)
|
|
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
let peersController = ShareWithPeersScreen(
|
|
context: component.context,
|
|
initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []),
|
|
stateContext: stateContext,
|
|
completion: { _, _, _, _, _, _, _ in },
|
|
editCategory: { _, _, _, _ in },
|
|
editBlockedPeers: { _, _, _, _ in },
|
|
peerCompletion: { [weak self] peerId in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
self.credentialsPromise.set(component.context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, isLiveStream: true, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
|
|
|
component.stateContext.sendAsPeerId = peerId
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
}
|
|
)
|
|
if let controller = self.environment?.controller() as? LiveStreamSettingsScreen {
|
|
controller.dismissAllTooltips()
|
|
controller.push(peersController)
|
|
}
|
|
})
|
|
}
|
|
|
|
private func complete(rtmp: Bool) {
|
|
guard let component = self.component else {
|
|
return
|
|
}
|
|
component.completion(
|
|
LiveStreamSettingsScreen.Result(
|
|
sendAsPeerId: component.stateContext.sendAsPeerId,
|
|
privacy: component.stateContext.privacy,
|
|
allowComments: component.stateContext.allowComments,
|
|
isForwardingDisabled: component.stateContext.isForwardingDisabled,
|
|
pin: component.stateContext.pin,
|
|
paidMessageStars: component.stateContext.paidMessageStars,
|
|
startRtmpStream: rtmp
|
|
)
|
|
)
|
|
}
|
|
|
|
func update(component: LiveStreamSettingsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
|
|
self.isUpdating = true
|
|
defer {
|
|
self.isUpdating = false
|
|
}
|
|
|
|
var alphaTransition = transition
|
|
if !transition.animation.isImmediate {
|
|
alphaTransition = alphaTransition.withAnimation(.curve(duration: 0.25, curve: .easeInOut))
|
|
}
|
|
|
|
let environment = environment[EnvironmentType.self].value
|
|
let themeUpdated = self.environment?.theme !== environment.theme
|
|
self.environment = environment
|
|
|
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
|
let theme = environment.theme.withModalBlocksBackground()
|
|
|
|
guard let screenState = component.stateContext.stateValue else {
|
|
return CGSize()
|
|
}
|
|
|
|
if self.component == nil {
|
|
self.credentialsPromise.set(component.context.engine.calls.getGroupCallStreamCredentials(peerId: screenState.sendAsPeerId ?? component.context.account.peerId, isLiveStream: true, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
|
}
|
|
|
|
self.component = component
|
|
self.state = state
|
|
|
|
let topInset: CGFloat = 24.0
|
|
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
|
let sectionSpacing: CGFloat = 32.0
|
|
|
|
if themeUpdated {
|
|
self.backgroundColor = theme.list.blocksBackgroundColor
|
|
}
|
|
|
|
let footerTextFont = Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
|
let footerBoldTextFont = Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize)
|
|
let footerTextColor = theme.list.freeTextColor
|
|
|
|
var contentHeight: CGFloat = 0.0
|
|
contentHeight += environment.navigationHeight
|
|
contentHeight += topInset
|
|
|
|
let effectiveSendAsPeerId = screenState.sendAsPeerId ?? component.context.account.peerId
|
|
if screenState.sendAsPeers.count > 1, let peer = screenState.sendAsPeers.first(where: { $0.id == effectiveSendAsPeerId }) {
|
|
let subtitle: String?
|
|
if case .user = peer {
|
|
subtitle = environment.strings.VoiceChat_PersonalAccount
|
|
} else {
|
|
if case let .channel(channel) = peer {
|
|
if case .broadcast = channel.info {
|
|
if let count = component.stateContext.stateValue?.participants[peer.id] {
|
|
subtitle = environment.strings.Conversation_StatusSubscribers(Int32(max(1, count)))
|
|
} else {
|
|
subtitle = environment.strings.Channel_Status
|
|
}
|
|
} else {
|
|
if let count = component.stateContext.stateValue?.participants[peer.id] {
|
|
subtitle = environment.strings.Conversation_StatusMembers(Int32(max(1, count)))
|
|
} else {
|
|
subtitle = environment.strings.Group_Status
|
|
}
|
|
}
|
|
} else {
|
|
subtitle = nil
|
|
}
|
|
}
|
|
|
|
let streamAsSectionItems = [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
|
PeerListItemComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
strings: environment.strings,
|
|
style: .generic,
|
|
sideInset: 0.0,
|
|
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
|
peer: peer,
|
|
subtitle: subtitle.flatMap { PeerListItemComponent.Subtitle(text: $0, color: .neutral) },
|
|
subtitleAccessory: .none,
|
|
presence: nil,
|
|
rightAccessory: screenState.isCustomTarget ? .none : .disclosure,
|
|
selectionState: .none,
|
|
hasNext: false,
|
|
action: screenState.isCustomTarget ? nil : { [weak self] _, _, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.presentStreamAsPeer()
|
|
}
|
|
)
|
|
))]
|
|
|
|
let streamAsSectionHeader = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: "START LIVE AS",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
|
textColor: theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let streamAsSectionSize = self.streamAsSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: streamAsSectionHeader,
|
|
footer: nil,
|
|
items: streamAsSectionItems
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let streamAsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: streamAsSectionSize)
|
|
if let streamAsSectionView = self.streamAsSection.view as? ListSectionComponent.View {
|
|
if streamAsSectionView.superview == nil {
|
|
self.scrollView.addSubview(streamAsSectionView)
|
|
self.streamAsSection.parentState = state
|
|
}
|
|
transition.setFrame(view: streamAsSectionView, frame: streamAsSectionFrame)
|
|
}
|
|
contentHeight += streamAsSectionSize.height
|
|
contentHeight += sectionSpacing
|
|
}
|
|
|
|
if screenState.sendAsPeerId?.namespace != Namespaces.Peer.CloudChannel {
|
|
var privacySectionItems: [AnyComponentWithIdentity<Empty>] = []
|
|
|
|
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
|
|
|
var everyoneSubtitle = environment.strings.Story_Privacy_ExcludePeople
|
|
if (screenState.savedSelectedPeers[.everyone]?.count ?? 0) > 0 {
|
|
var peerNamesArray: [String] = []
|
|
var peersCount = 0
|
|
if let peerIds = screenState.savedSelectedPeers[.everyone] {
|
|
peersCount = peerIds.count
|
|
for peerId in peerIds {
|
|
if let peer = screenState.peersMap[peerId] {
|
|
peerNamesArray.append(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder))
|
|
}
|
|
}
|
|
}
|
|
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
|
|
if peersCount == 1 {
|
|
if !peerNames.isEmpty {
|
|
everyoneSubtitle = environment.strings.Story_Privacy_ExcludePeopleExceptNames(peerNames).string
|
|
} else {
|
|
everyoneSubtitle = environment.strings.Story_Privacy_ExcludePeopleExcept(1)
|
|
}
|
|
} else {
|
|
if !peerNames.isEmpty {
|
|
everyoneSubtitle = environment.strings.Story_Privacy_ExcludePeopleExceptNames(peerNames).string
|
|
} else {
|
|
everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeopleExcept(Int32(peersCount))
|
|
}
|
|
}
|
|
}
|
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
|
id: .everyone,
|
|
title: environment.strings.Story_Privacy_CategoryEveryone,
|
|
icon: "Media Editor/Privacy/Everyone",
|
|
iconColor: .blue,
|
|
actionTitle: everyoneSubtitle
|
|
))
|
|
|
|
var contactsSubtitle = environment.strings.Story_Privacy_ExcludePeople
|
|
if (screenState.savedSelectedPeers[.contacts]?.count ?? 0) > 0 {
|
|
var peerNamesArray: [String] = []
|
|
var peersCount = 0
|
|
if let peerIds = screenState.savedSelectedPeers[.contacts] {
|
|
peersCount = peerIds.count
|
|
for peerId in peerIds {
|
|
if let peer = screenState.peersMap[peerId] {
|
|
peerNamesArray.append(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder))
|
|
}
|
|
}
|
|
}
|
|
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
|
|
if peersCount == 1 {
|
|
if !peerNames.isEmpty {
|
|
contactsSubtitle = environment.strings.Story_Privacy_ExcludePeopleExceptNames(peerNames).string
|
|
} else {
|
|
contactsSubtitle = environment.strings.Story_Privacy_ExcludePeopleExcept(1)
|
|
}
|
|
} else {
|
|
if !peerNames.isEmpty {
|
|
contactsSubtitle = environment.strings.Story_Privacy_ExcludePeopleExceptNames(peerNames).string
|
|
} else {
|
|
contactsSubtitle = environment.strings.Story_Privacy_ExcludePeopleExcept(Int32(peersCount))
|
|
}
|
|
}
|
|
}
|
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
|
id: .contacts,
|
|
title: environment.strings.Story_Privacy_CategoryContacts,
|
|
icon: "Media Editor/Privacy/Contacts",
|
|
iconColor: .violet,
|
|
actionTitle: contactsSubtitle
|
|
))
|
|
|
|
var closeFriendsSubtitle = environment.strings.Story_Privacy_EditList
|
|
if !screenState.closeFriendsPeers.isEmpty {
|
|
if screenState.closeFriendsPeers.count > 2 {
|
|
closeFriendsSubtitle = environment.strings.Story_Privacy_People(Int32(screenState.closeFriendsPeers.count))
|
|
} else {
|
|
closeFriendsSubtitle = String(screenState.closeFriendsPeers.map { $0.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder) }.joined(separator: ", "))
|
|
}
|
|
}
|
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
|
id: .closeFriends,
|
|
title: environment.strings.Story_Privacy_CategoryCloseFriends,
|
|
icon: "Media Editor/Privacy/CloseFriends",
|
|
iconColor: .green,
|
|
actionTitle: closeFriendsSubtitle
|
|
))
|
|
|
|
var selectedContactsSubtitle = environment.strings.Story_Privacy_Choose
|
|
if (screenState.savedSelectedPeers[.nobody]?.count ?? 0) > 0 {
|
|
var peerNamesArray: [String] = []
|
|
var peersCount = 0
|
|
if let peerIds = screenState.savedSelectedPeers[.nobody] {
|
|
peersCount = peerIds.count
|
|
for peerId in peerIds {
|
|
if let peer = screenState.peersMap[peerId] {
|
|
peerNamesArray.append(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder))
|
|
}
|
|
}
|
|
}
|
|
let peerNames = String(peerNamesArray.map { $0 }.joined(separator: ", "))
|
|
if peersCount == 1 {
|
|
if !peerNames.isEmpty {
|
|
selectedContactsSubtitle = peerNames
|
|
} else {
|
|
selectedContactsSubtitle = environment.strings.Story_Privacy_People(1)
|
|
}
|
|
} else {
|
|
if !peerNames.isEmpty {
|
|
selectedContactsSubtitle = peerNames
|
|
} else {
|
|
selectedContactsSubtitle = environment.strings.Story_Privacy_People(Int32(peersCount))
|
|
}
|
|
}
|
|
}
|
|
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
|
id: .selectedContacts,
|
|
title: environment.strings.Story_Privacy_CategorySelectedContacts,
|
|
icon: "Media Editor/Privacy/SelectedUsers",
|
|
iconColor: .yellow,
|
|
actionTitle: selectedContactsSubtitle
|
|
))
|
|
|
|
|
|
for i in 0 ..< categoryItems.count {
|
|
let item = categoryItems[i]
|
|
|
|
var isSelected = false
|
|
switch screenState.privacy.base {
|
|
case .everyone:
|
|
isSelected = item.id == .everyone
|
|
case .contacts:
|
|
isSelected = item.id == .contacts
|
|
case .closeFriends:
|
|
isSelected = item.id == .closeFriends
|
|
case .nobody:
|
|
isSelected = item.id == .selectedContacts
|
|
}
|
|
|
|
privacySectionItems.append(AnyComponentWithIdentity(id: item.id, component: AnyComponent(
|
|
CategoryListItemComponent(
|
|
context: component.context,
|
|
theme: theme,
|
|
title: item.title,
|
|
color: item.iconColor,
|
|
iconName: item.icon,
|
|
subtitle: item.actionTitle,
|
|
selectionState: .editing(isSelected:isSelected, isTinted: false),
|
|
hasNext: i != categoryItems.count - 1,
|
|
action: { [weak self] in
|
|
guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
if isSelected {
|
|
} else {
|
|
let base: EngineStoryPrivacy.Base
|
|
switch item.id {
|
|
case .everyone:
|
|
base = .everyone
|
|
case .contacts:
|
|
base = .contacts
|
|
case .closeFriends:
|
|
base = .closeFriends
|
|
case .selectedContacts:
|
|
base = .nobody
|
|
}
|
|
let selectedPeers = component.stateContext.stateValue?.savedSelectedPeers[base] ?? []
|
|
|
|
component.stateContext.privacy = EngineStoryPrivacy(base: base, additionallyIncludePeers: selectedPeers)
|
|
|
|
let closeFriends = self.component?.stateContext.stateValue?.closeFriendsPeers ?? []
|
|
if item.id == .selectedContacts && selectedPeers.isEmpty {
|
|
component.editCategory(
|
|
EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []),
|
|
screenState.allowComments,
|
|
screenState.isForwardingDisabled,
|
|
screenState.pin,
|
|
screenState.paidMessageStars
|
|
)
|
|
controller.dismissAllTooltips()
|
|
controller.dismiss()
|
|
} else if item.id == .closeFriends && closeFriends.isEmpty {
|
|
component.editCategory(
|
|
EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []),
|
|
screenState.allowComments,
|
|
screenState.isForwardingDisabled,
|
|
screenState.pin,
|
|
screenState.paidMessageStars
|
|
)
|
|
controller.dismissAllTooltips()
|
|
controller.dismiss()
|
|
}
|
|
}
|
|
self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.35, curve: .spring)))
|
|
},
|
|
secondaryAction: { [weak self] in
|
|
guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
let base: EngineStoryPrivacy.Base
|
|
switch item.id {
|
|
case .everyone:
|
|
base = .everyone
|
|
case .contacts:
|
|
base = .contacts
|
|
case .closeFriends:
|
|
base = .closeFriends
|
|
case .selectedContacts:
|
|
base = .nobody
|
|
}
|
|
let selectedPeers = component.stateContext.stateValue?.savedSelectedPeers[base] ?? []
|
|
|
|
component.editCategory(
|
|
EngineStoryPrivacy(base: base, additionallyIncludePeers: selectedPeers),
|
|
screenState.allowComments,
|
|
screenState.isForwardingDisabled,
|
|
screenState.pin,
|
|
screenState.paidMessageStars
|
|
)
|
|
controller.dismissAllTooltips()
|
|
controller.dismiss()
|
|
}
|
|
)
|
|
)))
|
|
}
|
|
|
|
let privacySectionHeader = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: "WHO CAN VIEW THIS LIVE",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
|
textColor: theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let privacySectionFooter = AnyComponent(MultilineTextComponent(
|
|
text: .markdown(
|
|
text: "[Select people]() who won't see your live.",
|
|
attributes: MarkdownAttributes(
|
|
body: MarkdownAttributeSet(font: footerTextFont, textColor: footerTextColor),
|
|
bold: MarkdownAttributeSet(font: footerBoldTextFont, textColor: footerTextColor),
|
|
link: MarkdownAttributeSet(font: footerTextFont, textColor: theme.list.itemAccentColor),
|
|
linkAttribute: { contents in
|
|
return (TelegramTextAttributes.URL, contents)
|
|
}
|
|
)
|
|
),
|
|
maximumNumberOfLines: 0,
|
|
highlightColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.2),
|
|
highlightAction: { attributes in
|
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
|
} else {
|
|
return nil
|
|
}
|
|
},
|
|
tapAction: { [weak self] _, _ in
|
|
guard let self, let component = self.component, let environment = self.environment, let controller = environment.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
component.editBlockedPeers(
|
|
component.stateContext.privacy,
|
|
screenState.allowComments,
|
|
screenState.isForwardingDisabled,
|
|
screenState.pin,
|
|
screenState.paidMessageStars
|
|
)
|
|
controller.dismissAllTooltips()
|
|
controller.dismiss()
|
|
}
|
|
))
|
|
|
|
var privacySectionTransition = transition
|
|
if self.privacySection.view == nil {
|
|
privacySectionTransition = .immediate
|
|
}
|
|
let privacySectionSize = self.privacySection.update(
|
|
transition: privacySectionTransition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: privacySectionHeader,
|
|
footer: privacySectionFooter,
|
|
items: privacySectionItems
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let privacySectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: privacySectionSize)
|
|
if let privacySectionView = self.privacySection.view as? ListSectionComponent.View {
|
|
if privacySectionView.superview == nil {
|
|
self.scrollView.addSubview(privacySectionView)
|
|
self.privacySection.parentState = state
|
|
|
|
privacySectionView.alpha = 1.0
|
|
transition.animateAlpha(view: privacySectionView, from: 0.0, to: 1.0)
|
|
}
|
|
privacySectionTransition.setFrame(view: privacySectionView, frame: privacySectionFrame)
|
|
}
|
|
contentHeight += privacySectionSize.height
|
|
contentHeight += sectionSpacing
|
|
} else if let privacySectionView = self.privacySection.view as? ListSectionComponent.View {
|
|
transition.setAlpha(view: privacySectionView, alpha: 0.0, completion: { _ in
|
|
privacySectionView.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
if !screenState.isEdit {
|
|
let externalStreamSectionItems = [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
|
ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Connect Stream", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: theme.list.itemPrimaryTextColor)))),
|
|
action: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.presentCreateExternalStream()
|
|
}
|
|
)
|
|
))]
|
|
let externalStreamFooterComponent = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "Stream with a different app.", font: footerTextFont, textColor: footerTextColor)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let externalStreamSectionSize = self.externalStreamSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: nil,
|
|
footer: externalStreamFooterComponent,
|
|
items: externalStreamSectionItems
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let externalStreamSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: externalStreamSectionSize)
|
|
if let externalStreamSectionView = self.externalStreamSection.view as? ListSectionComponent.View {
|
|
if externalStreamSectionView.superview == nil {
|
|
self.scrollView.addSubview(externalStreamSectionView)
|
|
self.externalStreamSection.parentState = state
|
|
}
|
|
transition.setFrame(view: externalStreamSectionView, frame: externalStreamSectionFrame)
|
|
}
|
|
contentHeight += externalStreamSectionSize.height
|
|
contentHeight += sectionSpacing
|
|
}
|
|
|
|
|
|
var settingsSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
|
settingsSectionItems.append(AnyComponentWithIdentity(id: "comments", component: AnyComponent(ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: "Allow Comments",
|
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)),
|
|
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: screenState.allowComments, action: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
component.stateContext.allowComments = !component.stateContext.allowComments
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
})),
|
|
action: nil
|
|
))))
|
|
|
|
settingsSectionItems.append(AnyComponentWithIdentity(id: "screenshots", component: AnyComponent(ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: "Allow Screenshots",
|
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)),
|
|
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: !screenState.isForwardingDisabled, action: { [weak self] _ in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
component.stateContext.isForwardingDisabled = !component.stateContext.isForwardingDisabled
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
})),
|
|
action: nil
|
|
))))
|
|
|
|
var pinTitle = "Post to My Profile"
|
|
var pinInfo = "Keep this live on your profile."
|
|
if screenState.sendAsPeerId?.namespace == Namespaces.Peer.CloudChannel {
|
|
pinTitle = "Post to Channel Profile"
|
|
pinInfo = "Keep this live on channel profile."
|
|
}
|
|
|
|
settingsSectionItems.append(AnyComponentWithIdentity(id: "pin", component: AnyComponent(ListActionItemComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
title: AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: pinTitle,
|
|
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
|
|
textColor: theme.list.itemPrimaryTextColor
|
|
)),
|
|
maximumNumberOfLines: 1
|
|
)),
|
|
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: screenState.pin, action: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
component.stateContext.pin = !component.stateContext.pin
|
|
self.state?.updated(transition: .spring(duration: 0.4))
|
|
})),
|
|
action: nil
|
|
))))
|
|
|
|
let settingsSectionFooterComponent = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: pinInfo, font: footerTextFont, textColor: footerTextColor)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let settingsSectionSize = self.settingsSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: nil,
|
|
footer: settingsSectionFooterComponent,
|
|
items: settingsSectionItems
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let settingsSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: settingsSectionSize)
|
|
if let settingsSectionView = self.settingsSection.view {
|
|
if settingsSectionView.superview == nil {
|
|
self.scrollView.addSubview(settingsSectionView)
|
|
self.settingsSection.parentState = state
|
|
}
|
|
transition.setFrame(view: settingsSectionView, frame: settingsSectionFrame)
|
|
}
|
|
contentHeight += settingsSectionSize.height
|
|
contentHeight += sectionSpacing
|
|
|
|
let paidMessageSectionItems = [AnyComponentWithIdentity(id: 0, component: AnyComponent(
|
|
ListItemSliderSelectorComponent(
|
|
theme: theme,
|
|
content: .continuous(ListItemSliderSelectorComponent.Continuous(
|
|
value: Double(screenState.paidMessageStars) / Double(screenState.maxPaidMessageStars),
|
|
minValue: 0,
|
|
lowerBoundTitle: "0",
|
|
upperBoundTitle: "\(presentationStringsFormattedNumber(Int32(clamping: screenState.maxPaidMessageStars), environment.dateTimeFormat.groupingSeparator))",
|
|
title: "\(screenState.paidMessageStars) Stars",
|
|
valueUpdated: { [weak self] value in
|
|
guard let self, let component = self.component else {
|
|
return
|
|
}
|
|
component.stateContext.paidMessageStars = Int64(Double(screenState.maxPaidMessageStars) * value)
|
|
self.state?.updated(transition: .immediate)
|
|
}
|
|
)),
|
|
preferNative: true
|
|
)
|
|
))]
|
|
|
|
let paidMessageSectionHeader = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(
|
|
string: "PRICE PER COMMENT",
|
|
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
|
|
textColor: theme.list.freeTextColor
|
|
)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let paidMessageSectionFooterComponent = AnyComponent(MultilineTextComponent(
|
|
text: .plain(NSAttributedString(string: "The price a viewer must pay to send a comment.", font: footerTextFont, textColor: footerTextColor)),
|
|
maximumNumberOfLines: 0
|
|
))
|
|
|
|
let paidMessageSectionSize = self.paidMessageSection.update(
|
|
transition: transition,
|
|
component: AnyComponent(ListSectionComponent(
|
|
theme: theme,
|
|
style: .glass,
|
|
header: paidMessageSectionHeader,
|
|
footer: paidMessageSectionFooterComponent,
|
|
items: paidMessageSectionItems
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
|
)
|
|
let paidMessageSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: paidMessageSectionSize)
|
|
if let paidMessageSectionView = self.paidMessageSection.view as? ListSectionComponent.View {
|
|
if paidMessageSectionView.superview == nil {
|
|
self.scrollView.addSubview(paidMessageSectionView)
|
|
self.paidMessageSection.parentState = state
|
|
}
|
|
transition.setFrame(view: paidMessageSectionView, frame: paidMessageSectionFrame)
|
|
}
|
|
contentHeight += paidMessageSectionSize.height
|
|
|
|
let edgeEffectHeight: CGFloat = 80.0
|
|
let edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: edgeEffectHeight))
|
|
transition.setFrame(view: self.topEdgeEffectView, frame: edgeEffectFrame)
|
|
self.topEdgeEffectView.update(content: theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: edgeEffectFrame, edge: .top, edgeSize: edgeEffectFrame.height, transition: transition)
|
|
|
|
let bottomEdgeEffectHeight: CGFloat = 96.0
|
|
let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomEdgeEffectHeight), size: CGSize(width: availableSize.width, height: bottomEdgeEffectHeight))
|
|
transition.setFrame(view: self.bottomEdgeEffectView, frame: bottomEdgeEffectFrame)
|
|
self.bottomEdgeEffectView.update(content: theme.list.blocksBackgroundColor, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: edgeEffectFrame.height, transition: transition)
|
|
|
|
let title: String = screenState.isEdit ? "Live Stream" : "Live Settings"
|
|
let titleSize = self.title.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: title,
|
|
font: Font.semibold(17.0),
|
|
textColor: environment.theme.rootController.navigationBar.primaryTextColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: 200.0, height: 40.0)
|
|
)
|
|
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: floorToScreenPixels((environment.navigationHeight - titleSize.height) / 2.0) + 3.0), size: titleSize)
|
|
if let titleView = self.title.view {
|
|
if titleView.superview == nil {
|
|
self.addSubview(titleView)
|
|
}
|
|
transition.setFrame(view: titleView, frame: titleFrame)
|
|
}
|
|
|
|
let barButtonSize = CGSize(width: 40.0, height: 40.0)
|
|
let cancelButtonSize = self.cancelButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(GlassBarButtonComponent(
|
|
size: barButtonSize,
|
|
backgroundColor: environment.theme.rootController.navigationBar.glassBarButtonBackgroundColor,
|
|
isDark: environment.theme.overallDarkAppearance,
|
|
state: .generic,
|
|
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Close",
|
|
tintColor: environment.theme.rootController.navigationBar.glassBarButtonForegroundColor
|
|
)
|
|
)),
|
|
action: { [weak self] _ in
|
|
guard let self, let controller = self.environment?.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
controller.dismiss()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: barButtonSize
|
|
)
|
|
let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: 16.0), size: cancelButtonSize)
|
|
if let cancelButtonView = self.cancelButton.view {
|
|
if cancelButtonView.superview == nil {
|
|
self.addSubview(cancelButtonView)
|
|
}
|
|
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
|
|
}
|
|
|
|
if screenState.isEdit {
|
|
let doneButtonSize = self.doneButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(GlassBarButtonComponent(
|
|
size: barButtonSize,
|
|
backgroundColor: environment.theme.list.itemCheckColors.fillColor,
|
|
isDark: environment.theme.overallDarkAppearance,
|
|
state: .tintedGlass,
|
|
isEnabled: true,
|
|
component: AnyComponentWithIdentity(id: "done", component: AnyComponent(
|
|
BundleIconComponent(
|
|
name: "Navigation/Done",
|
|
tintColor: environment.theme.list.itemCheckColors.foregroundColor
|
|
)
|
|
)),
|
|
action: { [weak self] _ in
|
|
guard let self, let controller = self.environment?.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
controller.dismiss()
|
|
}
|
|
)),
|
|
environment: {},
|
|
containerSize: barButtonSize
|
|
)
|
|
let doneButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - environment.safeInsets.right - 16.0 - doneButtonSize.width, y: 16.0), size: doneButtonSize)
|
|
if let doneButtonView = self.doneButton.view {
|
|
if doneButtonView.superview == nil {
|
|
self.addSubview(doneButtonView)
|
|
}
|
|
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
|
|
}
|
|
|
|
contentHeight += environment.safeInsets.bottom
|
|
} else {
|
|
let actionButtonSize = self.actionButton.update(
|
|
transition: transition,
|
|
component: AnyComponent(
|
|
ButtonComponent(
|
|
background: ButtonComponent.Background(
|
|
style: .glass,
|
|
color: theme.list.itemCheckColors.fillColor,
|
|
foreground: theme.list.itemCheckColors.foregroundColor,
|
|
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
|
),
|
|
content: AnyComponentWithIdentity(
|
|
id: "label",
|
|
component: AnyComponent(ButtonTextContentComponent(
|
|
text: "Save Settings",
|
|
badge: 0,
|
|
textColor: theme.list.itemCheckColors.foregroundColor,
|
|
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
|
badgeForeground: theme.list.itemCheckColors.fillColor,
|
|
combinedAlignment: true
|
|
))
|
|
),
|
|
isEnabled: true,
|
|
displaysProgress: false,
|
|
action: { [weak self] in
|
|
guard let self, let controller = self.environment?.controller() as? LiveStreamSettingsScreen else {
|
|
return
|
|
}
|
|
self.complete(rtmp: false)
|
|
controller.dismiss()
|
|
}
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: CGSize(width: availableSize.width - 30.0 * 2.0, height: 52.0)
|
|
)
|
|
|
|
let bottomPanelHeight = 10.0 + environment.safeInsets.bottom + actionButtonSize.height
|
|
let actionButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - actionButtonSize.width) / 2.0), y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
|
if let actionButtonView = self.actionButton.view {
|
|
if actionButtonView.superview == nil {
|
|
self.addSubview(actionButtonView)
|
|
}
|
|
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
|
}
|
|
|
|
contentHeight += bottomPanelHeight + sectionSpacing
|
|
}
|
|
|
|
self.ignoreScrolling = true
|
|
let previousBounds = self.scrollView.bounds
|
|
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
|
|
if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) {
|
|
self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize)
|
|
}
|
|
if self.scrollView.contentSize != contentSize {
|
|
self.scrollView.contentSize = contentSize
|
|
}
|
|
let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0)
|
|
if self.scrollView.verticalScrollIndicatorInsets != scrollInsets {
|
|
self.scrollView.verticalScrollIndicatorInsets = scrollInsets
|
|
}
|
|
|
|
if !previousBounds.isEmpty, !transition.animation.isImmediate {
|
|
let bounds = self.scrollView.bounds
|
|
if bounds.maxY != previousBounds.maxY {
|
|
let offsetY = previousBounds.maxY - bounds.maxY
|
|
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
|
|
}
|
|
}
|
|
self.ignoreScrolling = false
|
|
|
|
self.updateScrolling(transition: transition)
|
|
|
|
return availableSize
|
|
}
|
|
}
|
|
|
|
func makeView() -> View {
|
|
return View()
|
|
}
|
|
|
|
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 LiveStreamSettingsScreen: ViewControllerComponentContainer {
|
|
public enum Mode {
|
|
case create(sendAsPeerId: EnginePeer.Id?, isCustomTarget: Bool, privacy: EngineStoryPrivacy, allowComments: Bool, isForwardingDisabled: Bool, pin: Bool, paidMessageStars: Int64)
|
|
case edit(PresentationGroupCall)
|
|
}
|
|
|
|
public struct Result {
|
|
public var sendAsPeerId: EnginePeer.Id?
|
|
public var privacy: EngineStoryPrivacy
|
|
public var allowComments: Bool
|
|
public var isForwardingDisabled: Bool
|
|
public var pin: Bool
|
|
public var paidMessageStars: Int64
|
|
public var startRtmpStream: Bool
|
|
}
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
stateContext: StateContext,
|
|
editCategory: @escaping (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void,
|
|
editBlockedPeers: @escaping (EngineStoryPrivacy, Bool, Bool, Bool, Int64) -> Void,
|
|
completion: @escaping (Result) -> Void
|
|
) {
|
|
super.init(context: context, component: LiveStreamSettingsScreenComponent(
|
|
context: context,
|
|
stateContext: stateContext,
|
|
editCategory: editCategory,
|
|
editBlockedPeers: editBlockedPeers,
|
|
completion: completion
|
|
), navigationBarAppearance: .transparent, theme: .dark)
|
|
|
|
self.navigationPresentation = .modal
|
|
self._hasGlassStyle = true
|
|
|
|
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
guard let self, let componentView = self.node.hostView.componentView as? LiveStreamSettingsScreenComponent.View else {
|
|
return
|
|
}
|
|
componentView.scrollToTop()
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
fileprivate func dismissAllTooltips() {
|
|
self.window?.forEachController { controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss()
|
|
}
|
|
}
|
|
self.forEachController { controller in
|
|
if let controller = controller as? TooltipScreen {
|
|
controller.dismiss()
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
final class State {
|
|
var isEdit: Bool
|
|
var maxPaidMessageStars: Int64
|
|
var sendAsPeerId: EnginePeer.Id?
|
|
var isCustomTarget: Bool
|
|
var privacy: EngineStoryPrivacy
|
|
var allowComments: Bool
|
|
var isForwardingDisabled: Bool
|
|
var pin: Bool
|
|
var paidMessageStars: Int64
|
|
var sendAsPeers: [EnginePeer]
|
|
var peersMap: [EnginePeer.Id: EnginePeer]
|
|
var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]]
|
|
var participants: [EnginePeer.Id: Int]
|
|
var closeFriendsPeers: [EnginePeer]
|
|
var grayListPeers: [EnginePeer]
|
|
|
|
init(
|
|
isEdit: Bool,
|
|
maxPaidMessageStars: Int64,
|
|
sendAsPeerId: EnginePeer.Id?,
|
|
isCustomTarget: Bool,
|
|
privacy: EngineStoryPrivacy,
|
|
allowComments: Bool,
|
|
isForwardingDisabled: Bool,
|
|
pin: Bool,
|
|
paidMessageStars: Int64,
|
|
sendAsPeers: [EnginePeer],
|
|
peersMap: [EnginePeer.Id: EnginePeer],
|
|
savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]],
|
|
participants: [EnginePeer.Id: Int],
|
|
closeFriendsPeers: [EnginePeer],
|
|
grayListPeers: [EnginePeer]
|
|
) {
|
|
self.isEdit = isEdit
|
|
self.maxPaidMessageStars = maxPaidMessageStars
|
|
self.sendAsPeerId = sendAsPeerId
|
|
self.isCustomTarget = isCustomTarget
|
|
self.privacy = privacy
|
|
self.allowComments = allowComments
|
|
self.isForwardingDisabled = isForwardingDisabled
|
|
self.pin = pin
|
|
self.paidMessageStars = paidMessageStars
|
|
self.sendAsPeers = sendAsPeers
|
|
self.peersMap = peersMap
|
|
self.savedSelectedPeers = savedSelectedPeers
|
|
self.participants = participants
|
|
self.closeFriendsPeers = closeFriendsPeers
|
|
self.grayListPeers = grayListPeers
|
|
}
|
|
}
|
|
|
|
public final class StateContext {
|
|
let blockedPeersContext: BlockedPeersContext?
|
|
|
|
var stateValue: State?
|
|
private let statePromise = Promise<State>()
|
|
var state: Signal<State, NoError> {
|
|
return self.statePromise.get()
|
|
}
|
|
private var stateDisposable: Disposable?
|
|
|
|
private let readyPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
public var ready: Signal<Bool, NoError> {
|
|
return self.readyPromise.get()
|
|
}
|
|
|
|
var sendAsPeerId: EnginePeer.Id? {
|
|
get {
|
|
return self.stateValue?.sendAsPeerId
|
|
}
|
|
set(value) {
|
|
self.stateValue?.sendAsPeerId = value
|
|
}
|
|
}
|
|
var privacy: EngineStoryPrivacy {
|
|
get {
|
|
return self.stateValue?.privacy ?? EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
|
}
|
|
set(value) {
|
|
self.stateValue?.privacy = value
|
|
}
|
|
}
|
|
var allowComments: Bool {
|
|
get {
|
|
return self.stateValue?.allowComments ?? true
|
|
}
|
|
set(value) {
|
|
self.stateValue?.allowComments = value
|
|
}
|
|
}
|
|
var isForwardingDisabled: Bool {
|
|
get {
|
|
return self.stateValue?.isForwardingDisabled ?? false
|
|
}
|
|
set(value) {
|
|
self.stateValue?.isForwardingDisabled = value
|
|
}
|
|
}
|
|
var pin: Bool {
|
|
get {
|
|
return self.stateValue?.pin ?? true
|
|
}
|
|
set(value) {
|
|
self.stateValue?.pin = value
|
|
}
|
|
}
|
|
var paidMessageStars: Int64 {
|
|
get {
|
|
return self.stateValue?.paidMessageStars ?? 0
|
|
}
|
|
set(value) {
|
|
self.stateValue?.paidMessageStars = value
|
|
}
|
|
}
|
|
|
|
public init(
|
|
context: AccountContext,
|
|
mode: LiveStreamSettingsScreen.Mode,
|
|
initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:],
|
|
closeFriends: Signal<[EnginePeer], NoError>,
|
|
adminedChannels: Signal<[EnginePeer], NoError>,
|
|
blockedPeersContext: BlockedPeersContext?
|
|
) {
|
|
self.blockedPeersContext = blockedPeersContext
|
|
|
|
let grayListPeers: Signal<[EnginePeer], NoError>
|
|
if let blockedPeersContext {
|
|
grayListPeers = blockedPeersContext.state
|
|
|> map { state -> [EnginePeer] in
|
|
return state.peers.compactMap { $0.peer.flatMap(EnginePeer.init) }
|
|
}
|
|
} else {
|
|
grayListPeers = .single([])
|
|
}
|
|
|
|
let savedEveryoneExceptionPeers = peersListStoredState(engine: context.engine, base: .everyone)
|
|
let savedContactsExceptionPeers = peersListStoredState(engine: context.engine, base: .contacts)
|
|
let savedSelectedPeers = peersListStoredState(engine: context.engine, base: .nobody)
|
|
|
|
let savedPeers = combineLatest(
|
|
savedEveryoneExceptionPeers,
|
|
savedContactsExceptionPeers,
|
|
savedSelectedPeers
|
|
) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in
|
|
var everyone = everyone
|
|
if let initialPeerIds = initialSelectedPeers[.everyone] {
|
|
everyone = initialPeerIds
|
|
}
|
|
var everyonePeerSignals: [Signal<EnginePeer?, NoError>] = []
|
|
if everyone.count < 3 {
|
|
for peerId in everyone {
|
|
everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
|
}
|
|
}
|
|
|
|
var contacts = contacts
|
|
if let initialPeerIds = initialSelectedPeers[.contacts] {
|
|
contacts = initialPeerIds
|
|
}
|
|
var contactsPeerSignals: [Signal<EnginePeer?, NoError>] = []
|
|
if contacts.count < 3 {
|
|
for peerId in contacts {
|
|
contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
|
}
|
|
}
|
|
|
|
var selected = selected
|
|
if let initialPeerIds = initialSelectedPeers[.nobody] {
|
|
selected = initialPeerIds
|
|
}
|
|
var selectedPeerSignals: [Signal<EnginePeer?, NoError>] = []
|
|
if selected.count < 3 {
|
|
for peerId in selected {
|
|
selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
|
}
|
|
}
|
|
return combineLatest(
|
|
combineLatest(everyonePeerSignals),
|
|
combineLatest(contactsPeerSignals),
|
|
combineLatest(selectedPeerSignals)
|
|
) |> map { everyonePeers, contactsPeers, selectedPeers -> ([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]) in
|
|
var peersMap: [EnginePeer.Id: EnginePeer] = [:]
|
|
for peer in everyonePeers {
|
|
if let peer {
|
|
peersMap[peer.id] = peer
|
|
}
|
|
}
|
|
for peer in contactsPeers {
|
|
if let peer {
|
|
peersMap[peer.id] = peer
|
|
}
|
|
}
|
|
for peer in selectedPeers {
|
|
if let peer {
|
|
peersMap[peer.id] = peer
|
|
}
|
|
}
|
|
return (peersMap, everyone, contacts, selected)
|
|
}
|
|
}
|
|
|
|
let adminedChannelsWithParticipants = adminedChannels
|
|
|> mapToSignal { peers -> Signal<([EnginePeer], [EnginePeer.Id: Optional<Int>]), NoError> in
|
|
return context.engine.data.subscribe(
|
|
EngineDataMap(peers.map(\.id).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
|
)
|
|
|> map { participantCountMap -> ([EnginePeer], [EnginePeer.Id: Optional<Int>]) in
|
|
return (peers, participantCountMap)
|
|
}
|
|
}
|
|
|
|
self.stateDisposable = combineLatest(
|
|
queue: Queue.mainQueue(),
|
|
context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
|
|
adminedChannelsWithParticipants,
|
|
savedPeers,
|
|
closeFriends,
|
|
grayListPeers
|
|
).start(next: { [weak self] accountPeer, adminedChannelsWithParticipants, savedPeers, closeFriends, grayListPeers in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
let (adminedChannels, participantCounts) = adminedChannelsWithParticipants
|
|
var participants: [EnginePeer.Id: Int] = [:]
|
|
for (key, value) in participantCounts {
|
|
if let value {
|
|
participants[key] = value
|
|
}
|
|
}
|
|
|
|
var sendAsPeers: [EnginePeer] = []
|
|
if let accountPeer {
|
|
sendAsPeers.append(accountPeer)
|
|
}
|
|
for channel in adminedChannels {
|
|
if case let .channel(channel) = channel, channel.hasPermission(.postStories) {
|
|
if !sendAsPeers.contains(where: { $0.id == channel.id }) {
|
|
sendAsPeers.append(contentsOf: adminedChannels)
|
|
}
|
|
}
|
|
}
|
|
|
|
let (peersMap, everyonePeers, contactsPeers, selectedPeers) = savedPeers
|
|
var savedSelectedPeers: [Stories.Item.Privacy.Base: [EnginePeer.Id]] = [:]
|
|
savedSelectedPeers[.everyone] = everyonePeers
|
|
savedSelectedPeers[.contacts] = contactsPeers
|
|
savedSelectedPeers[.nobody] = selectedPeers
|
|
|
|
let isEdit: Bool
|
|
let maxPaidMessageStars: Int64 = 10000
|
|
let sendAsPeerId: EnginePeer.Id?
|
|
let isCustomTarget: Bool
|
|
let privacy: EngineStoryPrivacy
|
|
let allowComments: Bool
|
|
let isForwardingDisabled: Bool
|
|
let pin: Bool
|
|
let paidMessageStars: Int64
|
|
|
|
if let current = self.stateValue {
|
|
isEdit = current.isEdit
|
|
sendAsPeerId = current.sendAsPeerId
|
|
isCustomTarget = current.isCustomTarget
|
|
privacy = current.privacy
|
|
allowComments = current.allowComments
|
|
isForwardingDisabled = current.isForwardingDisabled
|
|
pin = current.pin
|
|
paidMessageStars = current.paidMessageStars
|
|
} else {
|
|
switch mode {
|
|
case let .create(sendAsPeerIdValue, isCustomTargetValue, privacyValue, allowCommentsValue, isForwardingDisabledValue, pinValue, paidMessageStarsValue):
|
|
isEdit = false
|
|
sendAsPeerId = sendAsPeerIdValue
|
|
isCustomTarget = isCustomTargetValue
|
|
privacy = privacyValue
|
|
allowComments = allowCommentsValue
|
|
isForwardingDisabled = isForwardingDisabledValue
|
|
pin = pinValue
|
|
paidMessageStars = paidMessageStarsValue
|
|
case let .edit(call):
|
|
let _ = call
|
|
isEdit = true
|
|
sendAsPeerId = nil
|
|
isCustomTarget = false
|
|
privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
|
allowComments = true
|
|
isForwardingDisabled = false
|
|
pin = true
|
|
paidMessageStars = 0
|
|
}
|
|
}
|
|
|
|
let state = State(
|
|
isEdit: isEdit,
|
|
maxPaidMessageStars: maxPaidMessageStars,
|
|
sendAsPeerId: sendAsPeerId,
|
|
isCustomTarget: isCustomTarget,
|
|
privacy: privacy,
|
|
allowComments: allowComments,
|
|
isForwardingDisabled: isForwardingDisabled,
|
|
pin: pin,
|
|
paidMessageStars: paidMessageStars,
|
|
sendAsPeers: sendAsPeers,
|
|
peersMap: peersMap,
|
|
savedSelectedPeers: savedSelectedPeers,
|
|
participants: participants,
|
|
closeFriendsPeers: closeFriends,
|
|
grayListPeers: grayListPeers
|
|
)
|
|
|
|
self.stateValue = state
|
|
self.statePromise.set(.single(state))
|
|
|
|
self.readyPromise.set(true)
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.stateDisposable?.dispose()
|
|
}
|
|
}
|
|
}
|
|
|