Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-03-08 22:14:54 +04:00
commit 953624e278
49 changed files with 3185 additions and 239 deletions

View File

@ -7343,8 +7343,20 @@ Sorry for the inconvenience.";
"LiveStream.ViewerCount_1" = "1 viewer";
"LiveStream.ViewerCount_any" = "%@ viewers";
"LiveStream.NoSignalAdminText" = "Oops! Telegram doesn't see any stream\ncoming from your streaming app.\n\nPlease make sure you entered the right Server\nURL and Stream Key in your app.";
"LiveStream.NoSignalUserText" = "%@ is currently not broadcasting live\nstream data to Telegram.";
"Attachment.MyAlbums" = "My Albums";
"Attachment.MediaTypes" = "Media Types";
"Attachment.LocationAccessTitle" = "Access Your Location";
"Attachment.LocationAccessText" = "Share places or your live location.";
"ChannelInfo.CreateExternalStream" = "Stream With...";
"CreateExternalStream.Title" = "Stream With...";
"CreateExternalStream.Text" = "To stream video with a another app, enter\nthese Server URL and Stream Key in your\nsteaming app.";
"CreateExternalStream.ServerUrl" = "server URL";
"CreateExternalStream.StreamKey" = "stream key";
"CreateExternalStream.StartStreamingInfo" = "Once you start broadcasting in your streaming\napp, tap Start Streaming below.";
"CreateExternalStream.StartStreaming" = "Start Streaming";

View File

@ -1664,7 +1664,6 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
var items: [ActionSheetItem] = []
//TODO:localize
items.append(ActionSheetAnimationAndTextItem(title: strongSelf.presentationData.strings.DownloadList_ClearAlertTitle, text: strongSelf.presentationData.strings.DownloadList_ClearAlertText))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DownloadList_OptionManageDeviceStorage, color: .accent, action: { [weak actionSheet] in

View File

@ -0,0 +1,49 @@
import Foundation
import UIKit
public final class RoundedRectangle: Component {
public let color: UIColor
public let cornerRadius: CGFloat
public init(color: UIColor, cornerRadius: CGFloat) {
self.color = color
self.cornerRadius = cornerRadius
}
public static func ==(lhs: RoundedRectangle, rhs: RoundedRectangle) -> Bool {
if !lhs.color.isEqual(rhs.color) {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
return true
}
public final class View: UIImageView {
var component: RoundedRectangle?
func update(component: RoundedRectangle, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component != component {
let imageSize = CGSize(width: component.cornerRadius * 2.0, height: component.cornerRadius * 2.0)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(component.color.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: imageSize))
}
self.image = UIGraphicsGetImageFromCurrentImageContext()?.stretchableImage(withLeftCapWidth: Int(component.cornerRadius), topCapHeight: Int(component.cornerRadius))
UIGraphicsEndImageContext()
}
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -3,11 +3,18 @@ import UIKit
import ComponentFlow
public final class ActivityIndicatorComponent: Component {
public let color: UIColor
public init(
color: UIColor
) {
self.color = color
}
public static func ==(lhs: ActivityIndicatorComponent, rhs: ActivityIndicatorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
return true
}
@ -21,6 +28,10 @@ public final class ActivityIndicatorComponent: Component {
}
func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if component.color != self.color {
self.color = component.color
}
if !self.isAnimating {
self.startAnimating()
}

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "AnimatedStickerComponent",
module_name = "AnimatedStickerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,107 @@
import Foundation
import UIKit
import ComponentFlow
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import HierarchyTrackingLayer
public final class AnimatedStickerComponent: Component {
public struct Animation: Equatable {
public var name: String
public var loop: Bool
public var isAnimating: Bool
public init(name: String, loop: Bool, isAnimating: Bool = true) {
self.name = name
self.loop = loop
self.isAnimating = isAnimating
}
}
public let animation: Animation
public let size: CGSize
public init(animation: Animation, size: CGSize) {
self.animation = animation
self.size = size
}
public static func ==(lhs: AnimatedStickerComponent, rhs: AnimatedStickerComponent) -> Bool {
if lhs.animation != rhs.animation {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
public final class View: UIView {
private var component: AnimatedStickerComponent?
private var animationNode: AnimatedStickerNode?
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var isInHierarchy: Bool = false
override init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = true
strongSelf.animationNode?.visibility = true
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isInHierarchy = false
strongSelf.animationNode?.visibility = false
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component?.animation != component.animation {
self.component = component
self.animationNode?.view.removeFromSuperview()
let animationNode = AnimatedStickerNode()
animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: component.animation.name), width: Int(component.size.width * 2.0), height: Int(component.size.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
animationNode.visibility = self.isInHierarchy
self.animationNode = animationNode
self.addSubnode(animationNode)
}
let animationSize = component.size
let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height))
if let animationNode = self.animationNode {
animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize)
animationNode.updateLayout(size: animationSize)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "BundleIconComponent",
module_name = "BundleIconComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/AppBundle:AppBundle",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,60 @@
import Foundation
import UIKit
import ComponentFlow
import AppBundle
import Display
public final class BundleIconComponent: Component {
public let name: String
public let tintColor: UIColor?
public init(name: String, tintColor: UIColor?) {
self.name = name
self.tintColor = tintColor
}
public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return false
}
public final class View: UIImageView {
private var component: BundleIconComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BundleIconComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor {
if let tintColor = component.tintColor {
self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
} else {
self.image = UIImage(bundleImageName: component.name)
}
}
self.component = component
let imageSize = self.image?.size ?? CGSize()
return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "HierarchyTrackingLayer",
module_name = "HierarchyTrackingLayer",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,22 @@
import UIKit
private final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
private let nullAction = NullActionClass()
open class HierarchyTrackingLayer: CALayer {
public var didEnterHierarchy: (() -> Void)?
public var didExitHierarchy: (() -> Void)?
override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.didExitHierarchy?()
}
return nullAction
}
}

View File

@ -13,6 +13,7 @@ swift_library(
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/lottie-ios:Lottie",
"//submodules/AppBundle:AppBundle",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View File

@ -2,27 +2,7 @@ import Foundation
import ComponentFlow
import Lottie
import AppBundle
private final class NullActionClass: NSObject, CAAction {
@objc public func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) {
}
}
private let nullAction = NullActionClass()
private final class HierarchyTrackingLayer: CALayer {
var didEnterHierarchy: (() -> Void)?
var didExitHierarchy: (() -> Void)?
override func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn {
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut {
self.didExitHierarchy?()
}
return nullAction
}
}
import HierarchyTrackingLayer
public final class LottieAnimationComponent: Component {
public struct Animation: Equatable {

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MultilineTextComponent",
module_name = "MultilineTextComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,115 @@
import Foundation
import UIKit
import ComponentFlow
import Display
public final class MultilineTextComponent: Component {
public let text: NSAttributedString
public let horizontalAlignment: NSTextAlignment
public let verticalAlignment: TextVerticalAlignment
public var truncationType: CTLineTruncationType
public var maximumNumberOfLines: Int
public var lineSpacing: CGFloat
public var insets: UIEdgeInsets
public var textShadowColor: UIColor?
public var textStroke: (UIColor, CGFloat)?
public init(
text: NSAttributedString,
horizontalAlignment: NSTextAlignment = .natural,
verticalAlignment: TextVerticalAlignment = .top,
truncationType: CTLineTruncationType = .end,
maximumNumberOfLines: Int = 1,
lineSpacing: CGFloat = 0.0,
insets: UIEdgeInsets = UIEdgeInsets(),
textShadowColor: UIColor? = nil,
textStroke: (UIColor, CGFloat)? = nil
) {
self.text = text
self.horizontalAlignment = horizontalAlignment
self.verticalAlignment = verticalAlignment
self.truncationType = truncationType
self.maximumNumberOfLines = maximumNumberOfLines
self.lineSpacing = lineSpacing
self.insets = insets
self.textShadowColor = textShadowColor
self.textStroke = textStroke
}
public static func ==(lhs: MultilineTextComponent, rhs: MultilineTextComponent) -> Bool {
if !lhs.text.isEqual(to: rhs.text) {
return false
}
if lhs.horizontalAlignment != rhs.horizontalAlignment {
return false
}
if lhs.verticalAlignment != rhs.verticalAlignment {
return false
}
if lhs.truncationType != rhs.truncationType {
return false
}
if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines {
return false
}
if lhs.lineSpacing != rhs.lineSpacing {
return false
}
if lhs.insets != rhs.insets {
return false
}
if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor {
if !lhsTextShadowColor.isEqual(rhsTextShadowColor) {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke {
if !lhsTextStroke.0.isEqual(rhsTextStroke.0) {
return false
}
if lhsTextStroke.1 != rhsTextStroke.1 {
return false
}
} else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) {
return false
}
return true
}
public final class View: TextView {
public func update(component: MultilineTextComponent, availableSize: CGSize) -> CGSize {
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(
attributedString: component.text,
backgroundColor: nil,
maximumNumberOfLines: component.maximumNumberOfLines,
truncationType: component.truncationType,
constrainedSize: availableSize,
alignment: component.horizontalAlignment,
verticalAlignment: component.verticalAlignment,
lineSpacing: component.lineSpacing,
cutout: nil,
insets: component.insets,
textShadowColor: component.textShadowColor,
textStroke: component.textStroke,
displaySpoilers: false
))
let _ = apply()
return layout.size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize)
}
}

View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SolidRoundedButtonComponent",
module_name = "SolidRoundedButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,114 @@
import Foundation
import UIKit
import ComponentFlow
import Display
import SolidRoundedButtonNode
public final class SolidRoundedButtonComponent: Component {
public typealias Theme = SolidRoundedButtonTheme
public let title: String?
public let icon: UIImage?
public let theme: SolidRoundedButtonTheme
public let font: SolidRoundedButtonFont
public let fontSize: CGFloat
public let height: CGFloat
public let cornerRadius: CGFloat
public let gloss: Bool
public let action: () -> Void
public init(
title: String? = nil,
icon: UIImage? = nil,
theme: SolidRoundedButtonTheme,
font: SolidRoundedButtonFont = .bold,
fontSize: CGFloat = 17.0,
height: CGFloat = 48.0,
cornerRadius: CGFloat = 24.0,
gloss: Bool = false,
action: @escaping () -> Void
) {
self.title = title
self.icon = icon
self.theme = theme
self.font = font
self.fontSize = fontSize
self.height = height
self.cornerRadius = cornerRadius
self.gloss = gloss
self.action = action
}
public static func ==(lhs: SolidRoundedButtonComponent, rhs: SolidRoundedButtonComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.icon !== rhs.icon {
return false
}
if lhs.theme != rhs.theme {
return false
}
if lhs.font != rhs.font {
return false
}
if lhs.fontSize != rhs.fontSize {
return false
}
if lhs.height != rhs.height {
return false
}
if lhs.cornerRadius != rhs.cornerRadius {
return false
}
if lhs.gloss != rhs.gloss {
return false
}
return true
}
public final class View: UIView {
private var component: SolidRoundedButtonComponent?
private var button: SolidRoundedButtonView?
public func update(component: SolidRoundedButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if self.button == nil {
let button = SolidRoundedButtonView(
title: component.title,
icon: component.icon,
theme: component.theme,
font: component.font,
fontSize: component.fontSize,
height: component.height,
cornerRadius: component.cornerRadius,
gloss: component.gloss
)
self.button = button
self.addSubview(button)
button.pressed = { [weak self] in
self?.component?.action()
}
}
if let button = self.button {
button.updateTheme(component.theme)
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)
}
self.component = component
return availableSize
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ViewControllerComponent",
module_name = "ViewControllerComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display:Display",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
],
visibility = [
"//visibility:public",
],
)

View File

@ -35,23 +35,35 @@ public extension Transition {
}
open class ViewControllerComponentContainer: ViewController {
public enum NavigationBarAppearance {
case none
case transparent
case `default`
}
public final class Environment: Equatable {
public let statusBarHeight: CGFloat
public let navigationHeight: CGFloat
public let safeInsets: UIEdgeInsets
public let isVisible: Bool
public let theme: PresentationTheme
public let strings: PresentationStrings
public let controller: () -> ViewController?
public init(
statusBarHeight: CGFloat,
navigationHeight: CGFloat,
safeInsets: UIEdgeInsets,
isVisible: Bool,
theme: PresentationTheme,
strings: PresentationStrings,
controller: @escaping () -> ViewController?
) {
self.statusBarHeight = statusBarHeight
self.navigationHeight = navigationHeight
self.safeInsets = safeInsets
self.isVisible = isVisible
self.theme = theme
self.strings = strings
self.controller = controller
}
@ -64,12 +76,18 @@ open class ViewControllerComponentContainer: ViewController {
if lhs.statusBarHeight != rhs.statusBarHeight {
return false
}
if lhs.navigationHeight != rhs.navigationHeight {
return false
}
if lhs.safeInsets != rhs.safeInsets {
return false
}
if lhs.isVisible != rhs.isVisible {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
@ -78,15 +96,15 @@ open class ViewControllerComponentContainer: ViewController {
}
}
final class Node: ViewControllerTracingNode {
public final class Node: ViewControllerTracingNode {
private var presentationData: PresentationData
private weak var controller: ViewControllerComponentContainer?
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private var currentIsVisible: Bool = false
private var currentLayout: ContainerViewLayout?
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>) {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -101,13 +119,15 @@ open class ViewControllerComponentContainer: ViewController {
self.view.addSubview(self.hostView)
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: Transition) {
self.currentLayout = layout
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
self.currentLayout = (layout, navigationHeight)
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: navigationHeight,
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.intrinsicInsets.left + layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.intrinsicInsets.right + layout.safeInsets.right),
isVisible: self.currentIsVisible,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
controller: { [weak self] in
return self?.controller
@ -133,22 +153,31 @@ open class ViewControllerComponentContainer: ViewController {
guard let currentLayout = self.currentLayout else {
return
}
self.containerLayoutUpdated(currentLayout, transition: .immediate)
self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate)
}
}
var node: Node {
public var node: Node {
return self.displayNode as! Node
}
private let context: AccountContext
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
public init<C: Component>(context: AccountContext, component: C) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
self.context = context
self.component = AnyComponent(component)
super.init(navigationBarPresentationData: nil)
let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance {
case .none:
navigationBarPresentationData = nil
case .transparent:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 }, hideBackground: true, hideBadge: false, hideSeparator: true)
case .default:
navigationBarPresentationData = NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })
}
super.init(navigationBarPresentationData: navigationBarPresentationData)
}
required public init(coder aDecoder: NSCoder) {
@ -176,6 +205,8 @@ open class ViewControllerComponentContainer: ViewController {
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.node.containerLayoutUpdated(layout, transition: Transition(transition))
let navigationHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition))
}
}

View File

@ -224,3 +224,175 @@ public class ASTextNode: ImmediateTextNode {
return self.updateLayout(constrainedSize)
}
}
public class ImmediateTextView: TextView {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
public var verticalAlignment: TextVerticalAlignment = .top
public var truncationType: CTLineTruncationType = .end
public var maximumNumberOfLines: Int = 1
public var lineSpacing: CGFloat = 0.0
public var insets: UIEdgeInsets = UIEdgeInsets()
public var textShadowColor: UIColor?
public var textStroke: (UIColor, CGFloat)?
public var cutout: TextNodeCutout?
public var displaySpoilers = false
public var truncationMode: NSLineBreakMode {
get {
switch self.truncationType {
case .start:
return .byTruncatingHead
case .middle:
return .byTruncatingMiddle
case .end:
return .byTruncatingTail
@unknown default:
return .byTruncatingTail
}
} set(value) {
switch value {
case .byTruncatingHead:
self.truncationType = .start
case .byTruncatingMiddle:
self.truncationType = .middle
case .byTruncatingTail:
self.truncationType = .end
default:
self.truncationType = .end
}
}
}
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
private var linkHighlightingNode: LinkHighlightingNode?
public var linkHighlightColor: UIColor?
public var trailingLineWidth: CGFloat?
var constrainedSize: CGSize?
public var highlightAttributeAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? {
didSet {
self.updateInteractiveActions()
}
}
public var tapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public var longTapAttributeAction: (([NSAttributedString.Key: Any], Int) -> Void)?
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke, displaySpoilers: self.displaySpoilers))
let _ = apply()
if layout.numberOfLines > 1 {
self.trailingLineWidth = layout.trailingLineWidth
} else {
self.trailingLineWidth = nil
}
return layout.size
}
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
}
public func updateLayoutFullInfo(_ constrainedSize: CGSize) -> TextNodeLayout {
self.constrainedSize = constrainedSize
let makeLayout = TextView.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, displaySpoilers: self.displaySpoilers))
let _ = apply()
return layout
}
public func redrawIfPossible() {
if let constrainedSize = self.constrainedSize {
let _ = self.updateLayout(constrainedSize)
}
}
private func updateInteractiveActions() {
if self.highlightAttributeAction != nil {
if self.tapRecognizer == nil {
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
tapRecognizer.highlight = { [weak self] point in
if let strongSelf = self {
var rects: [CGRect]?
if let point = point {
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
var mappedRects: [CGRect] = []
for i in 0 ..< initialRects.count {
let lineRect = initialRects[i].0
var itemRect = initialRects[i].1
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
mappedRects.append(itemRect)
}
rects = mappedRects
} else {
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = strongSelf.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
strongSelf.linkHighlightingNode = linkHighlightingNode
strongSelf.addSubnode(linkHighlightingNode)
}
linkHighlightingNode.frame = strongSelf.bounds
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
strongSelf.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
self.addGestureRecognizer(tapRecognizer)
}
} else if let tapRecognizer = self.tapRecognizer {
self.tapRecognizer = nil
self.removeGestureRecognizer(tapRecognizer)
}
}
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.tapAttributeAction?(attributes, index)
}
case .longTap:
if let (index, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
self.longTapAttributeAction?(attributes, index)
}
default:
break
}
}
default:
break
}
}
}

View File

@ -890,7 +890,7 @@ public class TextNode: ASDisplayNode {
}
}
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
static func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
if let attributedString = attributedString {
let stringLength = attributedString.length
@ -1485,3 +1485,649 @@ public class TextNode: ASDisplayNode {
}
}
}
open class TextView: UIView {
public internal(set) var cachedLayout: TextNodeLayout?
override public init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
self.isOpaque = false
self.clipsToBounds = false
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func attributesAtPoint(_ point: CGPoint, orNearest: Bool = false) -> (Int, [NSAttributedString.Key: Any])? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.attributesAtPoint(point, orNearest: orNearest)
} else {
return nil
}
}
public func textRangesRects(text: String) -> [[CGRect]] {
return self.cachedLayout?.textRangesRects(text: text) ?? []
}
public func attributeSubstring(name: String, index: Int) -> (String, String)? {
return self.cachedLayout?.attributeSubstring(name: name, index: index)
}
public func attributeRects(name: String, at index: Int) -> [CGRect]? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 }
} else {
return nil
}
}
public func rangeRects(in range: NSRange) -> (rects: [CGRect], start: TextRangeRectEdge, end: TextRangeRectEdge)? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.rangeRects(in: range)
} else {
return nil
}
}
public func lineAndAttributeRects(name: String, at index: Int) -> [(CGRect, CGRect)]? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.lineAndAttributeRects(name: name, at: index)
} else {
return nil
}
}
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?, displaySpoilers: Bool) -> TextNodeLayout {
if let attributedString = attributedString {
let stringLength = attributedString.length
let font: CTFont
let resolvedAlignment: NSTextAlignment
if stringLength != 0 {
if let stringFont = attributedString.attribute(NSAttributedString.Key.font, at: 0, effectiveRange: nil) {
font = stringFont as! CTFont
} else {
font = defaultFont
}
if alignment == .center {
resolvedAlignment = .center
} else {
if let paragraphStyle = attributedString.attribute(NSAttributedString.Key.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle {
resolvedAlignment = paragraphStyle.alignment
} else {
resolvedAlignment = alignment
}
}
} else {
font = defaultFont
resolvedAlignment = alignment
}
let fontAscent = CTFontGetAscent(font)
let fontDescent = CTFontGetDescent(font)
let fontLineHeight = floor(fontAscent + fontDescent)
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
var lines: [TextNodeLine] = []
var blockQuotes: [TextNodeBlockQuote] = []
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
if maybeTypesetter == nil {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke, displaySpoilers: displaySpoilers)
}
let typesetter = maybeTypesetter!
var lastLineCharacterIndex: CFIndex = 0
var layoutSize = CGSize()
var cutoutEnabled = false
var cutoutMinY: CGFloat = 0.0
var cutoutMaxY: CGFloat = 0.0
var cutoutWidth: CGFloat = 0.0
var cutoutOffset: CGFloat = 0.0
var bottomCutoutEnabled = false
var bottomCutoutSize = CGSize()
if let topLeft = cutout?.topLeft {
cutoutMinY = -fontLineSpacing
cutoutMaxY = topLeft.height + fontLineSpacing
cutoutWidth = topLeft.width
cutoutOffset = cutoutWidth
cutoutEnabled = true
} else if let topRight = cutout?.topRight {
cutoutMinY = -fontLineSpacing
cutoutMaxY = topRight.height + fontLineSpacing
cutoutWidth = topRight.width
cutoutEnabled = true
}
if let bottomRight = cutout?.bottomRight {
bottomCutoutSize = bottomRight
bottomCutoutEnabled = true
}
let firstLineOffset = floorToScreenPixels(fontDescent)
var truncated = false
var first = true
while true {
var strikethroughs: [TextNodeStrikethrough] = []
var spoilers: [TextNodeSpoiler] = []
var spoilerWords: [TextNodeSpoiler] = []
var lineConstrainedWidth = constrainedSize.width
var lineConstrainedWidthDelta: CGFloat = 0.0
var lineOriginY = floorToScreenPixels(layoutSize.height + fontAscent)
if !first {
lineOriginY += fontLineSpacing
}
var lineCutoutOffset: CGFloat = 0.0
var lineAdditionalWidth: CGFloat = 0.0
if cutoutEnabled {
if lineOriginY - fontLineHeight < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY {
lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth)
lineConstrainedWidthDelta = -cutoutWidth
lineCutoutOffset = cutoutOffset
lineAdditionalWidth = cutoutWidth
}
}
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth))
func addSpoiler(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
spoilers.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset), height: ascent + descent)))
}
func addSpoilerWord(line: CTLine, ascent: CGFloat, descent: CGFloat, startIndex: Int, endIndex: Int, rightInset: CGFloat = 0.0) {
var secondaryLeftOffset: CGFloat = 0.0
let rawLeftOffset = CTLineGetOffsetForStringIndex(line, startIndex, &secondaryLeftOffset)
var leftOffset = floor(rawLeftOffset)
if !rawLeftOffset.isEqual(to: secondaryLeftOffset) {
leftOffset = floor(secondaryLeftOffset)
}
var secondaryRightOffset: CGFloat = 0.0
let rawRightOffset = CTLineGetOffsetForStringIndex(line, endIndex, &secondaryRightOffset)
var rightOffset = ceil(rawRightOffset)
if !rawRightOffset.isEqual(to: secondaryRightOffset) {
rightOffset = ceil(secondaryRightOffset)
}
spoilerWords.append(TextNodeSpoiler(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent)))
}
var isLastLine = false
if maximumNumberOfLines != 0 && lines.count == maximumNumberOfLines - 1 && lineCharacterCount > 0 {
isLastLine = true
} else if layoutSize.height + (fontLineSpacing + fontLineHeight) * 2.0 > constrainedSize.height {
isLastLine = true
}
if isLastLine {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex)
var brokenLineRange = CFRange(location: lastLineCharacterIndex, length: lineCharacterCount)
if brokenLineRange.location + brokenLineRange.length > attributedString.length {
brokenLineRange.length = attributedString.length - brokenLineRange.location
}
if lineRange.length == 0 {
break
}
let coreTextLine: CTLine
let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0)
var lineConstrainedSize = constrainedSize
lineConstrainedSize.width += lineConstrainedWidthDelta
if bottomCutoutEnabled {
lineConstrainedSize.width -= bottomCutoutSize.width
}
if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(lineConstrainedSize.width) {
coreTextLine = originalLine
} else {
var truncationTokenAttributes: [NSAttributedString.Key : AnyObject] = [:]
truncationTokenAttributes[NSAttributedString.Key.font] = font
truncationTokenAttributes[NSAttributedString.Key(rawValue: kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber
let tokenString = "\u{2026}"
let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes)
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(lineConstrainedSize.width), truncationType, truncationToken) ?? truncationToken
let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun]
for run in runs {
let runAttributes: NSDictionary = CTRunGetAttributes(run)
if let _ = runAttributes["CTForegroundColorFromContext"] {
brokenLineRange.length = CTRunGetStringRange(run).location
break
}
}
if brokenLineRange.location + brokenLineRange.length > attributedString.length {
brokenLineRange.length = attributedString.length - brokenLineRange.location
}
truncated = true
}
var headIndent: CGFloat = 0.0
if brokenLineRange.location >= 0 && brokenLineRange.length > 0 && brokenLineRange.location + brokenLineRange.length <= attributedString.length {
attributedString.enumerateAttributes(in: NSMakeRange(brokenLineRange.location, brokenLineRange.length), options: []) { attributes, range, _ in
if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
var startIndex: Int?
var currentIndex: Int?
let nsString = (attributedString.string as NSString)
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
if let currentStartIndex = startIndex {
startIndex = nil
let endIndex = range.location
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
}
} else if startIndex == nil {
startIndex = range.location
}
currentIndex = range.location + range.length
}
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
startIndex = nil
let endIndex = currentIndex
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex, rightInset: truncated ? 12.0 : 0.0)
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
let x = lowerX < upperX ? lowerX : upperX
strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight)))
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
headIndent = paragraphStyle.headIndent
}
}
}
let lineWidth = min(lineConstrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight + fontLineSpacing
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
if glyphRuns.count != 0 {
let run = glyphRuns[0] as! CTRun
if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
isRTL = true
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords))
break
} else {
if lineCharacterCount > 0 {
if first {
first = false
} else {
layoutSize.height += fontLineSpacing
}
var lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount)
if lineRange.location + lineRange.length > attributedString.length {
lineRange.length = attributedString.length - lineRange.location
}
if lineRange.length < 0 {
break
}
let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0)
lastLineCharacterIndex += lineCharacterCount
var headIndent: CGFloat = 0.0
attributedString.enumerateAttributes(in: NSMakeRange(lineRange.location, lineRange.length), options: []) { attributes, range, _ in
if attributes[NSAttributedString.Key(rawValue: "TelegramSpoiler")] != nil || attributes[NSAttributedString.Key(rawValue: "Attribute__Spoiler")] != nil {
var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
CTLineGetTypographicBounds(coreTextLine, &ascent, &descent, nil)
var startIndex: Int?
var currentIndex: Int?
let nsString = (attributedString.string as NSString)
nsString.enumerateSubstrings(in: range, options: .byComposedCharacterSequences) { substring, range, _, _ in
if let substring = substring, substring.rangeOfCharacter(from: .whitespacesAndNewlines) != nil {
if let currentStartIndex = startIndex {
startIndex = nil
let endIndex = range.location
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
}
} else if startIndex == nil {
startIndex = range.location
}
currentIndex = range.location + range.length
}
if let currentStartIndex = startIndex, let currentIndex = currentIndex {
startIndex = nil
let endIndex = currentIndex
addSpoilerWord(line: coreTextLine, ascent: ascent, descent: descent, startIndex: currentStartIndex, endIndex: endIndex)
}
addSpoiler(line: coreTextLine, ascent: ascent, descent: descent, startIndex: range.location, endIndex: range.location + range.length)
} else if let _ = attributes[NSAttributedString.Key.strikethroughStyle] {
let lowerX = floor(CTLineGetOffsetForStringIndex(coreTextLine, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(coreTextLine, range.location + range.length, nil))
let x = lowerX < upperX ? lowerX : upperX
strikethroughs.append(TextNodeStrikethrough(range: range, frame: CGRect(x: x, y: 0.0, width: abs(upperX - lowerX), height: fontLineHeight)))
} else if let paragraphStyle = attributes[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle {
headIndent = paragraphStyle.headIndent
}
}
let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine)))
let lineFrame = CGRect(x: lineCutoutOffset + headIndent, y: lineOriginY, width: lineWidth, height: fontLineHeight)
layoutSize.height += fontLineHeight
layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth)
if headIndent > 0.0 {
blockQuotes.append(TextNodeBlockQuote(frame: lineFrame))
}
var isRTL = false
let glyphRuns = CTLineGetGlyphRuns(coreTextLine) as NSArray
if glyphRuns.count != 0 {
let run = glyphRuns[0] as! CTRun
if CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) {
isRTL = true
}
}
lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length), isRTL: isRTL, strikethroughs: strikethroughs, spoilers: spoilers, spoilerWords: spoilerWords))
} else {
if !lines.isEmpty {
layoutSize.height += fontLineSpacing
}
break
}
}
}
let rawLayoutSize = layoutSize
if !lines.isEmpty && bottomCutoutEnabled {
let proposedWidth = lines[lines.count - 1].frame.width + bottomCutoutSize.width
if proposedWidth > layoutSize.width {
if proposedWidth <= constrainedSize.width + .ulpOfOne {
layoutSize.width = proposedWidth
} else {
layoutSize.height += bottomCutoutSize.height
}
}
}
if lines.count < minimumNumberOfLines {
var lineCount = lines.count
while lineCount < minimumNumberOfLines {
if lineCount != 0 {
layoutSize.height += fontLineSpacing
}
layoutSize.height += fontLineHeight
lineCount += 1
}
}
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke, displaySpoilers: displaySpoilers)
} else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke, displaySpoilers: displaySpoilers)
}
}
public override func draw(_ rect: CGRect) {
let bounds = self.bounds
let layout = self.cachedLayout
let context = UIGraphicsGetCurrentContext()!
context.setAllowsAntialiasing(true)
context.setAllowsFontSmoothing(false)
context.setShouldSmoothFonts(false)
context.setAllowsFontSubpixelPositioning(false)
context.setShouldSubpixelPositionFonts(false)
context.setAllowsFontSubpixelQuantization(true)
context.setShouldSubpixelQuantizeFonts(true)
var clearRects: [CGRect] = []
if let layout = layout {
if layout.backgroundColor != nil {
context.setBlendMode(.copy)
context.setFillColor((layout.backgroundColor ?? UIColor.clear).cgColor)
context.fill(bounds)
}
if let textShadowColor = layout.textShadowColor {
context.setTextDrawingMode(.fill)
context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 0.0, color: textShadowColor.cgColor)
}
if let (textStrokeColor, textStrokeWidth) = layout.textStroke {
context.setBlendMode(.normal)
context.setLineCap(.round)
context.setLineJoin(.round)
context.setStrokeColor(textStrokeColor.cgColor)
context.setFillColor(textStrokeColor.cgColor)
context.setLineWidth(textStrokeWidth)
context.setTextDrawingMode(.fillStroke)
}
let textMatrix = context.textMatrix
let textPosition = context.textPosition
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
let alignment = layout.resolvedAlignment
var offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
switch layout.verticalAlignment {
case .top:
break
case .middle:
offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top
case .bottom:
offset.y = floor(bounds.height - layout.size.height) + layout.insets.top
}
for i in 0 ..< layout.lines.count {
let line = layout.lines[i]
var lineFrame = line.frame
lineFrame.origin.y += offset.y
if alignment == .center {
lineFrame.origin.x = offset.x + floor((bounds.size.width - lineFrame.width) / 2.0)
} else if alignment == .natural, line.isRTL {
lineFrame.origin.x = offset.x + floor(bounds.size.width - lineFrame.width)
lineFrame = displayLineFrame(frame: lineFrame, isRTL: line.isRTL, boundingRect: CGRect(origin: CGPoint(), size: bounds.size), cutout: layout.cutout)
}
context.textPosition = CGPoint(x: lineFrame.minX, y: lineFrame.minY)
if layout.displaySpoilers && !line.spoilers.isEmpty {
context.saveGState()
var clipRects: [CGRect] = []
for spoiler in line.spoilerWords {
var spoilerClipRect = spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY - UIScreenPixel)
spoilerClipRect.size.height += 1.0 + UIScreenPixel
clipRects.append(spoilerClipRect)
}
context.clip(to: clipRects)
}
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
if glyphRuns.count != 0 {
for run in glyphRuns {
let run = run as! CTRun
let glyphCount = CTRunGetGlyphCount(run)
CTRunDraw(run, context, CFRangeMake(0, glyphCount))
}
}
if !line.strikethroughs.isEmpty {
for strikethrough in line.strikethroughs {
var textColor: UIColor?
layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in
if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor {
textColor = color
}
}
if let textColor = textColor {
context.setFillColor(textColor.cgColor)
}
let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY)
context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0))
}
}
if !line.spoilers.isEmpty {
if layout.displaySpoilers {
context.restoreGState()
} else {
for spoiler in line.spoilerWords {
var spoilerClearRect = spoiler.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY - UIScreenPixel)
spoilerClearRect.size.height += 1.0 + UIScreenPixel
clearRects.append(spoilerClearRect)
}
}
}
}
var blockQuoteFrames: [CGRect] = []
var currentBlockQuoteFrame: CGRect?
for blockQuote in layout.blockQuotes {
if let frame = currentBlockQuoteFrame {
if blockQuote.frame.minY - frame.maxY < 20.0 {
currentBlockQuoteFrame = frame.union(blockQuote.frame)
} else {
blockQuoteFrames.append(frame)
currentBlockQuoteFrame = frame
}
} else {
currentBlockQuoteFrame = blockQuote.frame
}
}
if let frame = currentBlockQuoteFrame {
blockQuoteFrames.append(frame)
}
for frame in blockQuoteFrames {
if let lineColor = layout.lineColor {
context.setFillColor(lineColor.cgColor)
}
let rect = UIBezierPath(roundedRect: CGRect(x: frame.minX - 9.0, y: frame.minY - 14.0, width: 2.0, height: frame.height), cornerRadius: 1.0)
context.addPath(rect.cgPath)
context.fillPath()
}
context.textMatrix = textMatrix
context.textPosition = CGPoint(x: textPosition.x, y: textPosition.y)
}
context.setBlendMode(.normal)
for rect in clearRects {
context.clear(rect)
}
}
public static func asyncLayout(_ maybeView: TextView?) -> (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextView) {
let existingLayout: TextNodeLayout? = maybeView?.cachedLayout
return { arguments in
let layout: TextNodeLayout
var updated = false
if let existingLayout = existingLayout, existingLayout.constrainedSize == arguments.constrainedSize && existingLayout.maximumNumberOfLines == arguments.maximumNumberOfLines && existingLayout.truncationType == arguments.truncationType && existingLayout.cutout == arguments.cutout && existingLayout.explicitAlignment == arguments.alignment && existingLayout.lineSpacing.isEqual(to: arguments.lineSpacing) {
let stringMatch: Bool
var colorMatch: Bool = true
if let backgroundColor = arguments.backgroundColor, let previousBackgroundColor = existingLayout.backgroundColor {
if !backgroundColor.isEqual(previousBackgroundColor) {
colorMatch = false
}
} else if (arguments.backgroundColor != nil) != (existingLayout.backgroundColor != nil) {
colorMatch = false
}
if !colorMatch {
stringMatch = false
} else if let existingString = existingLayout.attributedString, let string = arguments.attributedString {
stringMatch = existingString.isEqual(to: string)
} else if existingLayout.attributedString == nil && arguments.attributedString == nil {
stringMatch = true
} else {
stringMatch = false
}
if stringMatch {
layout = existingLayout
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
updated = true
}
} else {
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke, displaySpoilers: arguments.displaySpoilers)
updated = true
}
let view = maybeView ?? TextView()
return (layout, {
view.cachedLayout = layout
if updated {
if layout.size.width.isZero && layout.size.height.isZero {
view.layer.contents = nil
}
view.setNeedsDisplay()
}
return view
})
}
}
}

View File

@ -74,6 +74,7 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/Components/ReactionImageComponent:ReactionImageComponent",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,32 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "CreateExternalMediaStreamScreen",
module_name = "CreateExternalMediaStreamScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent",
"//submodules/Components/ActivityIndicatorComponent:ActivityIndicatorComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,447 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import PresentationDataUtils
import AccountContext
import ComponentFlow
import ViewControllerComponent
import MultilineTextComponent
import SolidRoundedButtonComponent
import BundleIconComponent
import AnimatedStickerComponent
import ActivityIndicatorComponent
private final class CreateExternalMediaStreamScreenComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let peerId: EnginePeer.Id
let credentialsPromise: Promise<GroupCallStreamCredentials>?
init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
self.context = context
self.peerId = peerId
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.credentialsPromise !== rhs.credentialsPromise {
return false
}
return true
}
final class State: ComponentState {
let context: AccountContext
let peerId: EnginePeer.Id
private(set) var credentials: GroupCallStreamCredentials?
private var credentialsDisposable: Disposable?
private let activeActionDisposable = MetaDisposable()
init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
self.context = context
self.peerId = peerId
super.init()
let credentialsSignal: Signal<GroupCallStreamCredentials, NoError>
if let credentialsPromise = credentialsPromise {
credentialsSignal = credentialsPromise.get()
} else {
credentialsSignal = context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, revokePreviousCredentials: false)
|> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in
return .never()
}
}
self.credentialsDisposable = (credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.credentials = result
strongSelf.updated(transition: .immediate)
})
}
deinit {
self.credentialsDisposable?.dispose()
self.activeActionDisposable.dispose()
}
func copyCredentials(_ key: KeyPath<GroupCallStreamCredentials, String>) {
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
}
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { [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, credentialsPromise: self.credentialsPromise)
}
static var body: Body {
let background = Child(Rectangle.self)
let animation = Child(AnimatedStickerComponent.self)
let text = Child(MultilineTextComponent.self)
let bottomText = Child(MultilineTextComponent.self)
let button = Child(SolidRoundedButtonComponent.self)
let activityIndicator = Child(ActivityIndicatorComponent.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 credentialsSideInset: CGFloat = 16.0
let credentialsTopInset: CGFloat = 9.0
let credentialsTitleSpacing: CGFloat = 5.0
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state
let controller = environment.controller
let bottomInset: CGFloat
if environment.safeInsets.bottom.isZero {
bottomInset = 16.0
} else {
bottomInset = 42.0
}
let background = background.update(
component: Rectangle(color: environment.theme.list.blocksBackgroundColor),
availableSize: context.availableSize,
transition: context.transition
)
let animation = animation.update(
component: AnimatedStickerComponent(
animation: AnimatedStickerComponent.Animation(
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: NSAttributedString(string: environment.strings.CreateExternalStream_Text, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
let bottomText = bottomText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.1
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
transition: context.transition
)
let button = button.update(
component: SolidRoundedButtonComponent(
title: environment.strings.CreateExternalStream_StartStreaming,
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: true,
action: { [weak state] in
guard let state = state, let controller = controller() else {
return
}
state.createAndJoinGroupCall(baseController: controller, completion: { [weak controller] in
controller?.dismiss()
})
}
),
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
transition: context.transition
)
let credentialsItemHeight: CGFloat = 60.0
let credentialsAreaSize = CGSize(width: context.availableSize.width - sideInset * 2.0, height: credentialsItemHeight * 2.0)
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 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 + 16.0), size: text.size)
context.add(text
.position(CGPoint(x: textFrame.midX, y: textFrame.midY))
)
let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize)
if let credentials = context.state.credentials {
let credentialsURLTitle = credentialsURLTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsKeyTitle = credentialsKeyTitle.update(
component: MultilineTextComponent(
text: NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(14.0), textColor: environment.theme.list.itemPrimaryTextColor, paragraphAlignment: .left),
horizontalAlignment: .left,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsURLText = credentialsURLText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: credentials.url, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsKeyText = credentialsKeyText.update(
component: MultilineTextComponent(
text: NSAttributedString(string: credentials.streamKey, font: Font.regular(16.0), textColor: environment.theme.list.itemAccentColor, paragraphAlignment: .left),
horizontalAlignment: .left,
truncationType: .middle,
maximumNumberOfLines: 1
),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height),
transition: context.transition
)
let credentialsBackground = credentialsBackground.update(
component: RoundedRectangle(color: environment.theme.list.itemBlocksBackgroundColor, cornerRadius: 10.0),
availableSize: credentialsAreaSize,
transition: context.transition
)
let credentialsStripe = credentialsStripe.update(
component: Rectangle(color: environment.theme.list.itemPlainSeparatorColor),
availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset, height: UIScreenPixel),
transition: context.transition
)
let credentialsCopyURLButton = credentialsCopyURLButton.update(
component: Button(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: environment.theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.copyCredentials(\.url)
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
let credentialsCopyKeyButton = credentialsCopyKeyButton.update(
component: Button(
content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: environment.theme.list.itemAccentColor)),
action: { [weak state] in
guard let state = state else {
return
}
state.copyCredentials(\.streamKey)
}
).minSize(CGSize(width: 44.0, height: 44.0)),
availableSize: CGSize(width: 44.0, height: 44.0),
transition: context.transition
)
context.add(credentialsBackground
.position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY))
)
context.add(credentialsStripe
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsStripe.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight))
)
context.add(credentialsURLTitle
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsURLTitle.size.height / 2.0))
)
context.add(credentialsURLText
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLText.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsTitleSpacing + credentialsURLTitle.size.height + credentialsURLText.size.height / 2.0))
)
context.add(credentialsCopyURLButton
.position(CGPoint(x: credentialsFrame.maxX - 12.0 - credentialsCopyURLButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight / 2.0))
)
context.add(credentialsKeyTitle
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsKeyTitle.size.height / 2.0))
)
context.add(credentialsKeyText
.position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyText.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsTitleSpacing + credentialsKeyTitle.size.height + credentialsKeyText.size.height / 2.0))
)
context.add(credentialsCopyKeyButton
.position(CGPoint(x: credentialsFrame.maxX - 12.0 - credentialsCopyKeyButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsItemHeight / 2.0))
)
} else {
let activityIndicator = activityIndicator.update(
component: ActivityIndicatorComponent(color: environment.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: sideInset, y: context.availableSize.height - bottomInset - button.size.height), size: button.size)
context.add(bottomText
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - 14.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 {
private let context: AccountContext
private let peerId: EnginePeer.Id
public init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
self.context = context
self.peerId = peerId
super.init(context: context, component: CreateExternalMediaStreamScreenComponent(context: context, peerId: peerId, credentialsPromise: credentialsPromise), navigationBarAppearance: .transparent)
self.navigationPresentation = .modal
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.title = presentationData.strings.CreateExternalStream_Title
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
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)
}
}

