diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 83dcf2a517..dc8a1a4308 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -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 { diff --git a/submodules/ComponentFlow/Source/Utils/ActionSlot.swift b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift index b2419ed034..c6dd4aac40 100644 --- a/submodules/ComponentFlow/Source/Utils/ActionSlot.swift +++ b/submodules/ComponentFlow/Source/Utils/ActionSlot.swift @@ -12,12 +12,16 @@ public final class Action { } } -public final class ActionSlot { +public final class ActionSlot: Equatable { private var target: ((Arguments) -> Void)? init() { } + public static func ==(lhs: ActionSlot, rhs: ActionSlot) -> Bool { + return lhs === rhs + } + public func connect(_ target: @escaping (Arguments) -> Void) { self.target = target } diff --git a/submodules/Components/UndoPanelComponent/BUILD b/submodules/Components/UndoPanelComponent/BUILD new file mode 100644 index 0000000000..c8f46ab496 --- /dev/null +++ b/submodules/Components/UndoPanelComponent/BUILD @@ -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", + ], +) diff --git a/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift b/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift new file mode 100644 index 0000000000..e662d0f3c5 --- /dev/null +++ b/submodules/Components/UndoPanelComponent/Sources/UndoPanelComponent.swift @@ -0,0 +1,67 @@ +import Foundation +import UIKit +import ComponentFlow + +public final class UndoPanelComponent: Component { + public let icon: AnyComponent? + public let content: AnyComponent + public let action: AnyComponent? + + public init( + icon: AnyComponent?, + content: AnyComponent, + action: AnyComponent? + ) { + 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? + private let centralContentView: ComponentHostView + private var actionView: ComponentHostView? + + 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift b/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift new file mode 100644 index 0000000000..ec7eb73d5c --- /dev/null +++ b/submodules/Components/UndoPanelComponent/Sources/UndoPanelContainerComponent.swift @@ -0,0 +1,83 @@ +import Foundation +import UIKit +import ComponentFlow + +public final class UndoPanelContainerComponent: Component { + let push: ActionSlot + + public init(push: ActionSlot) { + 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? + + 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() + 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, transition: transition) + } +} diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 50f6cc5a8f..b92b4a6bc0 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -101,17 +101,19 @@ open class ViewControllerComponentContainer: ViewController { private weak var controller: ViewControllerComponentContainer? private let component: AnyComponent + private let theme: PresentationTheme? public let hostView: ComponentHostView private var currentIsVisible: Bool = false private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent) { + init(context: AccountContext, controller: ViewControllerComponentContainer, component: AnyComponent, 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 - public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance) where C.EnvironmentType == ViewControllerComponentContainer.Environment { + public init(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() } diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index 3b24847788..fb7f2ff311 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -19,11 +19,13 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let context: AccountContext let peerId: EnginePeer.Id + let mode: CreateExternalMediaStreamScreen.Mode let credentialsPromise: Promise? - init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?) { + init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) { self.context = context self.peerId = peerId + self.mode = mode self.credentialsPromise = credentialsPromise } @@ -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?) { + public init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?, 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)) diff --git a/submodules/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index 7077cca007..92f2c01c62 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -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", diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5df799a69a..a3ba11bc23 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -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 + private var indicatorView: UIImageView? + + private let trackingLayer: HierarchyTrackingLayer + + override init(frame: CGRect) { + self.textView = ComponentHostView() + + 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, 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() + credentialsPromise.set(call.accountContext.engine.calls.getGroupCallStreamCredentials(peerId: call.peerId, revokePreviousCredentials: false) |> `catch` { _ -> Signal 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)) diff --git a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift index 17acbdf4ee..2fa83533cb 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationCallManager.swift @@ -798,6 +798,11 @@ public final class PresentationCallManagerImpl: PresentationCallManager { let isVideo = false let accessEnabledSignal: Signal = 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: { diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index fb01d186d2..6e25bfdbb3 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -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() } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 571c61e8bd..15771dd1a2 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -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 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 8720e9feb6..2e6bf8bb16 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -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 } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 285a0d770b..3aa5a627ac 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -4108,7 +4108,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } private func createExternalStream(credentialsPromise: Promise?) { - 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?) { diff --git a/submodules/TgVoipWebrtc/tgcalls b/submodules/TgVoipWebrtc/tgcalls index 34d89a6166..aaa90f815a 160000 --- a/submodules/TgVoipWebrtc/tgcalls +++ b/submodules/TgVoipWebrtc/tgcalls @@ -1 +1 @@ -Subproject commit 34d89a61664cfcbe8897c3640ff0f3e9ff709f4a +Subproject commit aaa90f815aa3eb2e5343118fa186517a91fca4dc diff --git a/submodules/UndoUI/BUILD b/submodules/UndoUI/BUILD index 3887213ddc..35e2b4eeba 100644 --- a/submodules/UndoUI/BUILD +++ b/submodules/UndoUI/BUILD @@ -26,6 +26,7 @@ swift_library( "//submodules/SlotMachineAnimationNode:SlotMachineAnimationNode", "//submodules/AvatarNode:AvatarNode", "//submodules/AccountContext:AccountContext", + "//submodules/ComponentFlow:ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 94350e8f06..7a51adf24d 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -4,6 +4,7 @@ import Display import TelegramPresentationData import TelegramCore import AccountContext +import ComponentFlow public enum UndoOverlayContent { case removedChat(text: String)