import Foundation import UIKit import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import PresentationDataUtils import AccountContext import ComponentFlow import ViewControllerComponent import MultilineTextComponent import ButtonComponent import BundleIconComponent import AnimatedStickerComponent import ActivityIndicatorComponent import GlassBarButtonComponent import ListSectionComponent import ListActionItemComponent import PlainButtonComponent private final class CreateExternalMediaStreamScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let peerId: EnginePeer.Id let mode: CreateExternalMediaStreamScreen.Mode let credentialsPromise: Promise? init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) { self.context = context self.peerId = peerId self.mode = mode self.credentialsPromise = credentialsPromise } static func ==(lhs: CreateExternalMediaStreamScreenComponent, rhs: CreateExternalMediaStreamScreenComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.peerId != rhs.peerId { return false } if lhs.mode != rhs.mode { return false } if lhs.credentialsPromise !== rhs.credentialsPromise { return false } return true } final class State: ComponentState { let context: AccountContext let peerId: EnginePeer.Id let mode: CreateExternalMediaStreamScreen.Mode private(set) var credentials: GroupCallStreamCredentials? var isDelayingLoadingIndication: Bool = true private let credentialsDisposable = MetaDisposable() private let activeActionDisposable = MetaDisposable() init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) { self.context = context self.peerId = peerId self.mode = mode super.init() self.getCredentials(credentialsPromise: credentialsPromise) } deinit { self.credentialsDisposable.dispose() self.activeActionDisposable.dispose() } func getCredentials(credentialsPromise: Promise? = nil, revoke: Bool = false) { let credentialsSignal: Signal if let credentialsPromise = credentialsPromise { credentialsSignal = credentialsPromise.get() } else { var isLiveStream = false if case let .create(isLiveStreamValue) = self.mode { isLiveStream = isLiveStreamValue } credentialsSignal = self.context.engine.calls.getGroupCallStreamCredentials(peerId: self.peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revoke) |> `catch` { _ -> Signal in return .never() } } self.credentialsDisposable.set((credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.credentials = result strongSelf.updated(transition: .immediate) })) } func copyCredentials(_ key: KeyPath) { guard let credentials = self.credentials else { return } UIPasteboard.general.string = credentials[keyPath: key] } func createAndJoinGroupCall(baseController: ViewController, completion: @escaping () -> Void) { guard let _ = self.context.sharedContext.callManager else { return } let startCall: (Bool) -> Void = { [weak self, weak baseController] endCurrentIfAny in guard let strongSelf = self, let baseController = baseController else { return } strongSelf.isDelayingLoadingIndication = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak strongSelf] in guard let strongSelf else { return } strongSelf.isDelayingLoadingIndication = false strongSelf.updated(transition: .easeInOut(duration: 0.3)) } var cancelImpl: (() -> Void)? let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let progressSignal = Signal { [weak baseController] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) baseController?.present(controller, in: .window(.root)) return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() } } } |> runOn(Queue.mainQueue()) |> delay(0.15, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: strongSelf.peerId, title: nil, scheduleDate: nil, isExternalStream: true) |> afterDisposed { Queue.mainQueue().async { progressDisposable.dispose() } } cancelImpl = { self?.activeActionDisposable.set(nil) } strongSelf.activeActionDisposable.set((createSignal |> deliverOnMainQueue).start(next: { info in guard let strongSelf = self else { return } strongSelf.context.joinGroupCall(peerId: strongSelf.peerId, invite: nil, requestJoinAsPeerId: { result in result(nil) }, activeCall: EngineGroupCallDescription(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: nil, subscribedToScheduled: false, isStream: info.isStream)) completion() }, error: { [weak baseController] error in guard let strongSelf = self else { return } let text: String text = presentationData.strings.Login_UnknownError baseController?.present(textAlertController(context: strongSelf.context, updatedPresentationData: nil, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) })) } startCall(true) } } func makeState() -> State { return State(context: self.context, peerId: self.peerId, mode: self.mode, credentialsPromise: self.credentialsPromise) } static var body: Body { let background = Child(Rectangle.self) let closeButton = Child(GlassBarButtonComponent.self) let title = Child(Text.self) let animation = Child(AnimatedStickerComponent.self) let text = Child(MultilineTextComponent.self) let bottomText = Child(MultilineTextComponent.self) let button = Child(ButtonComponent.self) let activityIndicator = Child(ActivityIndicatorComponent.self) let credentialsSection = Child(ListSectionComponent.self) // let credentialsBackground = Child(RoundedRectangle.self) // let credentialsStripe = Child(Rectangle.self) // let credentialsURLTitle = Child(MultilineTextComponent.self) // let credentialsURLText = Child(MultilineTextComponent.self) // // let credentialsKeyTitle = Child(MultilineTextComponent.self) // let credentialsKeyText = Child(MultilineTextComponent.self) // // let credentialsCopyURLButton = Child(Button.self) // let credentialsCopyKeyButton = Child(Button.self) return { context in let topInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0 let buttonSideInset: CGFloat = 36.0 let component = context.component let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state let theme = environment.theme.withModalBlocksBackground() let mode = context.component.mode let controller = environment.controller let bottomInset: CGFloat if environment.safeInsets.bottom.isZero { bottomInset = 16.0 } else { bottomInset = 34.0 } let background = background.update( component: Rectangle(color: theme.list.blocksBackgroundColor), availableSize: context.availableSize, transition: context.transition ) context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) if case .create = context.component.mode { let closeButton = closeButton.update( component: GlassBarButtonComponent( size: CGSize(width: 40.0, height: 40.0), backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, isDark: theme.overallDarkAppearance, state: .tintedGlass, component: AnyComponentWithIdentity(id: "close", component: AnyComponent( BundleIconComponent( name: "Navigation/Close", tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor ) )), action: { _ in guard let controller = controller() else { return } controller.dismiss() } ), availableSize: CGSize(width: 40.0, height: 40.0), transition: context.transition ) context.add(closeButton .position(CGPoint(x: 16.0 + closeButton.size.width * 0.5, y: 16.0 + closeButton.size.height * 0.5)) ) } let titleString: String switch context.component.mode { case .create: titleString = environment.strings.CreateExternalStream_Title case .view: titleString = environment.strings.CreateExternalStream_StreamKeyTitle } let title = title.update( component: Text( text: titleString, font: Font.semibold(17.0), color: theme.list.itemPrimaryTextColor ), availableSize: context.availableSize, transition: context.transition ) context.add(title .position(CGPoint(x: context.availableSize.width * 0.5, y: 26.0 + title.size.height * 0.5)) ) let animation = animation.update( component: AnimatedStickerComponent( account: state.context.account, animation: AnimatedStickerComponent.Animation( source: .bundle(name: "CreateStream"), loop: true ), size: CGSize(width: 138.0, height: 138.0) ), availableSize: CGSize(width: 138.0, height: 138.0), transition: context.transition ) let text = text.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) let bottomText = Condition(context.component.mode.isCreate) { bottomText.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(13.0), textColor: theme.list.itemSecondaryTextColor, paragraphAlignment: .center)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) } let buttonAttributedString = NSMutableAttributedString(string: mode.isCreate ? environment.strings.CreateExternalStream_StartStreaming : environment.strings.Common_Close, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( style: .glass, color: mode.isCreate ? UIColor(rgb: 0xfa325a) : theme.list.itemCheckColors.fillColor, foreground: mode.isCreate ? .white : theme.list.itemCheckColors.foregroundColor, pressedColor: mode.isCreate ? UIColor(rgb: 0xfa325a) : theme.list.itemCheckColors.fillColor, isShimmering: mode.isCreate ), content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString))) ), isEnabled: true, displaysProgress: false, action: { [weak state] in guard let state = state, let controller = controller() as? CreateExternalMediaStreamScreen else { return } switch mode { case let .create(livestream): if livestream { controller.completion?() } else { state.createAndJoinGroupCall(baseController: controller, completion: { [weak controller] in controller?.completion?() controller?.dismiss() }) } case .view: controller.dismiss() } } ), availableSize: CGSize(width: context.availableSize.width - buttonSideInset * 2.0, height: 52.0), transition: context.transition ) let credentialsItemHeight: CGFloat = 64.0 let credentialsAreaSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: credentialsItemHeight * 2.0) let animationFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - animation.size.width) / 2.0), y: environment.navigationHeight + topInset), size: animation.size) context.add(animation .position(CGPoint(x: animationFrame.midX, y: animationFrame.midY)) ) let textFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - text.size.width) / 2.0), y: animationFrame.maxY + 18.0), size: text.size) context.add(text .position(CGPoint(x: textFrame.midX, y: textFrame.midY)) ) if let credentials = context.state.credentials { var credentialsSectionItems: [AnyComponentWithIdentity] = [] credentialsSectionItems.append( AnyComponentWithIdentity(id: "url", component: AnyComponent( ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 ))) ], alignment: .left, spacing: 5.0)), contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), accessory: .custom(ListActionItemComponent.CustomAccessory( component: AnyComponentWithIdentity( id: "copy", component: AnyComponent( PlainButtonComponent( content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), action: { [weak state] in guard let state = state else { return } state.copyCredentials(\.url) }, animateScale: false ) ) ), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), isInteractive: true )), action: nil ) )) ) credentialsSectionItems.append( AnyComponentWithIdentity(id: "key", component: AnyComponent( ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), horizontalAlignment: .left, truncationType: .middle, maximumNumberOfLines: 1 ))) ], alignment: .left, spacing: 5.0)), contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), accessory: .custom(ListActionItemComponent.CustomAccessory( component: AnyComponentWithIdentity( id: "copy", component: AnyComponent( PlainButtonComponent( content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), action: { [weak state] in guard let state = state else { return } state.copyCredentials(\.streamKey) }, animateScale: false ) ) ), insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), isInteractive: true )), action: nil ) )) ) credentialsSectionItems.append( AnyComponentWithIdentity(id: "revoke", component: AnyComponent( ListActionItemComponent( theme: theme, style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_RevokeStreamKey, font: Font.regular(17.0), textColor: theme.list.itemDestructiveColor)), horizontalAlignment: .center, truncationType: .middle, maximumNumberOfLines: 1 )), titleAlignment: .center, action: { [weak state] _ in guard let state = state else { return } let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in state?.getCredentials(revoke: true) })]) environment.controller()?.present(alertController, in: .window(.root)) } ) )) ) let credentialsSection = credentialsSection.update( component: ListSectionComponent( theme: theme, style: .glass, header: nil, footer: nil, items: credentialsSectionItems ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) context.add(credentialsSection .position(CGPoint(x: context.availableSize.width / 2.0, y: textFrame.maxY + 30.0 + credentialsSection.size.height / 2.0))) } else if !context.state.isDelayingLoadingIndication { let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize) let activityIndicator = activityIndicator.update( component: ActivityIndicatorComponent(color: theme.list.controlSecondaryColor), availableSize: CGSize(width: 100.0, height: 100.0), transition: context.transition ) context.add(activityIndicator .position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY)) ) } let buttonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: context.availableSize.height - bottomInset - button.size.height), size: button.size) if let bottomText { context.add(bottomText .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - 12.0 - bottomText.size.height / 2.0)) ) } context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) return context.availableSize } } } public final class CreateExternalMediaStreamScreen: ViewControllerComponentContainer { public enum Mode: Equatable { case create(liveStream: Bool) case view var isCreate: Bool { if case .create = self { return true } else { return false } } } private let context: AccountContext private let peerId: EnginePeer.Id private let mode: Mode fileprivate let completion: (() -> Void)? public init( context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?, mode: Mode, completion: (() -> Void)? = nil ) { self.context = context self.peerId = peerId self.mode = mode self.completion = completion super.init(context: context, component: CreateExternalMediaStreamScreenComponent(context: context, peerId: peerId, mode: mode, credentialsPromise: credentialsPromise), navigationBarAppearance: .none, theme: .dark) self._hasGlassStyle = true self.navigationPresentation = .modal self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func cancelPressed() { self.dismiss() } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } }