mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Media-related improvements
This commit is contained in:
parent
b82b1e41e0
commit
fe0311b1e9
@ -153,6 +153,17 @@ public struct Transition {
|
||||
return result
|
||||
}
|
||||
|
||||
public func withAnimationIfAnimated(_ animation: Animation) -> Transition {
|
||||
switch self.animation {
|
||||
case .none:
|
||||
return self
|
||||
default:
|
||||
var result = self
|
||||
result.animation = animation
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public static var immediate: Transition = Transition(animation: .none)
|
||||
|
||||
public static func easeInOut(duration: Double) -> Transition {
|
||||
|
@ -12,12 +12,16 @@ public final class Action<Arguments> {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ActionSlot<Arguments> {
|
||||
public final class ActionSlot<Arguments>: Equatable {
|
||||
private var target: ((Arguments) -> Void)?
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
public static func ==(lhs: ActionSlot<Arguments>, rhs: ActionSlot<Arguments>) -> Bool {
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
public func connect(_ target: @escaping (Arguments) -> Void) {
|
||||
self.target = target
|
||||
}
|
||||
|
18
submodules/Components/UndoPanelComponent/BUILD
Normal file
18
submodules/Components/UndoPanelComponent/BUILD
Normal file
@ -0,0 +1,18 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "UndoPanelComponent",
|
||||
module_name = "UndoPanelComponent",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,67 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
|
||||
public final class UndoPanelComponent: Component {
|
||||
public let icon: AnyComponent<Empty>?
|
||||
public let content: AnyComponent<Empty>
|
||||
public let action: AnyComponent<Empty>?
|
||||
|
||||
public init(
|
||||
icon: AnyComponent<Empty>?,
|
||||
content: AnyComponent<Empty>,
|
||||
action: AnyComponent<Empty>?
|
||||
) {
|
||||
self.icon = icon
|
||||
self.content = content
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public static func ==(lhs: UndoPanelComponent, rhs: UndoPanelComponent) -> Bool {
|
||||
if lhs.icon != rhs.icon {
|
||||
return false
|
||||
}
|
||||
if lhs.content !== rhs.content {
|
||||
return false
|
||||
}
|
||||
if lhs.action != rhs.action {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIVisualEffectView {
|
||||
private var iconView: ComponentHostView<Empty>?
|
||||
private let centralContentView: ComponentHostView<Empty>
|
||||
private var actionView: ComponentHostView<Empty>?
|
||||
|
||||
init() {
|
||||
self.centralContentView = ComponentHostView()
|
||||
|
||||
super.init(effect: nil)
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func update(component: UndoPanelComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.effect = UIBlurEffect(style: .dark)
|
||||
|
||||
self.layer.cornerRadius = 10.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: 50.0)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ComponentFlow
|
||||
|
||||
public final class UndoPanelContainerComponent: Component {
|
||||
let push: ActionSlot<UndoPanelComponent>
|
||||
|
||||
public init(push: ActionSlot<UndoPanelComponent>) {
|
||||
self.push = push
|
||||
}
|
||||
|
||||
public static func ==(lhs: UndoPanelContainerComponent, rhs: UndoPanelContainerComponent) -> Bool {
|
||||
if lhs.push != rhs.push {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var topPanel: UndoPanelComponent?
|
||||
private var topPanelView: ComponentHostView<Empty>?
|
||||
|
||||
private var nextPanel: UndoPanelComponent?
|
||||
|
||||
public func update(component: UndoPanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, transition: Transition) -> CGSize {
|
||||
component.push.connect { [weak self, weak state] panel in
|
||||
guard let strongSelf = self, let state = state else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.nextPanel = panel
|
||||
state.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
}
|
||||
|
||||
var animateTopPanelIn = false
|
||||
var topPanelTransition = transition
|
||||
if let nextPanel = self.nextPanel {
|
||||
self.nextPanel = nil
|
||||
self.topPanel = nextPanel
|
||||
|
||||
if let topPanelView = self.topPanelView {
|
||||
self.topPanelView = nil
|
||||
|
||||
transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut)).setAlpha(view: topPanelView, alpha: 0.0, completion: { [weak topPanelView] _ in
|
||||
topPanelView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
let topPanelView = ComponentHostView<Empty>()
|
||||
self.topPanelView = topPanelView
|
||||
self.addSubview(topPanelView)
|
||||
|
||||
topPanelTransition = topPanelTransition.withAnimation(.none)
|
||||
animateTopPanelIn = true
|
||||
}
|
||||
|
||||
if let topPanel = self.topPanel, let topPanelView = self.topPanelView {
|
||||
let topPanelSize = topPanelView.update(
|
||||
transition: topPanelTransition,
|
||||
component: AnyComponent(topPanel),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if animateTopPanelIn {
|
||||
let _ = transition.withAnimationIfAnimated(.curve(duration: 0.3, curve: .easeInOut))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: topPanelSize.height)
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
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, state: state, transition: transition)
|
||||
}
|
||||
}
|
@ -101,17 +101,19 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
private weak var controller: ViewControllerComponentContainer?
|
||||
|
||||
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
|
||||
private let theme: PresentationTheme?
|
||||
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
|
||||
|
||||
private var currentIsVisible: Bool = false
|
||||
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||
|
||||
init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>) {
|
||||
init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent<ViewControllerComponentContainer.Environment>, theme: PresentationTheme?) {
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.controller = controller
|
||||
|
||||
self.component = component
|
||||
self.theme = theme
|
||||
self.hostView = ComponentHostView()
|
||||
|
||||
super.init()
|
||||
@ -127,7 +129,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
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,
|
||||
theme: self.theme ?? self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
controller: { [weak self] in
|
||||
return self?.controller
|
||||
@ -162,11 +164,13 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let theme: PresentationTheme?
|
||||
private let component: AnyComponent<ViewControllerComponentContainer.Environment>
|
||||
|
||||
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
|
||||
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
|
||||
self.context = context
|
||||
self.component = AnyComponent(component)
|
||||
self.theme = theme
|
||||
|
||||
let navigationBarPresentationData: NavigationBarPresentationData?
|
||||
switch navigationBarAppearance {
|
||||
@ -185,7 +189,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
}
|
||||
|
||||
override open func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, component: self.component)
|
||||
self.displayNode = Node(context: self.context, controller: self, component: self.component, theme: self.theme)
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
@ -19,11 +19,13 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
|
||||
let context: AccountContext
|
||||
let peerId: EnginePeer.Id
|
||||
let mode: CreateExternalMediaStreamScreen.Mode
|
||||
let credentialsPromise: Promise<GroupCallStreamCredentials>?
|
||||
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.mode = mode
|
||||
self.credentialsPromise = credentialsPromise
|
||||
}
|
||||
|
||||
@ -34,6 +36,9 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
if lhs.peerId != rhs.peerId {
|
||||
return false
|
||||
}
|
||||
if lhs.mode != rhs.mode {
|
||||
return false
|
||||
}
|
||||
if lhs.credentialsPromise !== rhs.credentialsPromise {
|
||||
return false
|
||||
}
|
||||
@ -180,6 +185,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
|
||||
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let state = context.state
|
||||
let mode = context.component.mode
|
||||
let controller = environment.controller
|
||||
|
||||
let bottomInset: CGFloat
|
||||
@ -218,20 +224,22 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
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 bottomText = Condition(mode == .create) {
|
||||
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,
|
||||
title: mode == .create ? environment.strings.CreateExternalStream_StartStreaming : environment.strings.Common_Close,
|
||||
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
@ -243,9 +251,14 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
return
|
||||
}
|
||||
|
||||
state.createAndJoinGroupCall(baseController: controller, completion: { [weak controller] in
|
||||
controller?.dismiss()
|
||||
})
|
||||
switch mode {
|
||||
case .create:
|
||||
state.createAndJoinGroupCall(baseController: controller, completion: { [weak controller] in
|
||||
controller?.dismiss()
|
||||
})
|
||||
case .view:
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0),
|
||||
@ -396,9 +409,11 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
|
||||
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))
|
||||
)
|
||||
if let bottomText = bottomText {
|
||||
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))
|
||||
@ -410,19 +425,32 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
|
||||
}
|
||||
|
||||
public final class CreateExternalMediaStreamScreen: ViewControllerComponentContainer {
|
||||
public enum Mode {
|
||||
case create
|
||||
case view
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: EnginePeer.Id
|
||||
private let mode: Mode
|
||||
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?) {
|
||||
public init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise<GroupCallStreamCredentials>?, mode: Mode) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.mode = mode
|
||||
|
||||
super.init(context: context, component: CreateExternalMediaStreamScreenComponent(context: context, peerId: peerId, credentialsPromise: credentialsPromise), navigationBarAppearance: .transparent)
|
||||
super.init(context: context, component: CreateExternalMediaStreamScreenComponent(context: context, peerId: peerId, mode: mode, credentialsPromise: credentialsPromise), navigationBarAppearance: .transparent, theme: defaultDarkPresentationTheme)
|
||||
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.title = presentationData.strings.CreateExternalStream_Title
|
||||
switch mode {
|
||||
case .create:
|
||||
self.title = presentationData.strings.CreateExternalStream_Title
|
||||
case .view:
|
||||
//TODO:localize
|
||||
self.title = "Stream Key"
|
||||
}
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
|
@ -101,6 +101,9 @@ swift_library(
|
||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/UndoPanelComponent:UndoPanelComponent",
|
||||
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
|
||||
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,10 +10,14 @@ import Postbox
|
||||
import ShareController
|
||||
import UndoUI
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import LottieAnimationComponent
|
||||
import ContextUI
|
||||
import ViewControllerComponent
|
||||
import BundleIconComponent
|
||||
import CreateExternalMediaStreamScreen
|
||||
import HierarchyTrackingLayer
|
||||
import UndoPanelComponent
|
||||
|
||||
final class NavigationBackButtonComponent: Component {
|
||||
let text: String
|
||||
@ -99,6 +103,116 @@ final class NavigationBackButtonComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
final class StreamTitleComponent: Component {
|
||||
let text: String
|
||||
let isRecording: Bool
|
||||
|
||||
init(text: String, isRecording: Bool) {
|
||||
self.text = text
|
||||
self.isRecording = isRecording
|
||||
}
|
||||
|
||||
static func ==(lhs: StreamTitleComponent, rhs: StreamTitleComponent) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.isRecording != rhs.isRecording {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let textView: ComponentHostView<Empty>
|
||||
private var indicatorView: UIImageView?
|
||||
|
||||
private let trackingLayer: HierarchyTrackingLayer
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.textView = ComponentHostView<Empty>()
|
||||
|
||||
self.trackingLayer = HierarchyTrackingLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.textView)
|
||||
|
||||
self.trackingLayer.didEnterHierarchy = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateIndicatorAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateIndicatorAnimation() {
|
||||
guard let indicatorView = self.indicatorView else {
|
||||
return
|
||||
}
|
||||
if indicatorView.layer.animation(forKey: "blink") == nil {
|
||||
let animation = CAKeyframeAnimation(keyPath: "opacity")
|
||||
animation.values = [1.0 as NSNumber, 1.0 as NSNumber, 0.55 as NSNumber]
|
||||
animation.keyTimes = [0.0 as NSNumber, 0.4546 as NSNumber, 0.9091 as NSNumber, 1 as NSNumber]
|
||||
animation.duration = 0.7
|
||||
animation.autoreverses = true
|
||||
animation.repeatCount = Float.infinity
|
||||
indicatorView.layer.add(animation, forKey: "recording")
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.text,
|
||||
font: Font.semibold(17.0),
|
||||
color: .white
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
||||
if component.isRecording {
|
||||
if self.indicatorView == nil {
|
||||
let indicatorView = UIImageView(image: generateFilledCircleImage(diameter: 8.0, color: .red, strokeColor: nil, strokeWidth: nil, backgroundColor: nil))
|
||||
self.addSubview(indicatorView)
|
||||
self.indicatorView = indicatorView
|
||||
|
||||
self.updateIndicatorAnimation()
|
||||
}
|
||||
} else {
|
||||
if let indicatorView = self.indicatorView {
|
||||
self.indicatorView = nil
|
||||
indicatorView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 20.0
|
||||
let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height)
|
||||
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
self.textView.frame = textFrame
|
||||
|
||||
if let indicatorView = self.indicatorView, let image = indicatorView.image {
|
||||
indicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: floorToScreenPixels((size.height - image.size.height) / 2.0) + 1.0), size: image.size)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private final class NavigationBarComponent: CombinedComponent {
|
||||
let topInset: CGFloat
|
||||
let sideInset: CGFloat
|
||||
@ -442,7 +556,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
private(set) var canManageCall: Bool = false
|
||||
let isPictureInPictureSupported: Bool
|
||||
|
||||
private(set) var callTitle: String?
|
||||
private(set) var recordingStartTimestamp: Int32?
|
||||
|
||||
private(set) var peerTitle: String = ""
|
||||
private(set) var chatPeer: Peer?
|
||||
|
||||
private(set) var isVisibleInHierarchy: Bool = false
|
||||
private var isVisibleInHierarchyDisposable: Disposable?
|
||||
@ -498,6 +616,17 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
strongSelf.peerTitle = callPeer.debugDisplayTitle
|
||||
updated = true
|
||||
}
|
||||
strongSelf.chatPeer = callPeer
|
||||
|
||||
if strongSelf.callTitle != state.title {
|
||||
strongSelf.callTitle = state.title
|
||||
updated = true
|
||||
}
|
||||
|
||||
if strongSelf.recordingStartTimestamp != state.recordingStartTimestamp {
|
||||
strongSelf.recordingStartTimestamp = state.recordingStartTimestamp
|
||||
updated = true
|
||||
}
|
||||
|
||||
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
|
||||
if strongSelf.originInfo != originInfo {
|
||||
@ -593,6 +722,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
)
|
||||
|
||||
let call = context.component.call
|
||||
let state = context.state
|
||||
let controller = environment.controller
|
||||
|
||||
let video = video.update(
|
||||
@ -659,8 +789,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
size: CGSize(width: 22.0, height: 22.0)
|
||||
).tagged(moreAnimationTag))),
|
||||
])),
|
||||
action: { [weak call] in
|
||||
guard let call = call else {
|
||||
action: { [weak call, weak state] in
|
||||
guard let call = call, let state = state else {
|
||||
return
|
||||
}
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
@ -677,8 +807,141 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.primaryColor, backgroundColor: nil)
|
||||
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.LiveStream_EditTitle, textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak call, weak controller, weak state] _, a in
|
||||
guard let call = call, let controller = controller, let state = state, let chatPeer = state.chatPeer else {
|
||||
return
|
||||
}
|
||||
|
||||
let initialTitle = state.callTitle ?? ""
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let title: String = presentationData.strings.LiveStream_EditTitle
|
||||
let text: String = presentationData.strings.LiveStream_EditTitleText
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: EnginePeer(chatPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), value: initialTitle, maxLength: 40, apply: { [weak call] title in
|
||||
guard let call = call else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if let title = title, title != initialTitle {
|
||||
call.updateTitle(title)
|
||||
|
||||
let text: String = title.isEmpty ? presentationData.strings.LiveStream_EditTitleRemoveSuccess : presentationData.strings.LiveStream_EditTitleSuccess(title).string
|
||||
|
||||
let _ = text
|
||||
//strongSelf.presentUndoOverlay(content: .voiceChatFlag(text: text), action: { _ in return false })
|
||||
}
|
||||
})
|
||||
controller.present(editController, in: .window(.root))
|
||||
|
||||
a(.default)
|
||||
})))
|
||||
|
||||
if let recordingStartTimestamp = state.recordingStartTimestamp {
|
||||
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak call, weak controller] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let alertController = textAlertController(context: call.accountContext, forceTheme: defaultDarkPresentationTheme, title: nil, text: presentationData.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.VoiceChat_StopRecordingStop, action: { [weak call, weak controller] in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = presentationData.strings.LiveStream_RecordingSaved
|
||||
|
||||
let _ = text
|
||||
let _ = controller
|
||||
|
||||
/*strongSelf.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in
|
||||
if case .info = value, let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
let context = strongSelf.context
|
||||
strongSelf.controller?.dismiss(completion: {
|
||||
Queue.mainQueue().justDispatch {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(context.account.peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})*/
|
||||
})])
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}), false))
|
||||
} else {
|
||||
let text = presentationData.strings.LiveStream_StartRecording
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme -> UIImage? in
|
||||
return generateStartRecordingIcon(color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { [weak call, weak state, weak controller] _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
guard let call = call, let state = state, let _ = state.chatPeer, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
let placeholder: String = presentationData.strings.VoiceChat_RecordingTitlePlaceholderVideo
|
||||
|
||||
title = presentationData.strings.LiveStream_StartRecordingTitle
|
||||
text = presentationData.strings.LiveStream_StartRecordingTextVideo
|
||||
|
||||
let editController = voiceChatTitleEditController(sharedContext: call.accountContext.sharedContext, account: call.accountContext.account, forceTheme: defaultDarkPresentationTheme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak call, weak controller] title in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if let title = title {
|
||||
call.setShouldBeRecording(true, title: title, videoOrientation: false)
|
||||
|
||||
let text = presentationData.strings.LiveStream_RecordingStarted
|
||||
let _ = text
|
||||
|
||||
let _ = controller
|
||||
|
||||
call.playTone(.recordingStarted)
|
||||
}
|
||||
})
|
||||
controller.present(editController, in: .window(.root))
|
||||
})))
|
||||
}
|
||||
|
||||
let credentialsPromise = Promise<GroupCallStreamCredentials>()
|
||||
credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal<GroupCallStreamCredentials, NoError> in return .never() })
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: "View Stream Key", textColor: .primary, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.contextMenu.primaryColor, backgroundColor: nil)
|
||||
}, action: { [weak call, weak controller] _, a in
|
||||
guard let call = call, let controller = controller else {
|
||||
return
|
||||
}
|
||||
|
||||
controller.push(CreateExternalMediaStreamScreen(context: call.accountContext, peerId: call.peerId, credentialsPromise: credentialsPromise, mode: .view))
|
||||
|
||||
a(.default)
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(id: nil, text: presentationData.strings.VoiceChat_StopRecordingStop, textColor: .destructive, textLayout: .singleLine, textFont: .regular, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor, backgroundColor: nil)
|
||||
}, action: { [weak call] _, a in
|
||||
guard let call = call else {
|
||||
return
|
||||
@ -750,7 +1013,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
})
|
||||
),
|
||||
rightItems: navigationRightItems,
|
||||
centerItem: AnyComponent(Text(text: environment.strings.VoiceChatChannel_Title, font: Font.semibold(17.0), color: .white))
|
||||
centerItem: AnyComponent(StreamTitleComponent(text: environment.strings.VoiceChatChannel_Title, isRecording: state.recordingStartTimestamp != nil))
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
@ -775,7 +1038,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount))
|
||||
}
|
||||
infoItem = AnyComponent(OriginInfoComponent(
|
||||
title: originInfo.title,
|
||||
title: state.callTitle ?? originInfo.title,
|
||||
subtitle: memberCountString
|
||||
))
|
||||
}
|
||||
@ -813,7 +1076,6 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let state = context.state
|
||||
let height = context.availableSize.height
|
||||
context.add(background
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))
|
||||
|
@ -798,6 +798,11 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
let isVideo = false
|
||||
|
||||
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in
|
||||
if let isStream = initialCall.isStream, isStream {
|
||||
subscriber.putNext(true)
|
||||
return EmptyDisposable
|
||||
}
|
||||
|
||||
DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
|
||||
present(c, a)
|
||||
}, openSettings: {
|
||||
|
@ -981,7 +981,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
||||
} else if let ssrc = participantUpdate.ssrc, strongSelf.ssrcMapping[ssrc] == nil {
|
||||
}
|
||||
}
|
||||
case let .call(isTerminated, _, _, _, _, _):
|
||||
case let .call(isTerminated, _, _, _, _, _, _):
|
||||
if isTerminated {
|
||||
strongSelf.markAsCanBeRemoved()
|
||||
}
|
||||
|
@ -3183,14 +3183,14 @@ func replayFinalState(
|
||||
})
|
||||
|
||||
switch call {
|
||||
case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, _, _):
|
||||
case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _):
|
||||
let isMuted = (flags & (1 << 1)) != 0
|
||||
let canChange = (flags & (1 << 2)) != 0
|
||||
let isVideoEnabled = (flags & (1 << 9)) != 0
|
||||
let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange)
|
||||
updatedGroupCallParticipants.append((
|
||||
info.id,
|
||||
.call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled)
|
||||
.call(isTerminated: false, defaultParticipantsAreMuted: defaultParticipantsAreMuted, title: title, recordingStartTimestamp: recordStartDate, scheduleTimestamp: scheduleDate, isVideoEnabled: isVideoEnabled, participantCount: Int(participantsCount))
|
||||
))
|
||||
default:
|
||||
break
|
||||
@ -3199,7 +3199,7 @@ func replayFinalState(
|
||||
case let .groupCallDiscarded(callId, _, _):
|
||||
updatedGroupCallParticipants.append((
|
||||
callId,
|
||||
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false)
|
||||
.call(isTerminated: true, defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: false, canChange: false), title: nil, recordingStartTimestamp: nil, scheduleTimestamp: nil, isVideoEnabled: false, participantCount: nil)
|
||||
))
|
||||
|
||||
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
|
||||
|
@ -1200,7 +1200,7 @@ public final class GroupCallParticipantsContext {
|
||||
}
|
||||
|
||||
case state(update: StateUpdate)
|
||||
case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, title: String?, recordingStartTimestamp: Int32?, scheduleTimestamp: Int32?, isVideoEnabled: Bool)
|
||||
case call(isTerminated: Bool, defaultParticipantsAreMuted: State.DefaultParticipantsAreMuted, title: String?, recordingStartTimestamp: Int32?, scheduleTimestamp: Int32?, isVideoEnabled: Bool, participantCount: Int?)
|
||||
}
|
||||
|
||||
public final class MemberEvent {
|
||||
@ -1458,13 +1458,16 @@ public final class GroupCallParticipantsContext {
|
||||
for update in updates {
|
||||
if case let .state(update) = update {
|
||||
stateUpdates.append(update)
|
||||
} else if case let .call(_, defaultParticipantsAreMuted, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled) = update {
|
||||
} else if case let .call(_, defaultParticipantsAreMuted, title, recordingStartTimestamp, scheduleTimestamp, isVideoEnabled, participantsCount) = update {
|
||||
var state = self.stateValue.state
|
||||
state.defaultParticipantsAreMuted = defaultParticipantsAreMuted
|
||||
state.recordingStartTimestamp = recordingStartTimestamp
|
||||
state.title = title
|
||||
state.scheduleTimestamp = scheduleTimestamp
|
||||
state.isVideoEnabled = isVideoEnabled
|
||||
if let participantsCount = participantsCount {
|
||||
state.totalCount = participantsCount
|
||||
}
|
||||
|
||||
self.stateValue.state = state
|
||||
}
|
||||
|
@ -4108,7 +4108,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private func createExternalStream(credentialsPromise: Promise<GroupCallStreamCredentials>?) {
|
||||
self.controller?.push(CreateExternalMediaStreamScreen(context: self.context, peerId: self.peerId, credentialsPromise: credentialsPromise))
|
||||
self.controller?.push(CreateExternalMediaStreamScreen(context: self.context, peerId: self.peerId, credentialsPromise: credentialsPromise, mode: .create))
|
||||
}
|
||||
|
||||
private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 34d89a61664cfcbe8897c3640ff0f3e9ff709f4a
|
||||
Subproject commit aaa90f815aa3eb2e5343118fa186517a91fca4dc
|
@ -26,6 +26,7 @@ swift_library(
|
||||
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
|
||||
"//submodules/AvatarNode:AvatarNode",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -4,6 +4,7 @@ import Display
|
||||
import TelegramPresentationData
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
|
||||
public enum UndoOverlayContent {
|
||||
case removedChat(text: String)
|
||||
|
Loading…
x
Reference in New Issue
Block a user