Media-related improvements

This commit is contained in:
Ali 2022-03-15 22:08:20 +04:00
parent b82b1e41e0
commit fe0311b1e9
17 changed files with 531 additions and 41 deletions

View File

@ -153,6 +153,17 @@ public struct Transition {
return result 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 var immediate: Transition = Transition(animation: .none)
public static func easeInOut(duration: Double) -> Transition { public static func easeInOut(duration: Double) -> Transition {

View File

@ -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)? private var target: ((Arguments) -> Void)?
init() { init() {
} }
public static func ==(lhs: ActionSlot<Arguments>, rhs: ActionSlot<Arguments>) -> Bool {
return lhs === rhs
}
public func connect(_ target: @escaping (Arguments) -> Void) { public func connect(_ target: @escaping (Arguments) -> Void) {
self.target = target self.target = target
} }

View 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",
],
)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -101,17 +101,19 @@ open class ViewControllerComponentContainer: ViewController {
private weak var controller: ViewControllerComponentContainer? private weak var controller: ViewControllerComponentContainer?
private let component: AnyComponent<ViewControllerComponentContainer.Environment> private let component: AnyComponent<ViewControllerComponentContainer.Environment>
private let theme: PresentationTheme?
public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment> public let hostView: ComponentHostView<ViewControllerComponentContainer.Environment>
private var currentIsVisible: Bool = false private var currentIsVisible: Bool = false
private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? 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.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.controller = controller self.controller = controller
self.component = component self.component = component
self.theme = theme
self.hostView = ComponentHostView() self.hostView = ComponentHostView()
super.init() super.init()
@ -127,7 +129,7 @@ open class ViewControllerComponentContainer: ViewController {
navigationHeight: navigationHeight, 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), 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, isVisible: self.currentIsVisible,
theme: self.presentationData.theme, theme: self.theme ?? self.presentationData.theme,
strings: self.presentationData.strings, strings: self.presentationData.strings,
controller: { [weak self] in controller: { [weak self] in
return self?.controller return self?.controller
@ -162,11 +164,13 @@ open class ViewControllerComponentContainer: ViewController {
} }
private let context: AccountContext private let context: AccountContext
private let theme: PresentationTheme?
private let component: AnyComponent<ViewControllerComponentContainer.Environment> 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.context = context
self.component = AnyComponent(component) self.component = AnyComponent(component)
self.theme = theme
let navigationBarPresentationData: NavigationBarPresentationData? let navigationBarPresentationData: NavigationBarPresentationData?
switch navigationBarAppearance { switch navigationBarAppearance {
@ -185,7 +189,7 @@ open class ViewControllerComponentContainer: ViewController {
} }
override open func loadDisplayNode() { 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() self.displayNodeDidLoad()
} }

View File

@ -19,11 +19,13 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let context: AccountContext let context: AccountContext
let peerId: EnginePeer.Id let peerId: EnginePeer.Id
let mode: CreateExternalMediaStreamScreen.Mode
let credentialsPromise: Promise<GroupCallStreamCredentials>? 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.context = context
self.peerId = peerId self.peerId = peerId
self.mode = mode
self.credentialsPromise = credentialsPromise self.credentialsPromise = credentialsPromise
} }
@ -34,6 +36,9 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
if lhs.peerId != rhs.peerId { if lhs.peerId != rhs.peerId {
return false return false
} }
if lhs.mode != rhs.mode {
return false
}
if lhs.credentialsPromise !== rhs.credentialsPromise { if lhs.credentialsPromise !== rhs.credentialsPromise {
return false return false
} }
@ -180,6 +185,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let environment = context.environment[ViewControllerComponentContainer.Environment.self].value
let state = context.state let state = context.state
let mode = context.component.mode
let controller = environment.controller let controller = environment.controller
let bottomInset: CGFloat let bottomInset: CGFloat
@ -218,20 +224,22 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
transition: context.transition transition: context.transition
) )
let bottomText = bottomText.update( let bottomText = Condition(mode == .create) {
component: MultilineTextComponent( bottomText.update(
text: NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center), component: MultilineTextComponent(
horizontalAlignment: .center, text: NSAttributedString(string: environment.strings.CreateExternalStream_StartStreamingInfo, font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor, paragraphAlignment: .center),
maximumNumberOfLines: 0, horizontalAlignment: .center,
lineSpacing: 0.1 maximumNumberOfLines: 0,
), lineSpacing: 0.1
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), ),
transition: context.transition availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
) transition: context.transition
)
}
let button = button.update( let button = button.update(
component: SolidRoundedButtonComponent( component: SolidRoundedButtonComponent(
title: environment.strings.CreateExternalStream_StartStreaming, title: mode == .create ? environment.strings.CreateExternalStream_StartStreaming : environment.strings.Common_Close,
theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), theme: SolidRoundedButtonComponent.Theme(theme: environment.theme),
font: .bold, font: .bold,
fontSize: 17.0, fontSize: 17.0,
@ -243,9 +251,14 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
return return
} }
state.createAndJoinGroupCall(baseController: controller, completion: { [weak controller] in switch mode {
controller?.dismiss() 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), 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) let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: context.availableSize.height - bottomInset - button.size.height), size: button.size)
context.add(bottomText if let bottomText = bottomText {
.position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - 14.0 - bottomText.size.height / 2.0)) context.add(bottomText
) .position(CGPoint(x: context.availableSize.width / 2.0, y: buttonFrame.minY - 14.0 - bottomText.size.height / 2.0))
)
}
context.add(button context.add(button
.position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY))
@ -410,19 +425,32 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent
} }
public final class CreateExternalMediaStreamScreen: ViewControllerComponentContainer { public final class CreateExternalMediaStreamScreen: ViewControllerComponentContainer {
public enum Mode {
case create
case view
}
private let context: AccountContext private let context: AccountContext
private let peerId: EnginePeer.Id 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.context = context
self.peerId = peerId 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 self.navigationPresentation = .modal
let presentationData = context.sharedContext.currentPresentationData.with { $0 } 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)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))

View File

@ -101,6 +101,9 @@ swift_library(
"//submodules/Components/ViewControllerComponent:ViewControllerComponent", "//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/Components/UndoPanelComponent:UndoPanelComponent",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -10,10 +10,14 @@ import Postbox
import ShareController import ShareController
import UndoUI import UndoUI
import TelegramPresentationData import TelegramPresentationData
import PresentationDataUtils
import LottieAnimationComponent import LottieAnimationComponent
import ContextUI import ContextUI
import ViewControllerComponent import ViewControllerComponent
import BundleIconComponent import BundleIconComponent
import CreateExternalMediaStreamScreen
import HierarchyTrackingLayer
import UndoPanelComponent
final class NavigationBackButtonComponent: Component { final class NavigationBackButtonComponent: Component {
let text: String 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 { private final class NavigationBarComponent: CombinedComponent {
let topInset: CGFloat let topInset: CGFloat
let sideInset: CGFloat let sideInset: CGFloat
@ -442,7 +556,11 @@ public final class MediaStreamComponent: CombinedComponent {
private(set) var canManageCall: Bool = false private(set) var canManageCall: Bool = false
let isPictureInPictureSupported: Bool let isPictureInPictureSupported: Bool
private(set) var callTitle: String?
private(set) var recordingStartTimestamp: Int32?
private(set) var peerTitle: String = "" private(set) var peerTitle: String = ""
private(set) var chatPeer: Peer?
private(set) var isVisibleInHierarchy: Bool = false private(set) var isVisibleInHierarchy: Bool = false
private var isVisibleInHierarchyDisposable: Disposable? private var isVisibleInHierarchyDisposable: Disposable?
@ -498,6 +616,17 @@ public final class MediaStreamComponent: CombinedComponent {
strongSelf.peerTitle = callPeer.debugDisplayTitle strongSelf.peerTitle = callPeer.debugDisplayTitle
updated = true 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) let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount)
if strongSelf.originInfo != originInfo { if strongSelf.originInfo != originInfo {
@ -593,6 +722,7 @@ public final class MediaStreamComponent: CombinedComponent {
) )
let call = context.component.call let call = context.component.call
let state = context.state
let controller = environment.controller let controller = environment.controller
let video = video.update( let video = video.update(
@ -659,8 +789,8 @@ public final class MediaStreamComponent: CombinedComponent {
size: CGSize(width: 22.0, height: 22.0) size: CGSize(width: 22.0, height: 22.0)
).tagged(moreAnimationTag))), ).tagged(moreAnimationTag))),
])), ])),
action: { [weak call] in action: { [weak call, weak state] in
guard let call = call else { guard let call = call, let state = state else {
return return
} }
guard let controller = controller() as? MediaStreamComponentController else { guard let controller = controller() as? MediaStreamComponentController else {
@ -677,8 +807,141 @@ public final class MediaStreamComponent: CombinedComponent {
let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 }
var items: [ContextMenuItem] = [] 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 }, action: { [weak call] _, a in
guard let call = call else { guard let call = call else {
return return
@ -750,7 +1013,7 @@ public final class MediaStreamComponent: CombinedComponent {
}) })
), ),
rightItems: navigationRightItems, 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), availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: context.transition transition: context.transition
@ -775,7 +1038,7 @@ public final class MediaStreamComponent: CombinedComponent {
memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount)) memberCountString = environment.strings.LiveStream_ViewerCount(Int32(originInfo.memberCount))
} }
infoItem = AnyComponent(OriginInfoComponent( infoItem = AnyComponent(OriginInfoComponent(
title: originInfo.title, title: state.callTitle ?? originInfo.title,
subtitle: memberCountString subtitle: memberCountString
)) ))
} }
@ -813,7 +1076,6 @@ public final class MediaStreamComponent: CombinedComponent {
transition: context.transition transition: context.transition
) )
let state = context.state
let height = context.availableSize.height let height = context.availableSize.height
context.add(background context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0))