View File

@ -12,6 +12,7 @@ swift_library(
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import HierarchyTrackingLayer
private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter: CGFloat = 22.0, lineWidth: CGFloat = 2.0) -> UIImage? {
return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in
@ -15,7 +16,7 @@ private func generateIndefiniteActivityIndicatorImage(color: UIColor, diameter:
})
}
public final class SolidRoundedButtonTheme {
public final class SolidRoundedButtonTheme: Equatable {
public let backgroundColor: UIColor
public let gradientBackgroundColor: UIColor?
public let foregroundColor: UIColor
@ -25,6 +26,19 @@ public final class SolidRoundedButtonTheme {
self.gradientBackgroundColor = gradientBackgroundColor
self.foregroundColor = foregroundColor
}
public static func ==(lhs: SolidRoundedButtonTheme, rhs: SolidRoundedButtonTheme) -> Bool {
if lhs.backgroundColor != rhs.backgroundColor {
return false
}
if lhs.gradientBackgroundColor != rhs.gradientBackgroundColor {
return false
}
if lhs.foregroundColor != rhs.foregroundColor {
return false
}
return true
}
}
public enum SolidRoundedButtonFont {
@ -38,7 +52,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private var fontSize: CGFloat
private let buttonBackgroundNode: ASDisplayNode
private let buttonGlossNode: SolidRoundedButtonGlossNode?
private let buttonGlossView: SolidRoundedButtonGlossView?
private let buttonNode: HighlightTrackingButtonNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
@ -95,9 +109,9 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonBackgroundNode.cornerRadius = cornerRadius
if gloss {
self.buttonGlossNode = SolidRoundedButtonGlossNode(color: theme.foregroundColor, cornerRadius: cornerRadius)
self.buttonGlossView = SolidRoundedButtonGlossView(color: theme.foregroundColor, cornerRadius: cornerRadius)
} else {
self.buttonGlossNode = nil
self.buttonGlossView = nil
}
self.buttonNode = HighlightTrackingButtonNode()
@ -116,8 +130,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
super.init()
self.addSubnode(self.buttonBackgroundNode)
if let buttonGlossNode = self.buttonGlossNode {
self.addSubnode(buttonGlossNode)
if let buttonGlossView = self.buttonGlossView {
self.view.addSubview(buttonGlossView)
}
self.addSubnode(self.buttonNode)
self.addSubnode(self.titleNode)
@ -211,7 +225,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.theme = theme
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonGlossNode?.color = theme.foregroundColor
self.buttonGlossView?.color = theme.foregroundColor
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
@ -237,8 +251,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
let buttonSize = CGSize(width: width, height: self.buttonHeight)
let buttonFrame = CGRect(origin: CGPoint(), size: buttonSize)
transition.updateFrame(node: self.buttonBackgroundNode, frame: buttonFrame)
if let buttonGlossNode = self.buttonGlossNode {
transition.updateFrame(node: buttonGlossNode, frame: buttonFrame)
if let buttonGlossView = self.buttonGlossView {
transition.updateFrame(view: buttonGlossView, frame: buttonFrame)
}
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
@ -289,7 +303,257 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
}
private final class SolidRoundedButtonGlossNodeParameters: NSObject {
public final class SolidRoundedButtonView: UIView {
private var theme: SolidRoundedButtonTheme
private var font: SolidRoundedButtonFont
private var fontSize: CGFloat
private let buttonBackgroundNode: UIView
private let buttonGlossView: SolidRoundedButtonGlossView?
private let buttonNode: HighlightTrackingButton
private let titleNode: ImmediateTextView
private let subtitleNode: ImmediateTextView
private let iconNode: UIImageView
private var progressNode: UIImageView?
private let buttonHeight: CGFloat
private let buttonCornerRadius: CGFloat
public var pressed: (() -> Void)?
public var validLayout: CGFloat?
public var title: String? {
didSet {
if let width = self.validLayout {
_ = self.updateLayout(width: width, transition: .immediate)
}
}
}
public var subtitle: String? {
didSet {
if let width = self.validLayout {
_ = self.updateLayout(width: width, previousSubtitle: oldValue, transition: .immediate)
}
}
}
public var icon: UIImage? {
didSet {
self.iconNode.image = generateTintedImage(image: self.icon, color: self.theme.foregroundColor)
}
}
public var iconSpacing: CGFloat = 8.0 {
didSet {
if let width = self.validLayout {
_ = self.updateLayout(width: width, transition: .immediate)
}
}
}
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
self.theme = theme
self.font = font
self.fontSize = fontSize
self.buttonHeight = height
self.buttonCornerRadius = cornerRadius
self.title = title
self.buttonBackgroundNode = UIView()
self.buttonBackgroundNode.clipsToBounds = true
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonBackgroundNode.layer.cornerRadius = cornerRadius
if gloss {
self.buttonGlossView = SolidRoundedButtonGlossView(color: theme.foregroundColor, cornerRadius: cornerRadius)
} else {
self.buttonGlossView = nil
}
self.buttonNode = HighlightTrackingButton()
self.titleNode = ImmediateTextView()
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextView()
self.subtitleNode.isUserInteractionEnabled = false
self.iconNode = UIImageView()
self.iconNode.image = generateTintedImage(image: icon, color: self.theme.foregroundColor)
super.init(frame: CGRect())
self.addSubview(self.buttonBackgroundNode)
if let buttonGlossView = self.buttonGlossView {
self.addSubview(buttonGlossView)
}
self.addSubview(self.buttonNode)
self.addSubview(self.titleNode)
self.addSubview(self.subtitleNode)
self.addSubview(self.iconNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.55
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.55
strongSelf.subtitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.subtitleNode.alpha = 0.55
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.55
} else {
if strongSelf.buttonBackgroundNode.alpha > 0.0 {
strongSelf.buttonBackgroundNode.alpha = 1.0
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.subtitleNode.alpha = 1.0
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
}
}
}
if #available(iOS 13.0, *) {
self.buttonBackgroundNode.layer.cornerCurve = .continuous
}
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func transitionToProgress() {
guard self.progressNode == nil else {
return
}
self.isUserInteractionEnabled = false
let buttonOffset = self.buttonBackgroundNode.frame.minX
let buttonWidth = self.buttonBackgroundNode.frame.width
let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(buttonOffset + (buttonWidth - self.buttonHeight) / 2.0), y: 0.0), size: CGSize(width: self.buttonHeight, height: self.buttonHeight))
let progressNode = UIImageView()
progressNode.frame = progressFrame
progressNode.image = generateIndefiniteActivityIndicatorImage(color: self.buttonBackgroundNode.backgroundColor ?? .clear, diameter: self.buttonHeight, lineWidth: 2.0 + UIScreenPixel)
self.insertSubview(progressNode, at: 0)
self.progressNode = progressNode
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
basicAnimation.duration = 0.5
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
basicAnimation.beginTime = 1.0
progressNode.layer.add(basicAnimation, forKey: "progressRotation")
self.buttonBackgroundNode.layer.cornerRadius = self.buttonHeight / 2.0
self.buttonBackgroundNode.layer.animate(from: self.buttonCornerRadius as NSNumber, to: self.buttonHeight / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
self.buttonBackgroundNode.layer.animateFrame(from: self.buttonBackgroundNode.frame, to: progressFrame, duration: 0.2)
self.buttonBackgroundNode.alpha = 0.0
self.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2, removeOnCompletion: false)
progressNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false)
self.titleNode.alpha = 0.0
self.titleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
self.subtitleNode.alpha = 0.0
self.subtitleNode.layer.animateAlpha(from: 0.55, to: 0.0, duration: 0.2)
}
public func updateTheme(_ theme: SolidRoundedButtonTheme) {
guard theme !== self.theme else {
return
}
self.theme = theme
self.buttonBackgroundNode.backgroundColor = theme.backgroundColor
self.buttonGlossView?.color = theme.foregroundColor
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor)
if let width = self.validLayout {
_ = self.updateLayout(width: width, transition: .immediate)
}
}
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
return self.updateLayout(width: width, previousSubtitle: self.subtitle, transition: transition)
}
private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = width
let buttonSize = CGSize(width: width, height: self.buttonHeight)
let buttonFrame = CGRect(origin: CGPoint(), size: buttonSize)
transition.updateFrame(view: self.buttonBackgroundNode, frame: buttonFrame)
if let buttonGlossView = self.buttonGlossView {
transition.updateFrame(view: buttonGlossView, frame: buttonFrame)
}
transition.updateFrame(view: self.buttonNode, frame: buttonFrame)
if self.title != self.titleNode.attributedText?.string {
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor)
}
let iconSize = self.iconNode.image?.size ?? CGSize()
let titleSize = self.titleNode.updateLayout(buttonSize)
let iconSpacing: CGFloat = self.iconSpacing
var contentWidth: CGFloat = titleSize.width
if !iconSize.width.isZero {
contentWidth += iconSize.width + iconSpacing
}
var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0)
transition.updateFrame(view: self.iconNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize))
if !iconSize.width.isZero {
nextContentOrigin += iconSize.width + iconSpacing
}
let spacingOffset: CGFloat = 9.0
let verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset
let titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
transition.updateFrame(view: self.titleNode, frame: titleFrame)
if self.subtitle != self.subtitleNode.attributedText?.string {
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
}
let subtitleSize = self.subtitleNode.updateLayout(buttonSize)
let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize)
transition.updateFrame(view: self.subtitleNode, frame: subtitleFrame)
if previousSubtitle == nil && self.subtitle != nil {
self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
self.subtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
return buttonSize.height
}
@objc private func buttonPressed() {
self.pressed?()
}
}
private final class SolidRoundedButtonGlossViewParameters: NSObject {
let gradientColors: NSArray?
let cornerRadius: CGFloat
let progress: CGFloat
@ -301,7 +565,7 @@ private final class SolidRoundedButtonGlossNodeParameters: NSObject {
}
}
public final class SolidRoundedButtonGlossNode: ASDisplayNode {
public final class SolidRoundedButtonGlossView: UIView {
public var color: UIColor {
didSet {
self.updateGradientColors()
@ -313,14 +577,19 @@ public final class SolidRoundedButtonGlossNode: ASDisplayNode {
private let buttonCornerRadius: CGFloat
private var gradientColors: NSArray?
private let trackingLayer: HierarchyTrackingLayer
public init(color: UIColor, cornerRadius: CGFloat) {
self.color = color
self.buttonCornerRadius = cornerRadius
super.init()
self.trackingLayer = HierarchyTrackingLayer()
super.init(frame: CGRect())
self.layer.addSublayer(self.trackingLayer)
self.isOpaque = false
self.isLayerBacked = true
var previousTime: CFAbsoluteTime?
self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in
@ -347,41 +616,48 @@ public final class SolidRoundedButtonGlossNode: ASDisplayNode {
})
self.updateGradientColors()
self.trackingLayer.didEnterHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animator?.isPaused = false
}
self.trackingLayer.didExitHierarchy = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animator?.isPaused = true
}
}
required public init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateGradientColors() {
let transparentColor = self.color.withAlphaComponent(0.0).cgColor
self.gradientColors = [transparentColor, transparentColor, self.color.withAlphaComponent(0.12).cgColor, transparentColor, transparentColor]
}
override public func willEnterHierarchy() {
super.willEnterHierarchy()
self.animator?.isPaused = false
}
override public func didExitHierarchy() {
super.didExitHierarchy()
self.animator?.isPaused = true
}
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return SolidRoundedButtonGlossNodeParameters(gradientColors: self.gradientColors, cornerRadius: self.buttonCornerRadius, progress: self.progress)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
override public func draw(_ rect: CGRect) {
let parameters = SolidRoundedButtonGlossViewParameters(gradientColors: self.gradientColors, cornerRadius: self.buttonCornerRadius, progress: self.progress)
guard let gradientColors = parameters.gradientColors else {
return
}
let context = UIGraphicsGetCurrentContext()!
if let parameters = parameters as? SolidRoundedButtonGlossNodeParameters, let gradientColors = parameters.gradientColors {
let path = UIBezierPath(roundedRect: bounds, cornerRadius: parameters.cornerRadius)
context.addPath(path.cgPath)
context.clip()
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
let x = -4.0 * bounds.size.width + 8.0 * bounds.size.width * parameters.progress
context.drawLinearGradient(gradient, start: CGPoint(x: x, y: 0.0), end: CGPoint(x: x + bounds.size.width, y: 0.0), options: CGGradientDrawingOptions())
}
let path = UIBezierPath(roundedRect: bounds, cornerRadius: parameters.cornerRadius)
context.addPath(path.cgPath)
context.clip()
var locations: [CGFloat] = [0.0, 0.15, 0.5, 0.85, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
let x = -4.0 * bounds.size.width + 8.0 * bounds.size.width * parameters.progress
context.drawLinearGradient(gradient, start: CGPoint(x: x, y: 0.0), end: CGPoint(x: x + bounds.size.width, y: 0.0), options: CGGradientDrawingOptions())
}
}

View File

@ -98,6 +98,9 @@ swift_library(
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/ActivityIndicatorComponent:ActivityIndicatorComponent",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
],
visibility = [
"//visibility:public",

View File

@ -12,6 +12,8 @@ import UndoUI
import TelegramPresentationData
import LottieAnimationComponent
import ContextUI
import ViewControllerComponent
import BundleIconComponent
final class NavigationBackButtonComponent: Component {
let text: String
@ -97,61 +99,6 @@ final class NavigationBackButtonComponent: Component {
}
}
final class BundleIconComponent: Component {
let name: String
let tintColor: UIColor?
init(name: String, tintColor: UIColor?) {
self.name = name
self.tintColor = tintColor
}
static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool {
if lhs.name != rhs.name {
return false
}
if lhs.tintColor != rhs.tintColor {
return false
}
return false
}
public final class View: UIImageView {
private var component: BundleIconComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BundleIconComponent, availableSize: CGSize, transition: Transition) -> CGSize {
if self.component?.name != component.name || self.component?.tintColor != component.tintColor {
if let tintColor = component.tintColor {
self.image = generateTintedImage(image: UIImage(bundleImageName: component.name), color: tintColor, backgroundColor: nil)
} else {
self.image = UIImage(bundleImageName: component.name)
}
}
self.component = component
let imageSize = self.image?.size ?? CGSize()
return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
private final class NavigationBarComponent: CombinedComponent {
let topInset: CGFloat
let sideInset: CGFloat
@ -206,7 +153,7 @@ private final class NavigationBarComponent: CombinedComponent {
let contentHeight: CGFloat = 44.0
let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let leftItem = context.component.leftItem.flatMap { leftItemComponent in
return leftItem.update(
@ -389,7 +336,7 @@ private final class ToolbarComponent: CombinedComponent {
let contentHeight: CGFloat = 44.0
let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let leftItem = context.component.leftItem.flatMap { leftItemComponent in
return leftItem.update(
@ -495,6 +442,8 @@ public final class MediaStreamComponent: CombinedComponent {
private(set) var canManageCall: Bool = false
let isPictureInPictureSupported: Bool
private(set) var peerTitle: String = ""
private(set) var isVisibleInHierarchy: Bool = false
private var isVisibleInHierarchyDisposable: Disposable?
@ -545,6 +494,10 @@ public final class MediaStreamComponent: CombinedComponent {
strongSelf.canManageCall = state.canManageCall
updated = true
}
if strongSelf.peerTitle != callPeer.debugDisplayTitle {
strongSelf.peerTitle = callPeer.debugDisplayTitle
updated = true
}
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
if strongSelf.originInfo != originInfo {
@ -647,6 +600,8 @@ public final class MediaStreamComponent: CombinedComponent {
call: context.component.call,
hasVideo: context.state.hasVideo,
isVisible: environment.isVisible && context.state.isVisibleInHierarchy,
isAdmin: context.state.canManageCall,
peerTitle: context.state.peerTitle,
activatePictureInPicture: activatePictureInPicture,
bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in
guard let call = call else {
@ -924,7 +879,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
self.context = call.accountContext
self.call = call
super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl))
super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl), navigationBarAppearance: .none)
self.statusBar.statusBarStyle = .White
self.view.disablesInteractiveModalDismiss = true

View File

@ -4,18 +4,24 @@ import ComponentFlow
import ActivityIndicatorComponent
import AccountContext
import AVKit
import MultilineTextComponent
import Display
final class MediaStreamVideoComponent: Component {
let call: PresentationGroupCallImpl
let hasVideo: Bool
let isVisible: Bool
let isAdmin: Bool
let peerTitle: String
let activatePictureInPicture: ActionSlot<Action<Void>>
let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void
init(call: PresentationGroupCallImpl, hasVideo: Bool, isVisible: Bool, activatePictureInPicture: ActionSlot<Action<Void>>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) {
init(call: PresentationGroupCallImpl, hasVideo: Bool, isVisible: Bool, isAdmin: Bool, peerTitle: String, activatePictureInPicture: ActionSlot<Action<Void>>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) {
self.call = call
self.hasVideo = hasVideo
self.isVisible = isVisible
self.isAdmin = isAdmin
self.peerTitle = peerTitle
self.activatePictureInPicture = activatePictureInPicture
self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation
}
@ -30,6 +36,12 @@ final class MediaStreamVideoComponent: Component {
if lhs.isVisible != rhs.isVisible {
return false
}
if lhs.isAdmin != rhs.isAdmin {
return false
}
if lhs.peerTitle != rhs.peerTitle {
return false
}
return true
}
@ -44,33 +56,30 @@ final class MediaStreamVideoComponent: Component {
return State()
}
public final class View: UIView, AVPictureInPictureControllerDelegate, ComponentTaggedView {
public final class View: UIScrollView, AVPictureInPictureControllerDelegate, ComponentTaggedView {
public final class Tag {
}
private let videoRenderingContext = VideoRenderingContext()
private var videoView: VideoRenderingView?
private let blurTintView: UIView
private var videoBlurView: VideoRenderingView?
private var activityIndicatorView: ComponentHostView<Empty>?
private var noSignalView: ComponentHostView<Empty>?
private var pictureInPictureController: AVPictureInPictureController?
private var component: MediaStreamVideoComponent?
private var hadVideo: Bool = false
private var noSignalTimer: Timer?
private var noSignalTimeout: Bool = false
private weak var state: State?
override init(frame: CGRect) {
self.blurTintView = UIView()
self.blurTintView.backgroundColor = UIColor(white: 0.0, alpha: 0.55)
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.clipsToBounds = true
self.addSubview(self.blurTintView)
}
required init?(coder: NSCoder) {
@ -93,48 +102,49 @@ final class MediaStreamVideoComponent: Component {
if component.hasVideo, self.videoView == nil {
if let input = component.call.video(endpointId: "unified") {
if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) {
self.videoBlurView = videoBlurView
self.insertSubview(videoBlurView, belowSubview: self.blurTintView)
}
if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) {
self.videoView = videoView
self.addSubview(videoView)
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported(), let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView {
final class PlaybackDelegateImpl: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
}
func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
return CMTimeRange(start: .zero, duration: .positiveInfinity)
}
func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
completionHandler()
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
if let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView {
if #available(iOS 13.0, *) {
sampleBufferVideoView.sampleBufferLayer.preventsDisplaySleepDuringVideoPlayback = true
}
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: sampleBufferVideoView.sampleBufferLayer, playbackDelegate: PlaybackDelegateImpl()))
pictureInPictureController.delegate = self
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
pictureInPictureController.requiresLinearPlayback = true
self.pictureInPictureController = pictureInPictureController
if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() {
final class PlaybackDelegateImpl: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) {
}
func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange {
return CMTimeRange(start: .zero, duration: .positiveInfinity)
}
func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) {
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) {
completionHandler()
}
public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool {
return false
}
}
let pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: sampleBufferVideoView.sampleBufferLayer, playbackDelegate: PlaybackDelegateImpl()))
pictureInPictureController.delegate = self
pictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true
pictureInPictureController.requiresLinearPlayback = true
self.pictureInPictureController = pictureInPictureController
}
}
videoView.setOnOrientationUpdated { [weak state] _, _ in
@ -146,9 +156,19 @@ final class MediaStreamVideoComponent: Component {
}
strongSelf.hadVideo = true
strongSelf.activityIndicatorView?.removeFromSuperview()
strongSelf.activityIndicatorView = nil
strongSelf.noSignalTimer?.invalidate()
strongSelf.noSignalTimer = nil
strongSelf.noSignalTimeout = false
strongSelf.noSignalView?.removeFromSuperview()
strongSelf.noSignalView = nil
//strongSelf.translatesAutoresizingMaskIntoConstraints = false
//strongSelf.maximumZoomScale = 4.0
state?.updated(transition: .immediate)
}
}
@ -171,15 +191,8 @@ final class MediaStreamVideoComponent: Component {
}
let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(availableSize)
let blurredVideoSize = videoSize.aspectFilled(availableSize)
transition.withAnimation(.none).setFrame(view: videoView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize), completion: nil)
if let videoBlurView = self.videoBlurView {
videoBlurView.updateIsEnabled(component.isVisible)
transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil)
}
}
if !self.hadVideo {
@ -196,11 +209,53 @@ final class MediaStreamVideoComponent: Component {
let activityIndicatorSize = activityIndicatorView.update(
transition: transition,
component: AnyComponent(ActivityIndicatorComponent()),
component: AnyComponent(ActivityIndicatorComponent(color: .white)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - activityIndicatorSize.width) / 2.0), y: floor((availableSize.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize), completion: nil)
let activityIndicatorFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - activityIndicatorSize.width) / 2.0), y: floor((availableSize.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize)
activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: activityIndicatorFrame, completion: nil)
if self.noSignalTimer == nil {
if #available(iOS 10.0, *) {
let noSignalTimer = Timer(timeInterval: 20.0, repeats: false, block: { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.noSignalTimeout = true
strongSelf.state?.updated(transition: .immediate)
})
self.noSignalTimer = noSignalTimer
RunLoop.main.add(noSignalTimer, forMode: .common)
}
}
if self.noSignalTimeout {
var noSignalTransition = transition
let noSignalView: ComponentHostView<Empty>
if let current = self.noSignalView {
noSignalView = current
} else {
noSignalTransition = transition.withAnimation(.none)
noSignalView = ComponentHostView<Empty>()
self.noSignalView = noSignalView
self.addSubview(noSignalView)
noSignalView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with { $0 }
let noSignalSize = noSignalView.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: NSAttributedString(string: component.isAdmin ? presentationData.strings.LiveStream_NoSignalAdminText : presentationData.strings.LiveStream_NoSignalUserText(component.peerTitle).string, font: Font.regular(16.0), textColor: .white, paragraphAlignment: .center),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 1000.0)
)
noSignalTransition.setFrame(view: noSignalView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - noSignalSize.width) / 2.0), y: activityIndicatorFrame.maxY + 24.0), size: noSignalSize), completion: nil)
}
}
self.component = component

