From fd233a4657e9209c33d418540e33fcf6a5d13d18 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Sun, 27 Feb 2022 00:13:59 +0400 Subject: [PATCH] Video stream UI improvements --- .../ActivityIndicatorComponent/BUILD | 18 +++ .../Sources/ActivityIndicatorComponent.swift | 39 +++++ submodules/TelegramCallsUI/BUILD | 1 + .../Components/MediaStreamComponent.swift | 134 +++++++++++------- .../MediaStreamVideoComponent.swift | 39 ++++- .../Sources/PresentationGroupCall.swift | 9 +- 6 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 submodules/Components/ActivityIndicatorComponent/BUILD create mode 100644 submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift diff --git a/submodules/Components/ActivityIndicatorComponent/BUILD b/submodules/Components/ActivityIndicatorComponent/BUILD new file mode 100644 index 0000000000..3f3ab06cbf --- /dev/null +++ b/submodules/Components/ActivityIndicatorComponent/BUILD @@ -0,0 +1,18 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ActivityIndicatorComponent", + module_name = "ActivityIndicatorComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift b/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift new file mode 100644 index 0000000000..b40c991289 --- /dev/null +++ b/submodules/Components/ActivityIndicatorComponent/Sources/ActivityIndicatorComponent.swift @@ -0,0 +1,39 @@ +import Foundation +import UIKit +import ComponentFlow + +public final class ActivityIndicatorComponent: Component { + public init( + ) { + } + + public static func ==(lhs: ActivityIndicatorComponent, rhs: ActivityIndicatorComponent) -> Bool { + return true + } + + public final class View: UIActivityIndicatorView { + public init() { + super.init(style: .whiteLarge) + } + + required public init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ActivityIndicatorComponent, availableSize: CGSize, transition: Transition) -> CGSize { + if !self.isAnimating { + self.startAnimating() + } + + return CGSize(width: 22.0, height: 22.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/TelegramCallsUI/BUILD b/submodules/TelegramCallsUI/BUILD index bedebbc74d..591d757de4 100644 --- a/submodules/TelegramCallsUI/BUILD +++ b/submodules/TelegramCallsUI/BUILD @@ -97,6 +97,7 @@ swift_library( "//third-party/libyuv:LibYuvBinding", "//submodules/ComponentFlow:ComponentFlow", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/ActivityIndicatorComponent:ActivityIndicatorComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index b47b6e199c..ed1460c0bf 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -588,29 +588,28 @@ public final class MediaStreamComponent: CombinedComponent { let call = context.component.call let controller = environment.controller - let video = Condition(context.state.hasVideo) { - return video.update( - component: MediaStreamVideoComponent( - call: context.component.call, - activatePictureInPicture: activatePictureInPicture, - bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in - guard let call = call else { - completed() - return - } - - call.accountContext.sharedContext.mainWindow?.inCallNavigate?() - + let video = video.update( + component: MediaStreamVideoComponent( + call: context.component.call, + hasVideo: context.state.hasVideo, + activatePictureInPicture: activatePictureInPicture, + bringBackControllerForPictureInPictureDeactivation: { [weak call] completed in + guard let call = call else { completed() + return } - ), - availableSize: context.availableSize, - transition: context.transition - ) - } + + call.accountContext.sharedContext.mainWindow?.inCallNavigate?() + + completed() + } + ), + availableSize: context.availableSize, + transition: context.transition + ) var navigationRightItems: [AnyComponentWithIdentity] = [] - if context.state.isPictureInPictureSupported, let _ = video { + if context.state.isPictureInPictureSupported, context.state.hasVideo { navigationRightItems.append(AnyComponentWithIdentity(id: "pip", component: AnyComponent(Button( content: AnyComponent(BundleIconComponent( name: "Media Gallery/PictureInPictureButton", @@ -663,7 +662,7 @@ public final class MediaStreamComponent: CombinedComponent { topInset: environment.statusBarHeight, sideInset: environment.safeInsets.left, leftItem: AnyComponent(Button( - content: AnyComponent(NavigationBackButtonComponent(text: environment.strings.Common_Back, color: .white)), + content: AnyComponent(NavigationBackButtonComponent(text: environment.strings.Common_Close, color: .white)), action: { [weak call] in let _ = call?.leave(terminateIfPossible: false) }) @@ -754,11 +753,9 @@ public final class MediaStreamComponent: CombinedComponent { }) ) - if let video = video { - context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0 + context.state.dismissOffset)) - ) - } + context.add(video + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0 + context.state.dismissOffset)) + ) context.add(navigationBar .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height / 2.0)) @@ -776,6 +773,7 @@ public final class MediaStreamComponent: CombinedComponent { } public final class MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController { + private let context: AccountContext public let call: PresentationGroupCall public private(set) var currentOverlayController: VoiceChatOverlayController? = nil public var parentNavigationController: NavigationController? @@ -785,13 +783,19 @@ public final class MediaStreamComponentController: ViewControllerComponentContai private var initialOrientation: UIInterfaceOrientation? + private let inviteLinksPromise = Promise(nil) + public init(call: PresentationGroupCall) { + self.context = call.accountContext self.call = call super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl)) self.statusBar.statusBarStyle = .White self.view.disablesInteractiveModalDismiss = true + + self.inviteLinksPromise.set(.single(nil) + |> then(call.inviteLinks)) } required public init(coder aDecoder: NSCoder) { @@ -858,6 +862,41 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } func presentShare() { + let _ = (self.inviteLinksPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] inviteLinks in + guard let strongSelf = self else { + return + } + + let callPeerId = strongSelf.call.peerId + let _ = (strongSelf.call.accountContext.account.postbox.transaction { transaction -> GroupCallInviteLinks? in + if let inviteLinks = inviteLinks { + return inviteLinks + } else if let peer = transaction.getPeer(callPeerId), let addressName = peer.addressName, !addressName.isEmpty { + return GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)?voicechat", speakerLink: nil) + } else if let cachedData = transaction.getPeerCachedData(peerId: callPeerId) { + if let cachedData = cachedData as? CachedChannelData, let link = cachedData.exportedInvitation?.link { + return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) + } else if let cachedData = cachedData as? CachedGroupData, let link = cachedData.exportedInvitation?.link { + return GroupCallInviteLinks(listenerLink: link, speakerLink: nil) + } + } + return nil + } + |> deliverOnMainQueue).start(next: { links in + guard let strongSelf = self else { + return + } + + if let links = links { + strongSelf.presentShare(links: links) + } + }) + }) + } + + func presentShare(links inviteLinks: GroupCallInviteLinks) { let formatSendTitle: (String) -> String = { string in var string = string if string.contains("[") && string.contains("]") { @@ -869,34 +908,29 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } return string } + let _ = formatSendTitle - let _ = (combineLatest(self.call.accountContext.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1)) + let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(self.call.peerId), self.call.state |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] peer, callState in if let strongSelf = self { - var maybeInviteLinks: GroupCallInviteLinks? = nil + var inviteLinks = inviteLinks - if let peer = peer as? TelegramChannel, let addressName = peer.addressName { - maybeInviteLinks = GroupCallInviteLinks(listenerLink: "https://t.me/\(addressName)", speakerLink: nil) + if let peer = peer as? TelegramChannel, case .group = peer.info, !peer.flags.contains(.isGigagroup), !(peer.addressName ?? "").isEmpty, let defaultParticipantMuteState = callState.defaultParticipantMuteState { + let isMuted = defaultParticipantMuteState == .muted + + if !isMuted { + inviteLinks = GroupCallInviteLinks(listenerLink: inviteLinks.listenerLink, speakerLink: nil) + } } - guard let inviteLinks = maybeInviteLinks else { - return - } - - let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } var segmentedValues: [ShareControllerSegmentedValue]? - if let speakerLink = inviteLinks.speakerLink { - segmentedValues = [ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Speaker, subject: .url(speakerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopySpeakerLink, formatSendTitle: { count in - return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteSpeakers(Int32(count))) - }), ShareControllerSegmentedValue(title: presentationData.strings.VoiceChat_InviteLink_Listener, subject: .url(inviteLinks.listenerLink), actionTitle: presentationData.strings.VoiceChat_InviteLink_CopyListenerLink, formatSendTitle: { count in - return formatSendTitle(presentationData.strings.VoiceChat_InviteLink_InviteListeners(Int32(count))) - })] - } - let shareController = ShareController(context: strongSelf.call.accountContext, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkColorPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) + segmentedValues = nil + let shareController = ShareController(context: strongSelf.context, subject: .url(inviteLinks.listenerLink), segmentedValues: segmentedValues, forceTheme: defaultDarkPresentationTheme, forcedActionTitle: presentationData.strings.VoiceChat_CopyInviteLink) shareController.completed = { [weak self] peerIds in if let strongSelf = self { - let _ = (strongSelf.call.accountContext.account.postbox.transaction { transaction -> [Peer] in + let _ = (strongSelf.context.account.postbox.transaction { transaction -> [Peer] in var peers: [Peer] = [] for peerId in peerIds { if let peer = transaction.getPeer(peerId) { @@ -904,19 +938,19 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } } return peers - } |> deliverOnMainQueue).start(next: { peers in + } |> deliverOnMainQueue).start(next: { [weak self] peers in if let strongSelf = self { - let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let text: String var isSavedMessages = false if peers.count == 1, let peer = peers.first { - isSavedMessages = peer.id == strongSelf.call.accountContext.account.peerId - let peerName = peer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + isSavedMessages = peer.id == strongSelf.context.account.peerId + let peerName = peer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.VoiceChat_ForwardTooltip_Chat(peerName).string } else if peers.count == 2, let firstPeer = peers.first, let secondPeer = peers.last { - let firstPeerName = firstPeer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(firstPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - let secondPeerName = secondPeer.id == strongSelf.call.accountContext.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(secondPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let firstPeerName = firstPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(firstPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + let secondPeerName = secondPeer.id == strongSelf.context.account.peerId ? presentationData.strings.DialogList_SavedMessages : EnginePeer(secondPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) text = presentationData.strings.VoiceChat_ForwardTooltip_TwoChats(firstPeerName, secondPeerName).string } else if let peer = peers.first { let peerName = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) @@ -932,7 +966,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai } shareController.actionCompleted = { if let strongSelf = self { - let presentationData = strongSelf.call.accountContext.sharedContext.currentPresentationData.with { $0 } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.VoiceChat_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 9b05313e40..c67de46857 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -1,16 +1,19 @@ import Foundation import UIKit import ComponentFlow +import ActivityIndicatorComponent import AccountContext import AVKit final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl + let hasVideo: Bool let activatePictureInPicture: ActionSlot> let bringBackControllerForPictureInPictureDeactivation: (@escaping () -> Void) -> Void - init(call: PresentationGroupCallImpl, activatePictureInPicture: ActionSlot>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) { + init(call: PresentationGroupCallImpl, hasVideo: Bool, activatePictureInPicture: ActionSlot>, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void) { self.call = call + self.hasVideo = hasVideo self.activatePictureInPicture = activatePictureInPicture self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation } @@ -19,6 +22,9 @@ final class MediaStreamVideoComponent: Component { if lhs.call !== rhs.call { return false } + if lhs.hasVideo != rhs.hasVideo { + return false + } return true } @@ -38,6 +44,7 @@ final class MediaStreamVideoComponent: Component { private var videoView: VideoRenderingView? private let blurTintView: UIView private var videoBlurView: VideoRenderingView? + private var activityIndicatorView: ComponentHostView? private var pictureInPictureController: AVPictureInPictureController? @@ -60,7 +67,7 @@ final class MediaStreamVideoComponent: Component { } func update(component: MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { - if self.videoView == nil { + if component.hasVideo, self.videoView == nil { if let input = component.call.video(endpointId: "unified") { if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { self.videoBlurView = videoBlurView @@ -83,7 +90,14 @@ final class MediaStreamVideoComponent: Component { videoView.setOnOrientationUpdated { [weak state] _, _ in state?.updated(transition: .immediate) } - videoView.setOnFirstFrameReceived { [weak state] _ in + videoView.setOnFirstFrameReceived { [weak self, weak state] _ in + guard let strongSelf = self else { + return + } + + strongSelf.activityIndicatorView?.removeFromSuperview() + strongSelf.activityIndicatorView = nil + state?.updated(transition: .immediate) } } @@ -106,6 +120,25 @@ final class MediaStreamVideoComponent: Component { videoBlurView.updateIsEnabled(true) transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil) } + } else { + var activityIndicatorTransition = transition + let activityIndicatorView: ComponentHostView + if let current = self.activityIndicatorView { + activityIndicatorView = current + } else { + activityIndicatorTransition = transition.withAnimation(.none) + activityIndicatorView = ComponentHostView() + self.activityIndicatorView = activityIndicatorView + self.addSubview(activityIndicatorView) + } + + let activityIndicatorSize = activityIndicatorView.update( + transition: transition, + component: AnyComponent(ActivityIndicatorComponent()), + environment: {}, + containerSize: CGSize(width: 100.0, height: 100.0) + ) + activityIndicatorTransition.setFrame(view: activityIndicatorView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - activityIndicatorSize.width) / 2.0), y: floor((availableSize.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize), completion: nil) } self.component = component diff --git a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift index de1d054cd1..6a64b4c250 100644 --- a/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift +++ b/submodules/TelegramCallsUI/Sources/PresentationGroupCall.swift @@ -229,7 +229,14 @@ public final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCach } public func leaveInBackground(engine: TelegramEngine, id: Int64, accessHash: Int64, source: UInt32) { - let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start() + let disposable = engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + if let context = strongSelf.contexts[id] { + context.context.participantsContext?.removeLocalPeerId() + } + }) self.leaveDisposables.add(disposable) } }