View File

@ -798,6 +798,11 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
let isVideo = false let isVideo = false
let accessEnabledSignal: Signal<Bool, NoError> = Signal { subscriber in 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 DeviceAccess.authorizeAccess(to: .microphone(.voiceCall), presentationData: presentationData, present: { c, a in
present(c, a) present(c, a)
}, openSettings: { }, openSettings: {

View File

@ -981,7 +981,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
} else if let ssrc = participantUpdate.ssrc, strongSelf.ssrcMapping[ssrc] == nil { } else if let ssrc = participantUpdate.ssrc, strongSelf.ssrcMapping[ssrc] == nil {
} }
} }
case let .call(isTerminated, _, _, _, _, _): case let .call(isTerminated, _, _, _, _, _, _):
if isTerminated { if isTerminated {
strongSelf.markAsCanBeRemoved() strongSelf.markAsCanBeRemoved()
} }

View File

@ -3183,14 +3183,14 @@ func replayFinalState(
}) })
switch call { switch call {
case let .groupCall(flags, _, _, _, title, _, recordStartDate, scheduleDate, _, _, _): case let .groupCall(flags, _, _, participantsCount, title, _, recordStartDate, scheduleDate, _, _, _):
let isMuted = (flags & (1 << 1)) != 0 let isMuted = (flags & (1 << 1)) != 0
let canChange = (flags & (1 << 2)) != 0 let canChange = (flags & (1 << 2)) != 0
let isVideoEnabled = (flags & (1 << 9)) != 0 let isVideoEnabled = (flags & (1 << 9)) != 0
let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange) let defaultParticipantsAreMuted = GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: isMuted, canChange: canChange)
updatedGroupCallParticipants.append(( updatedGroupCallParticipants.append((
info.id, 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: default:
break break
@ -3199,7 +3199,7 @@ func replayFinalState(
case let .groupCallDiscarded(callId, _, _): case let .groupCallDiscarded(callId, _, _):
updatedGroupCallParticipants.append(( updatedGroupCallParticipants.append((
callId, 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 transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in

View File

@ -1200,7 +1200,7 @@ public final class GroupCallParticipantsContext {
} }
case state(update: StateUpdate) 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 { public final class MemberEvent {
@ -1458,13 +1458,16 @@ public final class GroupCallParticipantsContext {
for update in updates { for update in updates {
if case let .state(update) = update { if case let .state(update) = update {
stateUpdates.append(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 var state = self.stateValue.state
state.defaultParticipantsAreMuted = defaultParticipantsAreMuted state.defaultParticipantsAreMuted = defaultParticipantsAreMuted
state.recordingStartTimestamp = recordingStartTimestamp state.recordingStartTimestamp = recordingStartTimestamp
state.title = title state.title = title
state.scheduleTimestamp = scheduleTimestamp state.scheduleTimestamp = scheduleTimestamp
state.isVideoEnabled = isVideoEnabled state.isVideoEnabled = isVideoEnabled
if let participantsCount = participantsCount {
state.totalCount = participantsCount
}
self.stateValue.state = state self.stateValue.state = state
} }

View File

@ -4108,7 +4108,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
} }
private func createExternalStream(credentialsPromise: Promise<GroupCallStreamCredentials>?) { 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?) { private func createAndJoinGroupCall(peerId: PeerId, joinAsPeerId: PeerId?) {

@ -1 +1 @@
Subproject commit 34d89a61664cfcbe8897c3640ff0f3e9ff709f4a Subproject commit aaa90f815aa3eb2e5343118fa186517a91fca4dc

View File

@ -26,6 +26,7 @@ swift_library(
"//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode",
"//submodules/AvatarNode:AvatarNode", "//submodules/AvatarNode:AvatarNode",
"//submodules/AccountContext:AccountContext", "//submodules/AccountContext:AccountContext",
"//submodules/ComponentFlow:ComponentFlow",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -4,6 +4,7 @@ import Display
import TelegramPresentationData import TelegramPresentationData
import TelegramCore import TelegramCore
import AccountContext import AccountContext
import ComponentFlow
public enum UndoOverlayContent { public enum UndoOverlayContent {
case removedChat(text: String) case removedChat(text: String)