View File

@ -273,6 +273,168 @@ private extension PresentationGroupCallState {
}
}
private enum CurrentImpl {
case call(OngoingGroupCallContext)
case mediaStream(WrappedMediaStreamingContext)
}
private extension CurrentImpl {
var joinPayload: Signal<(String, UInt32), NoError> {
switch self {
case let .call(callContext):
return callContext.joinPayload
case .mediaStream:
let ssrcId = UInt32.random(in: 0 ..< UInt32(Int32.max - 1))
let dict: [String: Any] = [
"fingerprints": [],
"ufrag": "",
"pwd": "",
"ssrc": Int32(bitPattern: ssrcId),
"ssrc-groups": []
]
guard let jsonString = (try? JSONSerialization.data(withJSONObject: dict, options: [])).flatMap({ String(data: $0, encoding: .utf8) }) else {
return .never()
}
return .single((jsonString, ssrcId))
}
}
var networkState: Signal<OngoingGroupCallContext.NetworkState, NoError> {
switch self {
case let .call(callContext):
return callContext.networkState
case .mediaStream:
return .single(OngoingGroupCallContext.NetworkState(isConnected: true, isTransitioningFromBroadcastToRtc: false))
}
}
var audioLevels: Signal<[(OngoingGroupCallContext.AudioLevelKey, Float, Bool)], NoError> {
switch self {
case let .call(callContext):
return callContext.audioLevels
case .mediaStream:
return .single([])
}
}
var isMuted: Signal<Bool, NoError> {
switch self {
case let .call(callContext):
return callContext.isMuted
case .mediaStream:
return .single(true)
}
}
var isNoiseSuppressionEnabled: Signal<Bool, NoError> {
switch self {
case let .call(callContext):
return callContext.isNoiseSuppressionEnabled
case .mediaStream:
return .single(false)
}
}
func stop() {
switch self {
case let .call(callContext):
callContext.stop()
case .mediaStream:
break
}
}
func setIsMuted(_ isMuted: Bool) {
switch self {
case let .call(callContext):
callContext.setIsMuted(isMuted)
case .mediaStream:
break
}
}
func setIsNoiseSuppressionEnabled(_ isNoiseSuppressionEnabled: Bool) {
switch self {
case let .call(callContext):
callContext.setIsNoiseSuppressionEnabled(isNoiseSuppressionEnabled)
case .mediaStream:
break
}
}
func requestVideo(_ capturer: OngoingCallVideoCapturer?) {
switch self {
case let .call(callContext):
callContext.requestVideo(capturer)
case .mediaStream:
break
}
}
func disableVideo() {
switch self {
case let .call(callContext):
callContext.disableVideo()
case .mediaStream:
break
}
}
func setVolume(ssrc: UInt32, volume: Double) {
switch self {
case let .call(callContext):
callContext.setVolume(ssrc: ssrc, volume: volume)
case .mediaStream:
break
}
}
func setRequestedVideoChannels(_ channels: [OngoingGroupCallContext.VideoChannel]) {
switch self {
case let .call(callContext):
callContext.setRequestedVideoChannels(channels)
case .mediaStream:
break
}
}
func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (OngoingCallContextPresentationCallVideoView?, OngoingCallContextPresentationCallVideoView?) -> Void) {
switch self {
case let .call(callContext):
callContext.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: completion)
case .mediaStream:
break
}
}
func video(endpointId: String) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
switch self {
case let .call(callContext):
return callContext.video(endpointId: endpointId)
case let .mediaStream(mediaStreamContext):
return mediaStreamContext.video()
}
}
func addExternalAudioData(data: Data) {
switch self {
case let .call(callContext):
callContext.addExternalAudioData(data: data)
case .mediaStream:
break
}
}
func getStats(completion: @escaping (OngoingGroupCallContext.Stats) -> Void) {
switch self {
case let .call(callContext):
callContext.getStats(completion: completion)
case .mediaStream:
break
}
}
}
public final class PresentationGroupCallImpl: PresentationGroupCall {
private enum InternalState {
case requesting
@ -430,7 +592,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private var currentLocalSsrc: UInt32?
private var currentLocalEndpointId: String?
private var genericCallContext: OngoingGroupCallContext?
private var genericCallContext: CurrentImpl?
private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none
private var didInitializeConnectionMode: Bool = false
@ -827,7 +989,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
if !removedSsrc.isEmpty {
strongSelf.genericCallContext?.removeSsrcs(ssrcs: removedSsrc)
if case let .call(callContext) = strongSelf.genericCallContext {
callContext.removeSsrcs(ssrcs: removedSsrc)
}
}
}
})
@ -1411,39 +1575,57 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
if shouldJoin, let callInfo = activeCallInfo {
let genericCallContext: OngoingGroupCallContext
let genericCallContext: CurrentImpl
if let current = self.genericCallContext {
genericCallContext = current
} else {
var outgoingAudioBitrateKbit: Int32?
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Double {
outgoingAudioBitrateKbit = Int32(value)
}
if self.isStream, !"".isEmpty {
genericCallContext = .mediaStream(WrappedMediaStreamingContext(rejoinNeeded: { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if strongSelf.leaving {
return
}
if case .established = strongSelf.internalState {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
}))
} else {
var outgoingAudioBitrateKbit: Int32?
let appConfiguration = self.accountContext.currentAppConfiguration.with({ $0 })
if let data = appConfiguration.data, let value = data["voice_chat_send_bitrate"] as? Double {
outgoingAudioBitrateKbit = Int32(value)
}
genericCallContext = OngoingGroupCallContext(video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
let disposable = MetaDisposable()
Queue.mainQueue().async {
guard let strongSelf = self else {
return
genericCallContext = .call(OngoingGroupCallContext(video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in
let disposable = MetaDisposable()
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
disposable.set(strongSelf.requestMediaChannelDescriptions(ssrcs: ssrcs, completion: completion))
}
disposable.set(strongSelf.requestMediaChannelDescriptions(ssrcs: ssrcs, completion: completion))
}
return disposable
}, rejoinNeeded: { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
return disposable
}, rejoinNeeded: { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if case .established = strongSelf.internalState {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
if case .established = strongSelf.internalState {
strongSelf.requestCall(movingFromBroadcastToRtc: false)
}
}
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264")
}, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: self.isVideoEnabled ? .generic : .none, enableNoiseSuppression: false, disableAudioInput: self.isStream, preferX264: self.accountContext.sharedContext.immediateExperimentalUISettings.preferredVideoCodec == "H264"
))
}
self.genericCallContext = genericCallContext
self.stateVersionValue += 1
}
self.joinDisposable.set((genericCallContext.joinPayload
|> distinctUntilChanged(isEqual: { lhs, rhs in
if lhs.0 != rhs.0 {
@ -1528,15 +1710,28 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
}
switch joinCallResult.connectionMode {
case .rtc:
strongSelf.currentConnectionMode = .rtc
strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
strongSelf.genericCallContext?.setJoinResponse(payload: clientParams)
case .broadcast:
strongSelf.currentConnectionMode = .broadcast
strongSelf.genericCallContext?.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream))
strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: callInfo.isStream)
if let genericCallContext = strongSelf.genericCallContext {
switch genericCallContext {
case let .call(callContext):
switch joinCallResult.connectionMode {
case .rtc:
strongSelf.currentConnectionMode = .rtc
callContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: false)
callContext.setJoinResponse(payload: clientParams)
case .broadcast:
strongSelf.currentConnectionMode = .broadcast
callContext.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream))
callContext.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false, isUnifiedBroadcast: callInfo.isStream)
}
case let .mediaStream(mediaStreamContext):
switch joinCallResult.connectionMode {
case .rtc:
strongSelf.currentConnectionMode = .rtc
case .broadcast:
strongSelf.currentConnectionMode = .broadcast
mediaStreamContext.setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: strongSelf.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash, isExternalStream: callInfo.isStream))
}
}
}
strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state), audioSessionControl: strongSelf.audioSessionControl)
@ -2952,7 +3147,15 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
if !self.didInitializeConnectionMode || self.currentConnectionMode != .none {
self.didInitializeConnectionMode = true
self.currentConnectionMode = .none
self.genericCallContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false)
if let genericCallContext = self.genericCallContext {
switch genericCallContext {
case let .call(callContext):
callContext.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc, isUnifiedBroadcast: false)
case .mediaStream:
assertionFailure()
break
}
}
}
self.internalState = .requesting

