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
}
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 {

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

View File

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

View File

@ -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",

View File

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

View File

@ -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: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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