View File

@ -265,6 +265,7 @@ swift_library(
"//submodules/ChatTextLinkEditUI:ChatTextLinkEditUI",
"//submodules/MediaPickerUI:MediaPickerUI",
"//submodules/ChatMessageBackground:ChatMessageBackground",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -65,6 +65,7 @@ import TooltipUI
import QrCodeUI
import Translate
import ChatPresentationInterfaceState
import CreateExternalMediaStreamScreen
protocol PeerInfoScreenItem: AnyObject {
var id: AnyHashable { get }
@ -4106,6 +4107,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self.context.scheduleGroupCall(peerId: self.peerId)
}
private func createExternalStream(credentialsPromise: Promise<GroupCallStreamCredentials>?) {
self.controller?.push(CreateExternalMediaStreamScreen(context: self.context, peerId: self.peerId, credentialsPromise: credentialsPromise))
}
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
if let _ = self.context.sharedContext.callManager {
let startCall: (Bool) -> Void = { [weak self] endCurrentIfAny in
@ -4113,12 +4118,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
return
}
#if DEBUG
let isExternalStream: Bool = true
#else
let isExternalStream: Bool = false
#endif
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
@ -4135,7 +4134,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: nil, isExternalStream: isExternalStream)
let createSignal = strongSelf.context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: nil, isExternalStream: false)
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
@ -4467,6 +4466,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
}
private func openVoiceChatOptions(defaultJoinAsPeerId: PeerId?, gesture: ContextGesture? = nil, contextController: ContextControllerProtocol? = nil) {
guard let chatPeer = self.data?.peer else {
return
}
let context = self.context
let peerId = self.peerId
let defaultJoinAsPeerId = defaultJoinAsPeerId ?? self.context.account.peerId
@ -4534,6 +4536,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
self?.scheduleGroupCall()
})))
var credentialsPromise: Promise<GroupCallStreamCredentials>?
var canCreateStream = false
switch chatPeer {
case let group as TelegramGroup:
if case .creator = group.role {
canCreateStream = true
}
case let channel as TelegramChannel:
if channel.flags.contains(.isCreator) {
canCreateStream = true
credentialsPromise = Promise()
credentialsPromise?.set(context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
}
default:
break
}
if canCreateStream {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChannelInfo_CreateExternalStream, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VoiceChat"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.dismissWithoutContent)
self?.createExternalStream(credentialsPromise: credentialsPromise)
})))
}
if let contextController = contextController {
contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil)
} else {

View File

@ -2,8 +2,9 @@ import Foundation
import SwiftSignalKit
import TgVoipWebrtc
import TelegramCore
import Postbox
private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc {
final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueueWebrtc {
private let queue: Queue
init(queue: Queue) {
@ -27,17 +28,17 @@ private final class ContextQueueImpl: NSObject, OngoingCallThreadLocalContextQue
}
}
private enum BroadcastPartSubject {
enum BroadcastPartSubject {
case audio
case video(channelId: Int32, quality: OngoingGroupCallContext.VideoChannel.Quality)
}
private protocol BroadcastPartSource: AnyObject {
protocol BroadcastPartSource: AnyObject {
func requestTime(completion: @escaping (Int64) -> Void) -> Disposable
func requestPart(timestampMilliseconds: Int64, durationMilliseconds: Int64, subject: BroadcastPartSubject, completion: @escaping (OngoingGroupCallBroadcastPart) -> Void, rejoinNeeded: @escaping () -> Void) -> Disposable
}
private final class NetworkBroadcastPartSource: BroadcastPartSource {
final class NetworkBroadcastPartSource: BroadcastPartSource {
private let queue: Queue
private let engine: TelegramEngine
private let callId: Int64
@ -45,6 +46,10 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource {
private let isExternalStream: Bool
private var dataSource: AudioBroadcastDataSource?
#if DEBUG
private let debugDumpDirectory = TempBox.shared.tempDirectory()
#endif
init(queue: Queue, engine: TelegramEngine, callId: Int64, accessHash: Int64, isExternalStream: Bool) {
self.queue = queue
self.engine = engine
@ -139,6 +144,9 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource {
}
|> deliverOn(self.queue)
#if DEBUG
let debugDumpDirectory = self.debugDumpDirectory
#endif
return signal.start(next: { result in
guard let result = result else {
completion(OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: Double(timestampIdMilliseconds), status: .notReady, oggData: Data()))
@ -147,11 +155,11 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource {
let part: OngoingGroupCallBroadcastPart
switch result.status {
case let .data(dataValue):
/*#if DEBUG
let tempFile = EngineTempBox.shared.tempFile(fileName: "part.mp4")
let _ = try? dataValue.write(to: URL(fileURLWithPath: tempFile.path))
print("Dump stream part: \(tempFile.path)")
#endif*/
#if DEBUG
let tempFilePath = debugDumpDirectory.path + "/\(timestampMilliseconds).mp4"
let _ = try? dataValue.subdata(in: 32 ..< dataValue.count).write(to: URL(fileURLWithPath: tempFilePath))
print("Dump stream part: \(tempFilePath)")
#endif
part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .success, oggData: dataValue)
case .notReady:
part = OngoingGroupCallBroadcastPart(timestampMilliseconds: timestampIdMilliseconds, responseTimestamp: result.responseTimestamp, status: .notReady, oggData: Data())
@ -167,7 +175,7 @@ private final class NetworkBroadcastPartSource: BroadcastPartSource {
}
}
private final class OngoingGroupCallBroadcastPartTaskImpl : NSObject, OngoingGroupCallBroadcastPartTask {
final class OngoingGroupCallBroadcastPartTaskImpl: NSObject, OngoingGroupCallBroadcastPartTask {
private let disposable: Disposable?
init(disposable: Disposable?) {
@ -209,6 +217,11 @@ public final class OngoingGroupCallContext {
public struct NetworkState: Equatable {
public var isConnected: Bool
public var isTransitioningFromBroadcastToRtc: Bool
public init(isConnected: Bool, isTransitioningFromBroadcastToRtc: Bool) {
self.isConnected = isConnected
self.isTransitioningFromBroadcastToRtc = isTransitioningFromBroadcastToRtc
}
}
public enum AudioLevelKey: Hashable {

View File

@ -0,0 +1,134 @@
import Foundation
import SwiftSignalKit
import TgVoipWebrtc
import TelegramCore
public final class WrappedMediaStreamingContext {
private final class Impl {
let queue: Queue
let context: MediaStreamingContext
private let broadcastPartsSource = Atomic<BroadcastPartSource?>(value: nil)
init(queue: Queue, rejoinNeeded: @escaping () -> Void) {
self.queue = queue
var getBroadcastPartsSource: (() -> BroadcastPartSource?)?
self.context = MediaStreamingContext(
queue: ContextQueueImpl(queue: queue),
requestCurrentTime: { completion in
let disposable = MetaDisposable()
queue.async {
if let source = getBroadcastPartsSource?() {
disposable.set(source.requestTime(completion: completion))
} else {
completion(0)
}
}
return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable)
},
requestAudioBroadcastPart: { timestampMilliseconds, durationMilliseconds, completion in
let disposable = MetaDisposable()
queue.async {
disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .audio, completion: completion, rejoinNeeded: {
rejoinNeeded()
}))
}
return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable)
},
requestVideoBroadcastPart: { timestampMilliseconds, durationMilliseconds, channelId, quality, completion in
let disposable = MetaDisposable()
queue.async {
let mappedQuality: OngoingGroupCallContext.VideoChannel.Quality
switch quality {
case .thumbnail:
mappedQuality = .thumbnail
case .medium:
mappedQuality = .medium
case .full:
mappedQuality = .full
@unknown default:
mappedQuality = .thumbnail
}
disposable.set(getBroadcastPartsSource?()?.requestPart(timestampMilliseconds: timestampMilliseconds, durationMilliseconds: durationMilliseconds, subject: .video(channelId: channelId, quality: mappedQuality), completion: completion, rejoinNeeded: {
rejoinNeeded()
}))
}
return OngoingGroupCallBroadcastPartTaskImpl(disposable: disposable)
}
)
let broadcastPartsSource = self.broadcastPartsSource
getBroadcastPartsSource = {
return broadcastPartsSource.with { $0 }
}
}
deinit {
}
func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
if let audioStreamData = audioStreamData {
let broadcastPartsSource = NetworkBroadcastPartSource(queue: self.queue, engine: audioStreamData.engine, callId: audioStreamData.callId, accessHash: audioStreamData.accessHash, isExternalStream: audioStreamData.isExternalStream)
let _ = self.broadcastPartsSource.swap(broadcastPartsSource)
self.context.start()
}
}
func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
let queue = self.queue
return Signal { [weak self] subscriber in
let disposable = MetaDisposable()
queue.async {
guard let strongSelf = self else {
return
}
let innerDisposable = strongSelf.context.addVideoOutput() { videoFrameData in
subscriber.putNext(OngoingGroupCallContext.VideoFrameData(frameData: videoFrameData))
}
disposable.set(ActionDisposable {
innerDisposable.dispose()
})
}
return disposable
}
}
}
private let queue = Queue()
private let impl: QueueLocalObject<Impl>
public init(rejoinNeeded: @escaping () -> Void) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, rejoinNeeded: rejoinNeeded)
})
}
public func setAudioStreamData(audioStreamData: OngoingGroupCallContext.AudioStreamData?) {
self.impl.with { impl in
impl.setAudioStreamData(audioStreamData: audioStreamData)
}
}
public func video() -> Signal<OngoingGroupCallContext.VideoFrameData, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.video().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
}

View File

@ -0,0 +1,23 @@
#ifndef TgVoipWebrtc_MediaStreaming_h
#define TgVoipWebrtc_MediaStreaming_h
#import <Foundation/Foundation.h>
#import <TgVoipWebrtc/OngoingCallThreadLocalContext.h>
@interface MediaStreamingContext : NSObject
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue
requestCurrentTime:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(void (^ _Nonnull)(int64_t)))requestAudioBroadcastPart
requestAudioBroadcastPart:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestAudioBroadcastPart
requestVideoBroadcastPart:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(int64_t, int64_t, int32_t, OngoingGroupCallRequestedVideoQuality, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestVideoBroadcastPart;
- (void)start;
- (void)stop;
- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink;
- (void)getAudio:(int16_t * _Nonnull)audioSamples numSamples:(NSInteger)numSamples numChannels:(NSInteger)numChannels samplesPerSecond:(NSInteger)samplesPerSecond;
@end
#endif

View File

@ -112,6 +112,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
@interface GroupCallDisposable : NSObject
- (instancetype _Nonnull)initWithBlock:(dispatch_block_t _Nonnull)block;
- (void)dispose;
@end

View File

@ -0,0 +1,273 @@
#import <TgVoipWebrtc/MediaStreaming.h>
#import "MediaUtils.h"
#include "StaticThreads.h"
#include "group/StreamingMediaContext.h"
#include "api/video/video_sink_interface.h"
#include "sdk/objc/native/src/objc_frame_buffer.h"
#include "api/video/video_frame.h"
#import "components/video_frame_buffer/RTCCVPixelBuffer.h"
#import "platform/darwin/TGRTCCVPixelBuffer.h"
#include <memory>
namespace {
class BroadcastPartTaskImpl : public tgcalls::BroadcastPartTask {
public:
BroadcastPartTaskImpl(id<OngoingGroupCallBroadcastPartTask> task) {
_task = task;
}
virtual ~BroadcastPartTaskImpl() {
}
virtual void cancel() override {
[_task cancel];
}
private:
id<OngoingGroupCallBroadcastPartTask> _task;
};
class VideoSinkAdapter : public rtc::VideoSinkInterface<webrtc::VideoFrame> {
public:
VideoSinkAdapter(void (^frameReceived)(webrtc::VideoFrame const &)) {
_frameReceived = [frameReceived copy];
}
void OnFrame(const webrtc::VideoFrame& nativeVideoFrame) override {
@autoreleasepool {
if (_frameReceived) {
_frameReceived(nativeVideoFrame);
}
}
}
private:
void (^_frameReceived)(webrtc::VideoFrame const &);
};
}
@interface MediaStreamingVideoSink : NSObject {
std::shared_ptr<VideoSinkAdapter> _adapter;
}
@end
@implementation MediaStreamingVideoSink
- (instancetype)initWithSink:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink {
self = [super init];
if (self != nil) {
void (^storedSink)(CallVideoFrameData * _Nonnull) = [sink copy];
_adapter.reset(new VideoSinkAdapter(^(webrtc::VideoFrame const &videoFrame) {
id<CallVideoFrameBuffer> mappedBuffer = nil;
bool mirrorHorizontally = false;
bool mirrorVertically = false;
if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kNative) {
id<RTC_OBJC_TYPE(RTCVideoFrameBuffer)> nativeBuffer = static_cast<webrtc::ObjCFrameBuffer *>(videoFrame.video_frame_buffer().get())->wrapped_frame_buffer();
if ([nativeBuffer isKindOfClass:[RTC_OBJC_TYPE(RTCCVPixelBuffer) class]]) {
RTCCVPixelBuffer *pixelBuffer = (RTCCVPixelBuffer *)nativeBuffer;
mappedBuffer = [[CallVideoFrameNativePixelBuffer alloc] initWithPixelBuffer:pixelBuffer.pixelBuffer];
}
if ([nativeBuffer isKindOfClass:[TGRTCCVPixelBuffer class]]) {
if (((TGRTCCVPixelBuffer *)nativeBuffer).shouldBeMirrored) {
switch (videoFrame.rotation()) {
case webrtc::kVideoRotation_0:
case webrtc::kVideoRotation_180:
mirrorHorizontally = true;
break;
case webrtc::kVideoRotation_90:
case webrtc::kVideoRotation_270:
mirrorVertically = true;
break;
default:
break;
}
}
}
} else if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kNV12) {
rtc::scoped_refptr<webrtc::NV12BufferInterface> nv12Buffer = (webrtc::NV12BufferInterface *)videoFrame.video_frame_buffer().get();
mappedBuffer = [[CallVideoFrameNV12Buffer alloc] initWithBuffer:nv12Buffer];
} else if (videoFrame.video_frame_buffer()->type() == webrtc::VideoFrameBuffer::Type::kI420) {
rtc::scoped_refptr<webrtc::I420BufferInterface> i420Buffer = (webrtc::I420BufferInterface *)videoFrame.video_frame_buffer().get();
mappedBuffer = [[CallVideoFrameI420Buffer alloc] initWithBuffer:i420Buffer];
}
if (storedSink && mappedBuffer) {
storedSink([[CallVideoFrameData alloc] initWithBuffer:mappedBuffer frame:videoFrame mirrorHorizontally:mirrorHorizontally mirrorVertically:mirrorVertically]);
}
}));
}
return self;
}
- (std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>>)sink {
return _adapter;
}
@end
@interface MediaStreamingContext () {
id<OngoingCallThreadLocalContextQueueWebrtc> _queue;
id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull _requestCurrentTime)(void (^ _Nonnull)(int64_t));
id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull _requestAudioBroadcastPart)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable));
id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull _requestVideoBroadcastPart)(int64_t, int64_t, int32_t, OngoingGroupCallRequestedVideoQuality, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable));
std::unique_ptr<tgcalls::StreamingMediaContext> _context;
int _nextSinkId;
NSMutableDictionary<NSNumber *, MediaStreamingVideoSink *> *_sinks;
}
@end
@implementation MediaStreamingContext
- (instancetype _Nonnull)initWithQueue:(id<OngoingCallThreadLocalContextQueueWebrtc> _Nonnull)queue
requestCurrentTime:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(void (^ _Nonnull)(int64_t)))requestCurrentTime
requestAudioBroadcastPart:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(int64_t, int64_t, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestAudioBroadcastPart
requestVideoBroadcastPart:(id<OngoingGroupCallBroadcastPartTask> _Nonnull (^ _Nonnull)(int64_t, int64_t, int32_t, OngoingGroupCallRequestedVideoQuality, void (^ _Nonnull)(OngoingGroupCallBroadcastPart * _Nullable)))requestVideoBroadcastPart {
self = [super init];
if (self != nil) {
_queue = queue;
_requestCurrentTime = [requestCurrentTime copy];
_requestAudioBroadcastPart = [requestAudioBroadcastPart copy];
_requestVideoBroadcastPart = [requestVideoBroadcastPart copy];
_sinks = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
}
- (void)resetContext {
tgcalls::StreamingMediaContext::StreamingMediaContextArguments arguments;
arguments.threads = tgcalls::StaticThreads::getThreads();
arguments.isUnifiedBroadcast = true;
arguments.requestCurrentTime = [requestCurrentTime = _requestCurrentTime](std::function<void(int64_t)> completion) -> std::shared_ptr<tgcalls::BroadcastPartTask> {
id<OngoingGroupCallBroadcastPartTask> task = requestCurrentTime(^(int64_t result) {
completion(result);
});
return std::make_shared<BroadcastPartTaskImpl>(task);
};
arguments.requestAudioBroadcastPart = nullptr;
arguments.requestVideoBroadcastPart = [requestVideoBroadcastPart = _requestVideoBroadcastPart](int64_t timestampMilliseconds, int64_t durationMilliseconds, int32_t channelId, tgcalls::VideoChannelDescription::Quality quality, std::function<void(tgcalls::BroadcastPart &&)> completion) -> std::shared_ptr<tgcalls::BroadcastPartTask> {
OngoingGroupCallRequestedVideoQuality mappedQuality;
switch (quality) {
case tgcalls::VideoChannelDescription::Quality::Thumbnail: {
mappedQuality = OngoingGroupCallRequestedVideoQualityThumbnail;
break;
}
case tgcalls::VideoChannelDescription::Quality::Medium: {
mappedQuality = OngoingGroupCallRequestedVideoQualityMedium;
break;
}
case tgcalls::VideoChannelDescription::Quality::Full: {
mappedQuality = OngoingGroupCallRequestedVideoQualityFull;
break;
}
default: {
mappedQuality = OngoingGroupCallRequestedVideoQualityThumbnail;
break;
}
}
id<OngoingGroupCallBroadcastPartTask> task = requestVideoBroadcastPart(timestampMilliseconds, durationMilliseconds, channelId, mappedQuality, ^(OngoingGroupCallBroadcastPart * _Nullable part) {
tgcalls::BroadcastPart parsedPart;
parsedPart.timestampMilliseconds = part.timestampMilliseconds;
parsedPart.responseTimestamp = part.responseTimestamp;
tgcalls::BroadcastPart::Status mappedStatus;
switch (part.status) {
case OngoingGroupCallBroadcastPartStatusSuccess: {
mappedStatus = tgcalls::BroadcastPart::Status::Success;
break;
}
case OngoingGroupCallBroadcastPartStatusNotReady: {
mappedStatus = tgcalls::BroadcastPart::Status::NotReady;
break;
}
case OngoingGroupCallBroadcastPartStatusResyncNeeded: {
mappedStatus = tgcalls::BroadcastPart::Status::ResyncNeeded;
break;
}
default: {
mappedStatus = tgcalls::BroadcastPart::Status::NotReady;
break;
}
}
parsedPart.status = mappedStatus;
parsedPart.data.resize(part.oggData.length);
[part.oggData getBytes:parsedPart.data.data() length:part.oggData.length];
completion(std::move(parsedPart));
});
return std::make_shared<BroadcastPartTaskImpl>(task);
};
arguments.updateAudioLevel = nullptr;
_context = std::make_unique<tgcalls::StreamingMediaContext>(std::move(arguments));
for (MediaStreamingVideoSink *storedSink in _sinks.allValues) {
_context->addVideoSink("unified", [storedSink sink]);
}
}
- (void)start {
[self resetContext];
}
- (void)stop {
_context.reset();
}
- (GroupCallDisposable * _Nonnull)addVideoOutput:(void (^_Nonnull)(CallVideoFrameData * _Nonnull))sink {
int sinkId = _nextSinkId;
_nextSinkId += 1;
MediaStreamingVideoSink *storedSink = [[MediaStreamingVideoSink alloc] initWithSink:sink];
_sinks[@(sinkId)] = storedSink;
if (_context) {
_context->addVideoSink("unified", [storedSink sink]);
}
__weak MediaStreamingContext *weakSelf = self;
id<OngoingCallThreadLocalContextQueueWebrtc> queue = _queue;
return [[GroupCallDisposable alloc] initWithBlock:^{
[queue dispatch:^{
__strong MediaStreamingContext *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf->_sinks removeObjectForKey:@(sinkId)];
}];
}];
}
- (void)getAudio:(int16_t * _Nonnull)audioSamples numSamples:(NSInteger)numSamples numChannels:(NSInteger)numChannels samplesPerSecond:(NSInteger)samplesPerSecond {
if (_context) {
_context->getAudio(audioSamples, (size_t)numSamples, (size_t)numChannels, (uint32_t)samplesPerSecond);
} else {
memset(audioSamples, 0, numSamples * numChannels * sizeof(int16_t));
}
}
@end

View File

@ -0,0 +1,58 @@
#import <TgVoipWebrtc/OngoingCallThreadLocalContext.h>
#import "Instance.h"
#import "InstanceImpl.h"
#import "v2/InstanceV2Impl.h"
#include "StaticThreads.h"
#import "VideoCaptureInterface.h"
#import "platform/darwin/VideoCameraCapturer.h"
#ifndef WEBRTC_IOS
#import "platform/darwin/VideoMetalViewMac.h"
#import "platform/darwin/GLVideoViewMac.h"
#import "platform/darwin/VideoSampleBufferViewMac.h"
#define UIViewContentModeScaleAspectFill kCAGravityResizeAspectFill
#define UIViewContentModeScaleAspect kCAGravityResizeAspect
#else
#import "platform/darwin/VideoMetalView.h"
#import "platform/darwin/GLVideoView.h"
#import "platform/darwin/VideoSampleBufferView.h"
#import "platform/darwin/VideoCaptureView.h"
#import "platform/darwin/CustomExternalCapturer.h"
#endif
#import "group/GroupInstanceImpl.h"
#import "group/GroupInstanceCustomImpl.h"
#import "VideoCaptureInterfaceImpl.h"
#include "sdk/objc/native/src/objc_frame_buffer.h"
#import "components/video_frame_buffer/RTCCVPixelBuffer.h"
#import "platform/darwin/TGRTCCVPixelBuffer.h"
@interface CallVideoFrameNativePixelBuffer (Initialization)
- (instancetype _Nonnull)initWithPixelBuffer:(CVPixelBufferRef _Nonnull)pixelBuffer;
@end
@interface CallVideoFrameI420Buffer (Initialization)
- (instancetype _Nonnull)initWithBuffer:(rtc::scoped_refptr<webrtc::I420BufferInterface>)i420Buffer;
@end
@interface CallVideoFrameNV12Buffer (Initialization)
- (instancetype _Nonnull)initWithBuffer:(rtc::scoped_refptr<webrtc::NV12BufferInterface>)nv12Buffer;
@end
@interface CallVideoFrameData (Initialization)
- (instancetype _Nonnull)initWithBuffer:(id<CallVideoFrameBuffer> _Nonnull)buffer frame:(webrtc::VideoFrame const &)frame mirrorHorizontally:(bool)mirrorHorizontally mirrorVertically:(bool)mirrorVertically;
@end

View File

@ -1,5 +1,6 @@
#import <TgVoipWebrtc/OngoingCallThreadLocalContext.h>
#import "MediaUtils.h"
#import "Instance.h"
#import "InstanceImpl.h"

@ -1 +1 @@
Subproject commit d5d8fc5467d490319572fbccd864fa6bd78b7877
Subproject commit 4f3f4025b9b4ad9662612636af10e6fd5d204535