Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2025-01-25 02:33:53 +04:00
commit 5761761ee5
42 changed files with 1949 additions and 908 deletions

View File

@ -168,6 +168,7 @@ private final class EmbeddedBroadcastUploadImpl: BroadcastUploadImpl {
onMutedSpeechActivityDetected: { _ in },
encryptionKey: nil,
isConference: false,
isStream: false,
sharedAudioDevice: nil
)
self.callContext = callContext

View File

@ -1 +1 @@
2500
2510

View File

@ -131,6 +131,11 @@ public final class PresentationCallVideoView {
}
}
public enum PresentationCallConferenceState {
case preparing
case ready
}
public protocol PresentationCall: AnyObject {
var context: AccountContext { get }
var isIntegratedWithCallKit: Bool { get }
@ -144,7 +149,8 @@ public protocol PresentationCall: AnyObject {
var state: Signal<PresentationCallState, NoError> { get }
var audioLevel: Signal<Float, NoError> { get }
var hasConference: Signal<Bool, NoError> { get }
var conferenceState: Signal<PresentationCallConferenceState?, NoError> { get }
var conferenceStateValue: PresentationCallConferenceState? { get }
var conferenceCall: PresentationGroupCall? { get }
var isMuted: Signal<Bool, NoError> { get }
@ -468,6 +474,28 @@ public protocol PresentationGroupCall: AnyObject {
func loadMoreMembers(token: String)
}
public enum VideoChatCall: Equatable {
case group(PresentationGroupCall)
case conferenceSource(PresentationCall)
public static func ==(lhs: VideoChatCall, rhs: VideoChatCall) -> Bool {
switch lhs {
case let .group(lhsGroup):
if case let .group(rhsGroup) = rhs, lhsGroup === rhsGroup {
return true
} else {
return false
}
case let .conferenceSource(lhsConferenceSource):
if case let .conferenceSource(rhsConferenceSource) = rhs, lhsConferenceSource === rhsConferenceSource {
return true
} else {
return false
}
}
}
}
public protocol PresentationCallManager: AnyObject {
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
var currentGroupCallSignal: Signal<PresentationGroupCall?, NoError> { get }

View File

@ -49,11 +49,23 @@ public enum ShareControllerError {
}
public enum ShareControllerSubject {
public final class PublicLinkPrefix {
public let visibleString: String
public let actualString: String
public init(visibleString: String, actualString: String) {
self.visibleString = visibleString
self.actualString = actualString
}
}
public final class MediaParameters {
public let startAtTimestamp: Int32?
public let publicLinkPrefix: PublicLinkPrefix?
public init(startAtTimestamp: Int32?) {
public init(startAtTimestamp: Int32?, publicLinkPrefix: PublicLinkPrefix?) {
self.startAtTimestamp = startAtTimestamp
self.publicLinkPrefix = publicLinkPrefix
}
}

View File

@ -1367,6 +1367,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
return panelHeight
}
override func animateIn(transition: ContainedViewLayoutTransition) {
self.contentNode.alpha = 0.0
transition.updateAlpha(node: self.contentNode, alpha: self.visibilityAlpha)
}
override func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
if let previousContentNode = previousContentNode as? ChatItemGalleryFooterContentNode, previousContentNode.scrubberView != nil {
@ -1392,6 +1397,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
self.scrollWrapperNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
}
override func animateOut(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.contentNode, alpha: 0.0)
}
override func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
if let scrubberView = self.scrubberView, scrubberView.superview == self.view {
if let nextContentNode = nextContentNode as? ChatItemGalleryFooterContentNode, nextContentNode.scrubberView != nil {
@ -1718,6 +1727,30 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
shareController.dismissed = { [weak self] _ in
self?.interacting?(false)
}
shareController.onMediaTimestampLinkCopied = { [weak self] timestamp in
guard let self else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text: String
if let timestamp {
//TODO:localize
let startTimeString: String
let hours = timestamp / (60 * 60)
let minutes = timestamp % (60 * 60) / 60
let seconds = timestamp % 60
if hours != 0 {
startTimeString = String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
startTimeString = String(format: "%d:%02d", minutes, seconds)
}
text = "Link with start time at \(startTimeString) copied to clipboard."
} else {
text = presentationData.strings.Conversation_LinkCopied
}
self.controllerInteraction?.presentController(UndoOverlayController(presentationData: presentationData, content: .linkCopied(title: nil, text: text), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
}
shareController.actionCompleted = { [weak self] in
if let strongSelf = self, let actionCompletionText = actionCompletionText {

View File

@ -42,6 +42,8 @@ final class ChatVideoGalleryItemScrubberView: UIView {
private var currentChapter: MediaPlayerScrubbingChapter?
private var isAnimatedOut: Bool = false
var hideWhenDurationIsUnknown = false {
didSet {
if self.hideWhenDurationIsUnknown {
@ -150,6 +152,9 @@ final class ChatVideoGalleryItemScrubberView: UIView {
}
func updateTimestampsVisibility(animated: Bool) {
if self.isAnimatedOut {
return
}
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
let alpha: CGFloat = self.isCollapsed == true || self.isLoading ? 0.0 : 1.0
transition.updateAlpha(node: self.leftTimestampNode, alpha: alpha)
@ -375,4 +380,80 @@ final class ChatVideoGalleryItemScrubberView: UIView {
}
return hitTestRect.contains(point)
}
func animateIn(from scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) {
if let scrubberTransition {
let fromRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self)
let targetCloneView = scrubberTransition.makeView()
self.addSubview(targetCloneView)
targetCloneView.frame = fromRect
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 0.0), .immediate)
targetCloneView.alpha = 1.0
transition.updateFrame(view: targetCloneView, frame: CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - fromRect.height - 3.0), size: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height)))
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: fromRect.size, destinationSize: CGSize(width: self.scrubberNode.bounds.width, height: fromRect.height), progress: 1.0), transition)
let scrubberTransitionView = scrubberTransition.view
scrubberTransitionView.isHidden = true
ContainedViewLayoutTransition.animated(duration: 0.08, curve: .easeInOut).updateAlpha(layer: targetCloneView.layer, alpha: 0.0, completion: { [weak scrubberTransitionView, weak targetCloneView] _ in
scrubberTransitionView?.isHidden = false
targetCloneView?.removeFromSuperview()
})
let scrubberSourceRect = CGRect(origin: CGPoint(x: fromRect.minX, y: fromRect.maxY - 3.0), size: CGSize(width: fromRect.width, height: 3.0))
let leftTimestampOffset = CGPoint(x: self.leftTimestampNode.position.x - self.scrubberNode.frame.minX, y: self.leftTimestampNode.position.y - self.scrubberNode.frame.maxY)
let rightTimestampOffset = CGPoint(x: self.rightTimestampNode.position.x - self.scrubberNode.frame.maxX, y: self.rightTimestampNode.position.y - self.scrubberNode.frame.maxY)
transition.animatePosition(node: self.scrubberNode, from: scrubberSourceRect.center)
self.scrubberNode.animateWidth(from: scrubberSourceRect.width, transition: transition)
transition.animatePosition(node: self.leftTimestampNode, from: CGPoint(x: leftTimestampOffset.x + scrubberSourceRect.minX, y: leftTimestampOffset.y + scrubberSourceRect.maxY))
transition.animatePosition(node: self.rightTimestampNode, from: CGPoint(x: rightTimestampOffset.x + scrubberSourceRect.maxX, y: rightTimestampOffset.y + scrubberSourceRect.maxY))
}
self.scrubberNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25)
self.leftTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25)
self.rightTimestampNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25)
self.infoNode.layer.animateAlpha(from: 0.0, to: self.leftTimestampNode.alpha, duration: 0.25)
}
func animateOut(to scrubberTransition: GalleryItemScrubberTransition?, transition: ContainedViewLayoutTransition) {
self.isAnimatedOut = true
if let scrubberTransition {
let toRect = scrubberTransition.view.convert(scrubberTransition.view.bounds, to: self)
let scrubberDestinationRect = CGRect(origin: CGPoint(x: toRect.minX, y: toRect.maxY - 3.0), size: CGSize(width: toRect.width, height: 3.0))
let targetCloneView = scrubberTransition.makeView()
self.addSubview(targetCloneView)
targetCloneView.frame = CGRect(origin: CGPoint(x: self.scrubberNode.frame.minX, y: self.scrubberNode.frame.maxY - toRect.height), size: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height))
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 0.0), .immediate)
targetCloneView.alpha = 0.0
transition.updateFrame(view: targetCloneView, frame: toRect)
scrubberTransition.updateView(targetCloneView, GalleryItemScrubberTransition.TransitionState(sourceSize: CGSize(width: self.scrubberNode.bounds.width, height: toRect.height), destinationSize: toRect.size, progress: 1.0), transition)
let scrubberTransitionView = scrubberTransition.view
scrubberTransitionView.isHidden = true
transition.updateAlpha(layer: targetCloneView.layer, alpha: 1.0, completion: { [weak scrubberTransitionView] _ in
scrubberTransitionView?.isHidden = false
})
let leftTimestampOffset = CGPoint(x: self.leftTimestampNode.position.x - self.scrubberNode.frame.minX, y: self.leftTimestampNode.position.y - self.scrubberNode.frame.maxY)
let rightTimestampOffset = CGPoint(x: self.rightTimestampNode.position.x - self.scrubberNode.frame.maxX, y: self.rightTimestampNode.position.y - self.scrubberNode.frame.maxY)
transition.animatePositionAdditive(layer: self.scrubberNode.layer, offset: CGPoint(), to: CGPoint(x: scrubberDestinationRect.midX - self.scrubberNode.position.x, y: scrubberDestinationRect.midY - self.scrubberNode.position.y), removeOnCompletion: false)
self.scrubberNode.animateWidth(to: scrubberDestinationRect.width, transition: transition)
transition.animatePositionAdditive(layer: self.leftTimestampNode.layer, offset: CGPoint(), to: CGPoint(x: -self.leftTimestampNode.position.x + (leftTimestampOffset.x + scrubberDestinationRect.minX), y: -self.leftTimestampNode.position.y + (leftTimestampOffset.y + scrubberDestinationRect.maxY)), removeOnCompletion: false)
transition.animatePositionAdditive(layer: self.rightTimestampNode.layer, offset: CGPoint(), to: CGPoint(x: -self.rightTimestampNode.position.x + (rightTimestampOffset.x + scrubberDestinationRect.maxX), y: -self.rightTimestampNode.position.y + (rightTimestampOffset.y + scrubberDestinationRect.maxY)), removeOnCompletion: false)
}
transition.updateAlpha(layer: self.scrubberNode.layer, alpha: 0.0)
transition.updateAlpha(layer: self.leftTimestampNode.layer, alpha: 0.0)
transition.updateAlpha(layer: self.rightTimestampNode.layer, alpha: 0.0)
transition.updateAlpha(layer: self.infoNode.layer, alpha: 0.0)
}
}

View File

@ -83,6 +83,13 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
}
}
self.pager.controlsVisibility = { [weak self] in
guard let self else {
return true
}
return !self.areControlsHidden && self.footerNode.alpha != 0.0
}
self.pager.updateOrientation = { [weak self] orientation in
if let strongSelf = self {
strongSelf.updateOrientation?(orientation)
@ -364,11 +371,15 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
if !self.areControlsHidden {
self.statusBar?.alpha = 1.0
self.navigationBar?.alpha = 1.0
self.footerNode.alpha = 1.0
self.updateThumbnailContainerNodeAlpha(.immediate)
}
})
if !self.areControlsHidden {
self.footerNode.alpha = 1.0
self.footerNode.animateIn(transition: .animated(duration: 0.15, curve: .linear))
}
if animateContent {
self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), to: self.scrollView.layer.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
} else if useSimpleAnimation {
@ -402,13 +413,14 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture
UIView.animate(withDuration: 0.1, animations: {
self.statusBar?.alpha = 0.0
self.navigationBar?.alpha = 0.0
self.footerNode.alpha = 0.0
self.currentThumbnailContainerNode?.alpha = 0.0
}, completion: { _ in
interfaceAnimationCompleted = true
intermediateCompletion()
})
self.footerNode.animateOut(transition: .animated(duration: 0.1, curve: .easeInOut))
if animateContent {
contentAnimationCompleted = false
self.scrollView.layer.animateBounds(from: self.scrollView.layer.bounds, to: self.scrollView.layer.bounds.offsetBy(dx: 0.0, dy: -self.scrollView.layer.bounds.size.height), duration: 0.25, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { _ in

View File

@ -37,9 +37,18 @@ open class GalleryFooterContentNode: ASDisplayNode {
return 0.0
}
open func animateIn(transition: ContainedViewLayoutTransition) {
self.alpha = 0.0
transition.updateAlpha(node: self, alpha: 1.0)
}
open func animateIn(fromHeight: CGFloat, previousContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition) {
}
open func animateOut(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self, alpha: 0.0)
}
open func animateOut(toHeight: CGFloat, nextContentNode: GalleryFooterContentNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
completion()
}

View File

@ -32,6 +32,32 @@ public final class GalleryFooterNode: ASDisplayNode {
self.currentOverlayContentNode?.setVisibilityAlpha(alpha)
}
func animateIn(transition: ContainedViewLayoutTransition) {
self.backgroundNode.alpha = 0.0
transition.updateAlpha(node: self.backgroundNode, alpha: 1.0)
if let currentFooterContentNode = self.currentFooterContentNode {
currentFooterContentNode.animateIn(transition: transition)
}
if let currentOverlayContentNode = self.currentOverlayContentNode {
currentOverlayContentNode.alpha = 0.0
transition.updateAlpha(node: currentOverlayContentNode, alpha: 1.0)
}
}
func animateOut(transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
if let currentFooterContentNode = self.currentFooterContentNode {
currentFooterContentNode.animateOut(transition: transition)
}
if let currentOverlayContentNode = self.currentOverlayContentNode {
transition.updateAlpha(node: currentOverlayContentNode, alpha: 0.0)
}
}
public func updateLayout(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, footerContentNode: GalleryFooterContentNode?, overlayContentNode: GalleryOverlayContentNode?, thumbnailPanelHeight: CGFloat, isHidden: Bool, transition: ContainedViewLayoutTransition) {
self.currentLayout = (layout, navigationBarHeight, thumbnailPanelHeight, isHidden)
let cleanInsets = layout.insets(options: [])

View File

@ -27,6 +27,7 @@ open class GalleryItemNode: ASDisplayNode {
public var toggleControlsVisibility: () -> Void = { }
public var updateControlsVisibility: (Bool) -> Void = { _ in }
public var controlsVisibility: () -> Bool = { return true }
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in }

View File

@ -1,8 +1,42 @@
import Foundation
import UIKit
import AccountContext
import Display
public final class GalleryItemScrubberTransition {
public struct TransitionState: Equatable {
public var sourceSize: CGSize
public var destinationSize: CGSize
public var progress: CGFloat
public init(
sourceSize: CGSize,
destinationSize: CGSize,
progress: CGFloat
) {
self.sourceSize = sourceSize
self.destinationSize = destinationSize
self.progress = progress
}
}
public let view: UIView
public let makeView: () -> UIView
public let updateView: (UIView, TransitionState, ContainedViewLayoutTransition) -> Void
public let insertCloneTransitionView: ((UIView) -> Void)?
public init(view: UIView, makeView: @escaping () -> UIView, updateView: @escaping (UIView, TransitionState, ContainedViewLayoutTransition) -> Void, insertCloneTransitionView: ((UIView) -> Void)?) {
self.view = view
self.makeView = makeView
self.updateView = updateView
self.insertCloneTransitionView = insertCloneTransitionView
}
}
public protocol GalleryItemTransitionNode: AnyObject {
func isAvailableForGalleryTransition() -> Bool
func isAvailableForInstantPageTransition() -> Bool
var decoration: UniversalVideoDecoration? { get }
func scrubberTransition() -> GalleryItemScrubberTransition?
}

View File

@ -114,6 +114,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
public var centralItemIndexOffsetUpdated: (([GalleryItem]?, Int, CGFloat)?) -> Void = { _ in }
public var toggleControlsVisibility: () -> Void = { }
public var updateControlsVisibility: (Bool) -> Void = { _ in }
public var controlsVisibility: () -> Bool = { return true }
public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in }
public var dismiss: () -> Void = { }
public var beginCustomDismiss: (Bool) -> Void = { _ in }
@ -595,6 +596,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest
let node = self.items[index].node(synchronous: synchronous)
node.toggleControlsVisibility = self.toggleControlsVisibility
node.updateControlsVisibility = self.updateControlsVisibility
node.controlsVisibility = self.controlsVisibility
node.updateOrientation = self.updateOrientation
node.dismiss = self.dismiss
node.beginCustomDismiss = self.beginCustomDismiss

View File

@ -1404,17 +1404,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.clipsToBounds = true
//TODO:wip-release
/*self.footerContentNode.shareMediaParameters = { [weak self] in
self.footerContentNode.shareMediaParameters = { [weak self] in
guard let self, let playerStatusValue = self.playerStatusValue else {
return nil
}
if playerStatusValue.duration >= 60.0 * 10.0 {
return ShareControllerSubject.MediaParameters(startAtTimestamp: Int32(playerStatusValue.timestamp))
var publicLinkPrefix: ShareControllerSubject.PublicLinkPrefix?
if case let .message(message, _) = self.item?.contentInfo, message.id.namespace == Namespaces.Message.Cloud, let peer = message.peers[message.id.peerId] as? TelegramChannel, let username = peer.username {
let visibleString = "t.me/\(username)/\(message.id.id)"
publicLinkPrefix = ShareControllerSubject.PublicLinkPrefix(
visibleString: visibleString,
actualString: "https://\(visibleString)"
)
}
return ShareControllerSubject.MediaParameters(
startAtTimestamp: Int32(playerStatusValue.timestamp),
publicLinkPrefix: publicLinkPrefix
)
} else {
return nil
}
}*/
}
self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside)
self.settingsBarButton.addTarget(self, action: #selector(self.settingsButtonPressed), forControlEvents: .touchUpInside)
@ -2456,6 +2468,10 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
if let node = node.0 as? OverlayMediaItemNode, self.context.sharedContext.mediaManager.hasOverlayVideoNode(node) {
if let scrubberView = self.scrubberView {
scrubberView.animateIn(from: nil, transition: .animated(duration: 0.25, curve: .spring))
}
var transformedFrame = node.view.convert(node.view.bounds, to: videoNode.view)
let transformedSuperFrame = node.view.convert(node.view.bounds, to: videoNode.view.superview)
@ -2471,6 +2487,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.context.sharedContext.mediaManager.setOverlayVideoNode(nil)
} else {
if let scrubberView = self.scrubberView {
let scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
scrubberView.animateIn(from: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring))
}
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
var transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
@ -2570,6 +2591,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return
}
if let scrubberView = self.scrubberView {
var scrubberTransition = (node.0 as? GalleryItemTransitionNode)?.scrubberTransition()
if !self.controlsVisibility() {
scrubberTransition = nil
}
scrubberView.animateOut(to: scrubberTransition, transition: .animated(duration: 0.25, curve: .spring))
}
let transformedFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view)
var transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: videoNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)

View File

@ -177,4 +177,8 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
}
}
}
func scrubberTransition() -> GalleryItemScrubberTransition? {
return nil
}
}

View File

@ -168,11 +168,14 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
private let mediaDataReaderParams: MediaDataReaderParams
private let audioSessionManager: ManagedAudioSession
private let onSeeked: (() -> Void)?
private weak var playerNode: MediaPlayerNode?
private let renderSynchronizer: AVSampleBufferRenderSynchronizer
private var videoRenderer: AVSampleBufferDisplayLayer
private var audioRenderer: AVSampleBufferAudioRenderer?
private var didNotifySentVideoFrames: Bool = false
private var partsState = ChunkMediaPlayerPartsState(duration: nil, content: .parts([]))
private var loadedParts: [LoadedPart] = []
private var loadedPartsMediaData: QueueLocalObject<LoadedPartsMediaData>
@ -245,6 +248,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
self.mediaDataReaderParams = params
self.audioSessionManager = audioSessionManager
self.onSeeked = onSeeked
self.playerNode = playerNode
self.loadedPartsMediaData = QueueLocalObject(queue: self.dataQueue, generate: {
return LoadedPartsMediaData()
@ -938,10 +942,11 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
videoTarget = self.videoRenderer
}
let didNotifySentVideoFrames = self.didNotifySentVideoFrames
videoTarget.requestMediaDataWhenReady(on: self.dataQueue.queue, using: { [weak self] in
if let loadedPartsMediaData = loadedPartsMediaData.unsafeGet() {
let bufferIsReadyForMoreData = ChunkMediaPlayerV2.fillRendererBuffer(bufferTarget: videoTarget, loadedPartsMediaData: loadedPartsMediaData, isVideo: true)
if bufferIsReadyForMoreData {
let bufferFillResult = ChunkMediaPlayerV2.fillRendererBuffer(bufferTarget: videoTarget, loadedPartsMediaData: loadedPartsMediaData, isVideo: true)
if bufferFillResult.bufferIsReadyForMoreData {
videoTarget.stopRequestingMediaData()
Queue.mainQueue().async {
guard let self else {
@ -951,6 +956,21 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
self.updateInternalState()
}
}
if !didNotifySentVideoFrames {
Queue.mainQueue().async {
guard let self else {
return
}
if self.didNotifySentVideoFrames {
return
}
self.didNotifySentVideoFrames = true
if #available(iOS 17.4, *) {
} else {
self.playerNode?.hasSentFramesToDisplay?()
}
}
}
}
})
}
@ -961,8 +981,8 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
let audioTarget = audioRenderer
audioTarget.requestMediaDataWhenReady(on: self.dataQueue.queue, using: { [weak self] in
if let loadedPartsMediaData = loadedPartsMediaData.unsafeGet() {
let bufferIsReadyForMoreData = ChunkMediaPlayerV2.fillRendererBuffer(bufferTarget: audioTarget, loadedPartsMediaData: loadedPartsMediaData, isVideo: false)
if bufferIsReadyForMoreData {
let bufferFillResult = ChunkMediaPlayerV2.fillRendererBuffer(bufferTarget: audioTarget, loadedPartsMediaData: loadedPartsMediaData, isVideo: false)
if bufferFillResult.bufferIsReadyForMoreData {
audioTarget.stopRequestingMediaData()
Queue.mainQueue().async {
guard let self else {
@ -977,8 +997,9 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
}
}
private static func fillRendererBuffer(bufferTarget: AVQueuedSampleBufferRendering, loadedPartsMediaData: LoadedPartsMediaData, isVideo: Bool) -> Bool {
private static func fillRendererBuffer(bufferTarget: AVQueuedSampleBufferRendering, loadedPartsMediaData: LoadedPartsMediaData, isVideo: Bool) -> (bufferIsReadyForMoreData: Bool, didEnqueue: Bool) {
var bufferIsReadyForMoreData = true
var didEnqueue = false
outer: while true {
if !bufferTarget.isReadyForMoreMediaData {
bufferIsReadyForMoreData = false
@ -1077,6 +1098,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
print("Enqueue audio \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value) next: \(CMSampleBufferGetPresentationTimeStamp(sampleBuffer).value + 1024)")
}*/
bufferTarget.enqueue(sampleBuffer)
didEnqueue = true
hasData = true
continue outer
case .waitingForMoreData:
@ -1090,7 +1112,7 @@ public final class ChunkMediaPlayerV2: ChunkMediaPlayer {
}
}
return bufferIsReadyForMoreData
return (bufferIsReadyForMoreData: bufferIsReadyForMoreData, didEnqueue: didEnqueue)
}
}

View File

@ -1091,4 +1091,12 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode {
}
}
}
public func animateWidth(from: CGFloat, transition: ContainedViewLayoutTransition) {
transition.animateTransformScale(layer: self.layer, from: CGPoint(x: from / self.bounds.width, y: 1.0))
}
public func animateWidth(to: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateTransformScale(node: self, scale: CGPoint(x: to / self.bounds.width, y: 1.0))
}
}

View File

@ -41,6 +41,7 @@ swift_library(
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/UndoUI",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",

View File

@ -132,6 +132,8 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode {
return self.checkNode.selected
}
public var updated: (() -> Void)?
public init(titleText: String, titleTextColor: UIColor, checkNodeTheme: CheckNodeTheme) {
self.titleText = titleText
self.titleTextColor = titleTextColor
@ -154,6 +156,7 @@ public final class ShareStartAtTimestampNode: HighlightTrackingButtonNode {
@objc private func pressed() {
self.checkNode.setSelected(!self.checkNode.selected, animated: true)
self.updated?()
}
override public func layout() {

View File

@ -436,6 +436,8 @@ public final class ShareController: ViewController {
public var debugAction: (() -> Void)?
public var onMediaTimestampLinkCopied: ((Int32?) -> Void)?
public var parentNavigationController: NavigationController?
public convenience init(context: AccountContext, subject: ShareControllerSubject, presetText: String? = nil, preferredAction: ShareControllerPreferredAction = .default, showInChat: ((Message) -> Void)? = nil, fromForeignApp: Bool = false, segmentedValues: [ShareControllerSegmentedValue]? = nil, externalShare: Bool = true, immediateExternalShare: Bool = false, switchableAccounts: [AccountWithInfo] = [], immediatePeerId: PeerId? = nil, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, forceTheme: PresentationTheme? = nil, forcedActionTitle: String? = nil, shareAsLink: Bool = false, collectibleItemInfo: TelegramCollectibleItemInfo? = nil) {
@ -1210,6 +1212,12 @@ public final class ShareController: ViewController {
return false
}), in: .current)
}
self.controllerNode.onMediaTimestampLinkCopied = { [weak self] timestamp in
guard let self else {
return
}
self.onMediaTimestampLinkCopied?(timestamp)
}
self.controllerNode.debugAction = { [weak self] in
self?.debugAction?()
}

View File

@ -326,6 +326,8 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
private let fromPublicChannel: Bool
private let segmentedValues: [ShareControllerSegmentedValue]?
private let collectibleItemInfo: TelegramCollectibleItemInfo?
private let mediaParameters: ShareControllerSubject.MediaParameters?
var selectedSegmentedIndex: Int = 0
private let defaultAction: ShareControllerAction?
@ -365,6 +367,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
var enqueued: (([PeerId], [Int64]) -> Void)?
var present: ((ViewController) -> Void)?
var disabledPeerSelected: ((EnginePeer) -> Void)?
var onMediaTimestampLinkCopied: ((Int32?) -> Void)?
let ready = Promise<Bool>()
@ -397,6 +400,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.fromPublicChannel = fromPublicChannel
self.segmentedValues = segmentedValues
self.collectibleItemInfo = collectibleItemInfo
self.mediaParameters = mediaParameters
self.presetText = presetText
@ -471,7 +475,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
self.startAtTimestampNode = nil
}
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), placeholder: self.presentationData.strings.ShareMenu_Comment)
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: self.presentationData.theme), strings: self.presentationData.strings, placeholder: self.presentationData.strings.ShareMenu_Comment)
self.inputFieldNode.text = presetText ?? ""
self.inputFieldNode.preselectText()
self.inputFieldNode.alpha = 0.0
@ -490,6 +494,15 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
super.init()
self.isHidden = true
self.startAtTimestampNode?.updated = { [weak self] in
guard let self else {
return
}
if let (layout, navigationBarHeight, _) = self.containerLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
}
}
self.actionButtonNode.shouldBegin = { [weak self] in
if let strongSelf = self {
@ -593,7 +606,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
}
if !openedTopicList {
strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil, inputField: true, actions: strongSelf.defaultAction == nil)
strongSelf.setActionNodesHidden(strongSelf.controllerInteraction!.selectedPeers.isEmpty && strongSelf.presetText == nil && strongSelf.mediaParameters?.publicLinkPrefix == nil, inputField: true, actions: strongSelf.defaultAction == nil)
strongSelf.updateButton()
@ -686,10 +699,44 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
}
}
}
self.inputFieldNode.onInputCopyText = { [weak self] in
guard let self else {
return
}
if let publicLinkPrefix = self.mediaParameters?.publicLinkPrefix {
var timestampSuffix = ""
var effectiveStartTimestamp: Int32?
if let startAtTimestamp = self.mediaParameters?.startAtTimestamp, let startAtTimestampNode = self.startAtTimestampNode, startAtTimestampNode.value {
var startAtTimestampString = ""
let hours = startAtTimestamp / 3600
let minutes = startAtTimestamp / 60 % 60
let seconds = startAtTimestamp % 60
if hours == 0 && minutes == 0 {
startAtTimestampString = "\(startAtTimestamp)"
} else {
if hours != 0 {
startAtTimestampString += "\(hours)h"
}
if minutes != 0 {
startAtTimestampString += "\(minutes)m"
}
if seconds != 0 {
startAtTimestampString += "\(seconds)s"
}
}
timestampSuffix = "?t=\(startAtTimestampString)"
effectiveStartTimestamp = startAtTimestamp
}
let inputCopyText = "\(publicLinkPrefix.actualString)\(timestampSuffix)"
UIPasteboard.general.string = inputCopyText
self.onMediaTimestampLinkCopied?(effectiveStartTimestamp)
}
self.cancel?()
}
self.updateButton()
if self.presetText != nil {
if self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil {
self.setActionNodesHidden(false, inputField: true, actions: true, animated: false)
}
}
@ -971,7 +1018,7 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
if contentNode is ShareSearchContainerNode {
self.setActionNodesHidden(true, inputField: true, actions: true)
} else if !(contentNode is ShareLoadingContainer) {
self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil, actions: true)
self.setActionNodesHidden(false, inputField: !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil, actions: true)
}
} else {
if let contentNode = self.contentNode {
@ -1021,16 +1068,45 @@ final class ShareControllerNode: ViewControllerTracingNode, ASScrollViewDelegate
var bottomGridInset: CGFloat = 0
var actionButtonHeight: CGFloat = 0
if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil {
if self.defaultAction != nil || !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil {
actionButtonHeight = buttonHeight
bottomGridInset += actionButtonHeight
}
if self.startAtTimestampNode != nil {
bottomGridInset += buttonHeight
}
let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, transition: transition)
var inputCopyText: String?
if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil {
} else {
if let publicLinkPrefix = self.mediaParameters?.publicLinkPrefix {
var timestampSuffix = ""
if let startAtTimestamp = self.mediaParameters?.startAtTimestamp, let startAtTimestampNode = self.startAtTimestampNode, startAtTimestampNode.value {
var startAtTimestampString = ""
let hours = startAtTimestamp / 3600
let minutes = startAtTimestamp / 60 % 60
let seconds = startAtTimestamp % 60
if hours == 0 && minutes == 0 {
startAtTimestampString = "\(startAtTimestamp)"
} else {
if hours != 0 {
startAtTimestampString += "\(hours)h"
}
if minutes != 0 {
startAtTimestampString += "\(minutes)m"
}
if seconds != 0 {
startAtTimestampString += "\(seconds)s"
}
}
timestampSuffix = "?t=\(startAtTimestampString)"
}
inputCopyText = "\(publicLinkPrefix.visibleString)\(timestampSuffix)"
}
}
let inputHeight = self.inputFieldNode.updateLayout(width: contentContainerFrame.size.width, inputCopyText: inputCopyText, transition: transition)
if !self.controllerInteraction!.selectedPeers.isEmpty || self.presetText != nil || self.mediaParameters?.publicLinkPrefix != nil {
bottomGridInset += inputHeight
}

View File

@ -4,6 +4,9 @@ import AsyncDisplayKit
import Display
import TelegramPresentationData
import AppBundle
import ComponentFlow
import MultilineTextComponent
import AnimatedTextComponent
private func generateClearIcon(color: UIColor) -> UIImage? {
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
@ -55,19 +58,154 @@ public extension ShareInputFieldNodeTheme {
}
}
private final class ShareInputCopyComponent: Component {
let theme: ShareInputFieldNodeTheme
let strings: PresentationStrings
let text: String
let action: () -> Void
init(
theme: ShareInputFieldNodeTheme,
strings: PresentationStrings,
text: String,
action: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.text = text
self.action = action
}
static func ==(lhs: ShareInputCopyComponent, rhs: ShareInputCopyComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.text != rhs.text {
return false
}
return true
}
final class View: UIView {
let text = ComponentView<Empty>()
let button = ComponentView<Empty>()
let textMask = UIImageView()
var component: ShareInputCopyComponent?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: ShareInputCopyComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let textChanged = self.component != nil && self.component?.text != component.text
self.component = component
var textItems: [AnimatedTextComponent.Item] = []
if let range = component.text.range(of: "?", options: .backwards) {
textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(String(component.text[component.text.startIndex ..< range.lowerBound]))))
textItems.append(AnimatedTextComponent.Item(id: 1, isUnbreakable: true, content: .text(String(component.text[range.lowerBound...]))))
} else {
textItems.append(AnimatedTextComponent.Item(id: 0, isUnbreakable: true, content: .text(component.text)))
}
let sideInset: CGFloat = 12.0
let textSize = self.text.update(
transition: textChanged ? .spring(duration: 0.4) : .immediate,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(17.0),
color: component.theme.textColor,
items: textItems,
animateScale: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((availableSize.height - textSize.height) * 0.5)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
textView.mask = self.textMask
}
textView.frame = textFrame
}
let buttonSize = self.button.update(
transition: .immediate,
component: AnyComponent(Button(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.strings.Conversation_LinkDialogCopy, font: Font.regular(17.0), textColor: component.theme.accentColor))
)),
action: { [weak self] in
guard let self else {
return
}
self.component?.action()
}
).minSize(CGSize(width: 0.0, height: availableSize.height))),
environment: {},
containerSize: CGSize(width: availableSize.width - 40.0, height: 1000.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: availableSize.width - sideInset - buttonSize.width, y: floor((availableSize.height - buttonSize.height) * 0.5)), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
buttonView.frame = buttonFrame
}
if self.textMask.image == nil {
let gradientWidth: CGFloat = 26.0
self.textMask.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 8.0), colors: [
UIColor(white: 1.0, alpha: 1.0),
UIColor(white: 1.0, alpha: 1.0),
UIColor(white: 1.0, alpha: 0.0)
], locations: [
0.0,
1.0 / gradientWidth,
1.0
], direction: .horizontal)?.resizableImage(withCapInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: gradientWidth - 1.0), resizingMode: .stretch)
self.textMask.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, buttonFrame.minX - 4.0 - textFrame.minX), height: textFrame.height))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private let theme: ShareInputFieldNodeTheme
private let strings: PresentationStrings
private let backgroundNode: ASImageNode
private let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode
private let clearButton: HighlightableButtonNode
private var copyView: ComponentView<Empty>?
public var updateHeight: (() -> Void)?
public var updateText: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 1.0, right: 16.0)
private let inputInsets = UIEdgeInsets(top: 10.0, left: 8.0, bottom: 10.0, right: 22.0)
private let accessoryButtonsWidth: CGFloat = 10.0
private var inputCopyText: String?
public var onInputCopyText: (() -> Void)?
private var selectTextOnce: Bool = false
@ -77,7 +215,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
}
set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(17.0), textColor: self.theme.textColor)
self.placeholderNode.isHidden = !newValue.isEmpty
self.placeholderNode.isHidden = !newValue.isEmpty || self.inputCopyText != nil
self.clearButton.isHidden = newValue.isEmpty
}
}
@ -88,8 +226,9 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
}
}
public init(theme: ShareInputFieldNodeTheme, placeholder: String) {
public init(theme: ShareInputFieldNodeTheme, strings: PresentationStrings, placeholder: String) {
self.theme = theme
self.strings = strings
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
@ -136,10 +275,11 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
self.selectTextOnce = true
}
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
public func updateLayout(width: CGFloat, inputCopyText: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
self.inputCopyText = inputCopyText
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
@ -156,6 +296,43 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: backgroundFrame.size.height)))
self.textInputNode.isUserInteractionEnabled = inputCopyText == nil
self.textInputNode.isHidden = inputCopyText != nil
self.placeholderNode.isHidden = !(self.textInputNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
if let inputCopyText {
let copyView: ComponentView<Empty>
if let current = self.copyView {
copyView = current
} else {
copyView = ComponentView()
self.copyView = copyView
}
let copyViewSize = copyView.update(
transition: .immediate,
component: AnyComponent(ShareInputCopyComponent(
theme: self.theme,
strings: self.strings,
text: inputCopyText,
action: {
self.onInputCopyText?()
}
)),
environment: {},
containerSize: backgroundFrame.size
)
let copyViewFrame = CGRect(origin: backgroundFrame.origin, size: copyViewSize)
if let copyComponentView = copyView.view {
if copyComponentView.superview == nil {
self.view.addSubview(copyComponentView)
}
copyComponentView.frame = copyViewFrame
}
} else if let copyView = self.copyView {
self.copyView = nil
copyView.view?.removeFromSuperview()
}
return panelHeight
}
@ -170,7 +347,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.updateTextNodeText(animated: true)
self.updateText?(editableTextNode.attributedText?.string ?? "")
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
}
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
@ -185,7 +362,7 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty || self.inputCopyText != nil
self.clearButton.isHidden = true
}
@ -194,9 +371,13 @@ public final class ShareInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegat
let inputInsets = self.inputInsets
let accessoryButtonsWidth = self.accessoryButtonsWidth
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(41.0, unboundTextFieldHeight))
if self.inputCopyText != nil {
return 41.0
} else {
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(41.0, unboundTextFieldHeight))
}
}
private func updateTextNodeText(animated: Bool) {

View File

@ -517,18 +517,20 @@ public final class CallController: ViewController {
}
controller?.displayProgress = true
let _ = self.call.upgradeToConference(completion: { [weak self] _ in
guard let self else {
let call = self.call
controller?.dismiss()
let _ = self.call.upgradeToConference(completion: { [weak call] _ in
guard let call else {
return
}
for peerId in peerIds {
if case let .peer(peerId) = peerId {
let _ = (self.call as? PresentationCallImpl)?.requestAddToConference(peerId: peerId)
let _ = (call as? PresentationCallImpl)?.requestAddToConference(peerId: peerId)
}
}
controller?.dismiss()
})
})

View File

@ -1007,7 +1007,10 @@ public final class MediaStreamComponent: CombinedComponent {
public final class MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController {
private let context: AccountContext
public let call: PresentationGroupCall
public let callImpl: PresentationGroupCall
public var call: VideoChatCall {
return .group(self.callImpl)
}
public private(set) var currentOverlayController: VoiceChatOverlayController? = nil
public var parentNavigationController: NavigationController?
@ -1020,7 +1023,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
public init(call: PresentationGroupCall) {
self.context = call.accountContext
self.call = call
self.callImpl = call
super.init(context: call.accountContext, component: MediaStreamComponent(call: call as! PresentationGroupCallImpl), navigationBarAppearance: .none)
@ -1131,7 +1134,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
guard let strongSelf = self else {
return
}
guard let peerId = strongSelf.call.peerId else {
guard let peerId = strongSelf.callImpl.peerId else {
return
}
@ -1175,11 +1178,11 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
}
let _ = formatSendTitle
guard let peerId = self.call.peerId else {
guard let peerId = self.callImpl.peerId else {
return
}
let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(peerId), self.call.state |> take(1))
let _ = (combineLatest(queue: .mainQueue(), self.context.account.postbox.loadedPeerWithId(peerId), self.callImpl.state |> take(1))
|> deliverOnMainQueue).start(next: { [weak self] peer, callState in
if let strongSelf = self {
var inviteLinks = inviteLinks

View File

@ -120,10 +120,16 @@ public final class PresentationCallImpl: PresentationCall {
private var useFrontCamera: Bool = true
private var videoCapturer: OngoingCallVideoCapturer?
public var hasVideo: Bool {
return self.videoCapturer != nil
}
private var screencastBufferServerContext: IpcGroupCallBufferAppContext?
private var screencastCapturer: OngoingCallVideoCapturer?
private var isScreencastActive: Bool = false
public var hasScreencast: Bool {
return self.screencastCapturer != nil
}
private var proximityManagerIndex: Int?
@ -133,26 +139,22 @@ public final class PresentationCallImpl: PresentationCall {
private var conferenceCallImpl: PresentationGroupCallImpl?
public var conferenceCall: PresentationGroupCall? {
if !self.hasConferenceValue {
return nil
}
return self.conferenceCallImpl
}
private var conferenceCallDisposable: Disposable?
private var upgradedToConferenceCompletions = Bag<(PresentationGroupCall) -> Void>()
private var waitForConferenceCallReadyDisposable: Disposable?
private let hasConferencePromise = ValuePromise<Bool>(false)
private var hasConferenceValue: Bool = false {
private let conferenceStatePromise = ValuePromise<PresentationCallConferenceState?>(nil)
public private(set) var conferenceStateValue: PresentationCallConferenceState? {
didSet {
if self.hasConferenceValue != oldValue {
self.hasConferencePromise.set(self.hasConferenceValue)
if self.conferenceStateValue != oldValue {
self.conferenceStatePromise.set(self.conferenceStateValue)
}
}
}
public var hasConference: Signal<Bool, NoError> {
return self.hasConferencePromise.get()
public var conferenceState: Signal<PresentationCallConferenceState?, NoError> {
return self.conferenceStatePromise.get()
}
private var localVideoEndpointId: String?
@ -367,6 +369,13 @@ public final class PresentationCallImpl: PresentationCall {
}
}
func internal_markAsCanBeRemoved() {
if !self.didSetCanBeRemoved {
self.didSetCanBeRemoved = true
self.canBeRemovedPromise.set(.single(true))
}
}
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) {
self.reception = reception
@ -602,14 +611,9 @@ public final class PresentationCallImpl: PresentationCall {
break
}
if let (key, keyVisualHash, conferenceCall) = conferenceCallData {
if let (key, _, conferenceCall) = conferenceCallData {
if self.conferenceCallDisposable == nil {
self.conferenceCallDisposable = EmptyDisposable
self.ongoingContextStateDisposable?.dispose()
self.ongoingContextStateDisposable = nil
self.ongoingContext?.stop(debugLogValue: Promise())
self.ongoingContext = nil
let conferenceCall = PresentationGroupCallImpl(
accountContext: self.context,
@ -636,183 +640,13 @@ public final class PresentationCallImpl: PresentationCall {
sharedAudioDevice: self.sharedAudioDevice
)
self.conferenceCallImpl = conferenceCall
conferenceCall.upgradedConferenceCall = self
conferenceCall.setIsMuted(action: self.isMutedValue ? .muted(isPushToTalkActive: false) : .unmuted)
if let videoCapturer = self.videoCapturer {
conferenceCall.requestVideo(capturer: videoCapturer)
}
let accountPeerId = conferenceCall.account.peerId
let videoEndpoints: Signal<(local: String?, remote: PresentationGroupCallRequestedVideo?), NoError> = conferenceCall.members
|> map { members -> (local: String?, remote: PresentationGroupCallRequestedVideo?) in
guard let members else {
return (nil, nil)
}
var local: String?
var remote: PresentationGroupCallRequestedVideo?
for participant in members.participants {
if let video = participant.requestedPresentationVideoChannel(minQuality: .thumbnail, maxQuality: .full) ?? participant.requestedVideoChannel(minQuality: .thumbnail, maxQuality: .full) {
if participant.peer.id == accountPeerId {
local = video.endpointId
} else {
if remote == nil {
remote = video
}
}
}
}
return (local, remote)
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
var startTimestamp: Double?
self.ongoingContextStateDisposable = (combineLatest(queue: .mainQueue(),
conferenceCall.state,
videoEndpoints,
conferenceCall.signalBars,
conferenceCall.isFailed
)
|> deliverOnMainQueue).startStrict(next: { [weak self] callState, videoEndpoints, signalBars, isFailed in
guard let self else {
return
}
var mappedLocalVideoState: PresentationCallState.VideoState = .inactive
var mappedRemoteVideoState: PresentationCallState.RemoteVideoState = .inactive
if let local = videoEndpoints.local {
mappedLocalVideoState = .active(isScreencast: false, endpointId: local)
}
if let remote = videoEndpoints.remote {
mappedRemoteVideoState = .active(endpointId: remote.endpointId)
}
self.localVideoEndpointId = videoEndpoints.local
self.remoteVideoEndpointId = videoEndpoints.remote?.endpointId
if let conferenceCall = self.conferenceCall {
var requestedVideo: [PresentationGroupCallRequestedVideo] = []
if let remote = videoEndpoints.remote {
requestedVideo.append(remote)
}
conferenceCall.setRequestedVideoList(items: requestedVideo)
}
let mappedState: PresentationCallState.State
if isFailed {
mappedState = .terminating(.error(.disconnected))
} else {
switch callState.networkState {
case .connecting:
mappedState = .connecting(keyVisualHash)
case .connected:
let timestamp = startTimestamp ?? CFAbsoluteTimeGetCurrent()
startTimestamp = timestamp
mappedState = .active(timestamp, signalBars, keyVisualHash)
}
}
if !self.didDropCall && !self.droppedCall {
/*let presentationState = PresentationCallState(
state: mappedState,
videoState: mappedLocalVideoState,
remoteVideoState: mappedRemoteVideoState,
remoteAudioState: .active,
remoteBatteryLevel: .normal
)*/
let _ = mappedState
let timestamp: Double
if let activeTimestamp = self.activeTimestamp {
timestamp = activeTimestamp
} else {
timestamp = CFAbsoluteTimeGetCurrent()
self.activeTimestamp = timestamp
}
mappedLocalVideoState = .inactive
mappedRemoteVideoState = .inactive
if self.videoCapturer != nil {
mappedLocalVideoState = .active(isScreencast: false, endpointId: "local")
}
if let callContextState = self.callContextState {
switch callContextState.remoteVideoState {
case .active, .paused:
mappedRemoteVideoState = .active(endpointId: "temp-\(self.peerId.toInt64())")
case .inactive:
break
}
}
let presentationState = PresentationCallState(
state: .active(timestamp, signalBars, keyVisualHash),
videoState: mappedLocalVideoState,
remoteVideoState: mappedRemoteVideoState,
remoteAudioState: .active,
remoteBatteryLevel: .normal
)
self.statePromise.set(presentationState)
self.updateTone(presentationState, callContextState: nil, previous: nil)
}
})
self.ongoingContextIsFailedDisposable = (conferenceCall.isFailed
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
guard let self else {
return
}
if !self.didDropCall {
self.didDropCall = true
self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil))
}
})
self.ongoingContextIsDroppedDisposable = (conferenceCall.canBeRemoved
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
guard let self else {
return
}
if !self.didDropCall {
self.didDropCall = true
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: .single(nil))
}
})
var audioLevelId: UInt32?
let audioLevel = conferenceCall.audioLevels |> map { audioLevels -> Float in
var result: Float = 0
for item in audioLevels {
if let audioLevelId {
if item.1 == audioLevelId {
result = item.2
break
}
} else {
if item.1 != 0 {
audioLevelId = item.1
result = item.2
break
}
}
}
return result
}
self.audioLevelDisposable = (audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self {
strongSelf.audioLevelPromise.set(level)
}
})
let waitForLocalVideo = self.videoCapturer != nil
let waitForRemotePeerId: EnginePeer.Id? = self.peerId
@ -826,6 +660,8 @@ public final class PresentationCallImpl: PresentationCall {
}
}
self.conferenceStateValue = .preparing
self.waitForConferenceCallReadyDisposable?.dispose()
self.waitForConferenceCallReadyDisposable = (combineLatest(queue: .mainQueue(),
conferenceCall.state,
@ -881,7 +717,12 @@ public final class PresentationCallImpl: PresentationCall {
guard let self else {
return
}
self.hasConferenceValue = true
self.ongoingContext?.stop(debugLogValue: Promise())
self.ongoingContext = nil
self.ongoingContextStateDisposable?.dispose()
self.conferenceStateValue = .ready
let upgradedToConferenceCompletions = self.upgradedToConferenceCompletions.copyItems()
self.upgradedToConferenceCompletions.removeAll()
@ -974,7 +815,6 @@ public final class PresentationCallImpl: PresentationCall {
if wasActive {
let debugLogValue = Promise<String?>()
self.ongoingContext?.stop(sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue)
let _ = self.conferenceCallImpl?.leave(terminateIfPossible: false).start()
}
case .dropping:
break
@ -982,12 +822,7 @@ public final class PresentationCallImpl: PresentationCall {
self.audioSessionShouldBeActive.set(false)
if wasActive {
let debugLogValue = Promise<String?>()
if let conferenceCall = self.conferenceCallImpl {
debugLogValue.set(conferenceCall.debugLog.get())
let _ = conferenceCall.leave(terminateIfPossible: false).start()
} else {
self.ongoingContext?.stop(debugLogValue: debugLogValue)
}
self.ongoingContext?.stop(debugLogValue: debugLogValue)
}
}
var terminating = false
@ -1184,12 +1019,7 @@ public final class PresentationCallImpl: PresentationCall {
public func hangUp() -> Signal<Bool, NoError> {
let debugLogValue = Promise<String?>()
self.callSessionManager.drop(internalId: self.internalId, reason: .hangUp, debugLog: debugLogValue.get())
if let conferenceCall = self.conferenceCallImpl {
debugLogValue.set(conferenceCall.debugLog.get())
let _ = conferenceCall.leave(terminateIfPossible: false).start()
} else {
self.ongoingContext?.stop(debugLogValue: debugLogValue)
}
self.ongoingContext?.stop(debugLogValue: debugLogValue)
return self.hungUpPromise.get()
}
@ -1197,12 +1027,7 @@ public final class PresentationCallImpl: PresentationCall {
public func rejectBusy() {
self.callSessionManager.drop(internalId: self.internalId, reason: .busy, debugLog: .single(nil))
let debugLog = Promise<String?>()
if let conferenceCall = self.conferenceCallImpl {
debugLog.set(conferenceCall.debugLog.get())
let _ = conferenceCall.leave(terminateIfPossible: false).start()
} else {
self.ongoingContext?.stop(debugLogValue: debugLog)
}
self.ongoingContext?.stop(debugLogValue: debugLog)
}
public func toggleIsMuted() {
@ -1223,8 +1048,17 @@ public final class PresentationCallImpl: PresentationCall {
if let videoCapturer = self.videoCapturer {
if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(videoCapturer)
} else if let conferenceCall = self.conferenceCallImpl {
conferenceCall.requestVideo(capturer: videoCapturer)
}
}
}
public func requestVideo(capturer: OngoingCallVideoCapturer) {
if self.videoCapturer == nil {
self.videoCapturer = capturer
}
if let videoCapturer = self.videoCapturer {
if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(videoCapturer)
}
}
}
@ -1239,8 +1073,6 @@ public final class PresentationCallImpl: PresentationCall {
self.videoCapturer = nil
if let ongoingContext = self.ongoingContext {
ongoingContext.disableVideo()
} else if let conferenceCall = self.conferenceCallImpl {
conferenceCall.disableVideo()
}
}
}
@ -1289,8 +1121,6 @@ public final class PresentationCallImpl: PresentationCall {
self.isScreencastActive = true
if let ongoingContext = self.ongoingContext {
ongoingContext.requestVideo(screencastCapturer)
} else if let conferenceCall = self.conferenceCallImpl {
conferenceCall.requestVideo(capturer: screencastCapturer)
}
}
}
@ -1388,8 +1218,6 @@ public final class PresentationCallImpl: PresentationCall {
if isIncoming {
if let ongoingContext = self.ongoingContext {
return ongoingContext.video(isIncoming: isIncoming)
} else if let conferenceCall = self.conferenceCallImpl, let remoteVideoEndpointId = self.remoteVideoEndpointId {
return conferenceCall.video(endpointId: remoteVideoEndpointId)
} else {
return nil
}

View File

@ -856,6 +856,10 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
return .joined
}
public func switchToConference(call: PresentationCall) {
}
private func startGroupCall(
accountContext: AccountContext,
peerId: PeerId,

View File

@ -538,6 +538,7 @@ private final class ScreencastInProcessIPCContext: ScreencastIPCContext {
onMutedSpeechActivityDetected: { _ in },
encryptionKey: nil,
isConference: self.isConference,
isStream: false,
sharedAudioDevice: nil
)
)
@ -1006,6 +1007,8 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
let debugLog = Promise<String?>()
weak var upgradedConferenceCall: PresentationCallImpl?
init(
accountContext: AccountContext,
audioSession: ManagedAudioSession,
@ -1869,7 +1872,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
}
strongSelf.onMutedSpeechActivityDetected?(value)
}
}, encryptionKey: encryptionKey, isConference: self.isConference, sharedAudioDevice: self.sharedAudioDevice))
}, encryptionKey: encryptionKey, isConference: self.isConference, isStream: self.isStream, sharedAudioDevice: self.sharedAudioDevice))
}
self.genericCallContext = genericCallContext
@ -2795,6 +2798,10 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
self._canBeRemoved.set(.single(true))
if let upgradedConferenceCall = self.upgradedConferenceCall {
upgradedConferenceCall.internal_markAsCanBeRemoved()
}
if self.didConnectOnce {
if let callManager = self.accountContext.sharedContext.callManager {
let _ = (callManager.currentGroupCallSignal

View File

@ -14,7 +14,7 @@ import AvatarNode
import ContextUI
final class VideoChatParticipantThumbnailComponent: Component {
let call: PresentationGroupCall
let call: VideoChatCall
let theme: PresentationTheme
let participant: GroupCallParticipantsContext.Participant
let isPresentation: Bool
@ -26,7 +26,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
call: VideoChatCall,
theme: PresentationTheme,
participant: GroupCallParticipantsContext.Participant,
isPresentation: Bool,
@ -50,7 +50,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
}
static func ==(lhs: VideoChatParticipantThumbnailComponent, rhs: VideoChatParticipantThumbnailComponent) -> Bool {
if lhs.call !== rhs.call {
if lhs.call != rhs.call {
return false
}
if lhs.theme !== rhs.theme {
@ -280,7 +280,7 @@ final class VideoChatParticipantThumbnailComponent: Component {
videoLayer.blurredLayer.opacity = 0.25
if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) {
if let input = component.call.video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource
@ -474,7 +474,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
}
}
let call: PresentationGroupCall
let call: VideoChatCall
let theme: PresentationTheme
let displayVideo: Bool
let participants: [Participant]
@ -485,7 +485,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
init(
call: PresentationGroupCall,
call: VideoChatCall,
theme: PresentationTheme,
displayVideo: Bool,
participants: [Participant],
@ -507,7 +507,7 @@ final class VideoChatExpandedParticipantThumbnailsComponent: Component {
}
static func ==(lhs: VideoChatExpandedParticipantThumbnailsComponent, rhs: VideoChatExpandedParticipantThumbnailsComponent) -> Bool {
if lhs.call !== rhs.call {
if lhs.call != rhs.call {
return false
}
if lhs.theme !== rhs.theme {

View File

@ -188,7 +188,7 @@ final class VideoChatMicButtonComponent: Component {
case scheduled(state: ScheduledState)
}
let call: PresentationGroupCall
let call: VideoChatCall
let strings: PresentationStrings
let content: Content
let isCollapsed: Bool
@ -197,7 +197,7 @@ final class VideoChatMicButtonComponent: Component {
let scheduleAction: () -> Void
init(
call: PresentationGroupCall,
call: VideoChatCall,
strings: PresentationStrings,
content: Content,
isCollapsed: Bool,
@ -215,6 +215,9 @@ final class VideoChatMicButtonComponent: Component {
}
static func ==(lhs: VideoChatMicButtonComponent, rhs: VideoChatMicButtonComponent) -> Bool {
if lhs.call != rhs.call {
return false
}
if lhs.content != rhs.content {
return false
}
@ -612,8 +615,8 @@ final class VideoChatMicButtonComponent: Component {
switch component.content {
case .unmuted:
if self.audioLevelDisposable == nil {
self.audioLevelDisposable = (component.call.myAudioLevel
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
self.audioLevelDisposable = (component.call.myAudioLevelAndSpeaking
|> deliverOnMainQueue).startStrict(next: { [weak self] value, _ in
guard let self, let blobView = self.blobView else {
return
}

View File

@ -132,14 +132,14 @@ private final class BlobView: UIView {
}
final class VideoChatParticipantAvatarComponent: Component {
let call: PresentationGroupCall
let call: VideoChatCall
let peer: EnginePeer
let myPeerId: EnginePeer.Id
let isSpeaking: Bool
let theme: PresentationTheme
init(
call: PresentationGroupCall,
call: VideoChatCall,
peer: EnginePeer,
myPeerId: EnginePeer.Id,
isSpeaking: Bool,
@ -153,7 +153,7 @@ final class VideoChatParticipantAvatarComponent: Component {
}
static func ==(lhs: VideoChatParticipantAvatarComponent, rhs: VideoChatParticipantAvatarComponent) -> Bool {
if lhs.call !== rhs.call {
if lhs.call != rhs.call {
return false
}
if lhs.peer != rhs.peer {

View File

@ -40,7 +40,7 @@ private let activityBorderImage: UIImage = {
final class VideoChatParticipantVideoComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let call: PresentationGroupCall
let call: VideoChatCall
let participant: GroupCallParticipantsContext.Participant
let isMyPeer: Bool
let isPresentation: Bool
@ -59,7 +59,7 @@ final class VideoChatParticipantVideoComponent: Component {
init(
theme: PresentationTheme,
strings: PresentationStrings,
call: PresentationGroupCall,
call: VideoChatCall,
participant: GroupCallParticipantsContext.Participant,
isMyPeer: Bool,
isPresentation: Bool,
@ -95,6 +95,9 @@ final class VideoChatParticipantVideoComponent: Component {
}
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
if lhs.call != rhs.call {
return false
}
if lhs.participant != rhs.participant {
return false
}
@ -514,7 +517,7 @@ final class VideoChatParticipantVideoComponent: Component {
videoLayer.blurredLayer.opacity = 0.0
if let input = (component.call as! PresentationGroupCallImpl).video(endpointId: videoDescription.endpointId) {
if let input = component.call.video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource

View File

@ -126,7 +126,7 @@ final class VideoChatParticipantsComponent: Component {
}
}
let call: PresentationGroupCall
let call: VideoChatCall
let participants: Participants?
let speakingParticipants: Set<EnginePeer.Id>
let expandedVideoState: ExpandedVideoState?
@ -145,7 +145,7 @@ final class VideoChatParticipantsComponent: Component {
let visibleParticipantsUpdated: (Set<EnginePeer.Id>) -> Void
init(
call: PresentationGroupCall,
call: VideoChatCall,
participants: Participants?,
speakingParticipants: Set<EnginePeer.Id>,
expandedVideoState: ExpandedVideoState?,
@ -183,6 +183,9 @@ final class VideoChatParticipantsComponent: Component {
}
static func ==(lhs: VideoChatParticipantsComponent, rhs: VideoChatParticipantsComponent) -> Bool {
if lhs.call != rhs.call {
return false
}
if lhs.participants != rhs.participants {
return false
}
@ -1853,7 +1856,7 @@ final class VideoChatParticipantsComponent: Component {
}
}
}
(component.call as! PresentationGroupCallImpl).setRequestedVideoList(items: requestedVideo)
component.call.setRequestedVideoList(items: requestedVideo)
transition.setPosition(view: self.scrollViewClippingContainer, position: itemLayout.scrollClippingFrame.center)
transition.setBounds(view: self.scrollViewClippingContainer, bounds: CGRect(origin: CGPoint(x: itemLayout.scrollClippingFrame.minX - itemLayout.listFrame.minX, y: itemLayout.scrollClippingFrame.minY - itemLayout.listFrame.minY), size: itemLayout.scrollClippingFrame.size))

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import PresentationDataUtils
extension VideoChatScreenComponent.View {
func openInviteMembers() {
guard let component = self.component else {
guard case let .group(groupCall) = self.currentCall else {
return
}
@ -38,16 +38,16 @@ extension VideoChatScreenComponent.View {
guard let inviteType else {
return
}
guard let peerId = component.call.peerId else {
guard let peerId = groupCall.peerId else {
return
}
switch inviteType {
case .invite:
let groupPeer = component.call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
let groupPeer = groupCall.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
let _ = (groupPeer
|> deliverOnMainQueue).start(next: { [weak self] groupPeer in
guard let self, let component = self.component, let environment = self.environment, let groupPeer else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let groupPeer else {
return
}
let inviteLinks = self.inviteLinks
@ -81,8 +81,8 @@ extension VideoChatScreenComponent.View {
filters.append(.excludeBots)
var dismissController: (() -> Void)?
let controller = ChannelMembersSearchController(context: component.call.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
guard let self, let component = self.component, let environment = self.environment else {
let controller = ChannelMembersSearchController(context: groupCall.accountContext, peerId: groupPeer.id, forceTheme: environment.theme, mode: .inviteToCall, filters: filters, openPeer: { [weak self] peer, participant in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
dismissController?()
return
}
@ -90,51 +90,51 @@ extension VideoChatScreenComponent.View {
return
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
if peer.id == callState.myPeerId {
return
}
if let participant {
dismissController?()
if component.call.invitePeer(participant.peer.id) {
if groupCall.invitePeer(participant.peer.id) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: EnginePeer(participant.peer), title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
} else {
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
let text = environment.strings.VoiceChat_SendPublicLinkText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_SendPublicLinkSend, action: { [weak self] in
dismissController?()
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
let _ = (enqueueMessages(account: component.call.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self, let environment = self.environment else {
let _ = (enqueueMessages(account: groupCall.accountContext.account, peerId: peer.id, messages: [.message(text: listenerLink, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
self.presentUndoOverlay(content: .forward(savedMessages: false, text: environment.strings.UserInfo_LinkForwardTooltip_Chat_One(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
})
})]), in: .window(.root))
} else {
let text: String
if case let .channel(groupPeer) = groupPeer, case .broadcast = groupPeer.info {
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
text = environment.strings.VoiceChat_InviteMemberToChannelFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), EnginePeer(groupPeer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
} else {
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
text = environment.strings.VoiceChat_InviteMemberToGroupFirstText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), groupPeer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_InviteMemberToGroupFirstAdd, action: { [weak self] in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
@ -143,7 +143,7 @@ extension VideoChatScreenComponent.View {
return
}
let inviteDisposable = self.inviteDisposable
var inviteSignal = component.call.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: component.call.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
var inviteSignal = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.addMembers(engine: groupCall.accountContext.engine, peerId: groupPeer.id, memberIds: [peer.id])
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
@ -172,7 +172,7 @@ extension VideoChatScreenComponent.View {
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
dismissController?()
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
@ -201,22 +201,22 @@ extension VideoChatScreenComponent.View {
case .kicked:
text = environment.strings.Channel_AddUserKickedError
}
environment.controller()?.present(textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
environment.controller()?.present(textAlertController(context: groupCall.accountContext, forceTheme: environment.theme, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
}, completed: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
dismissController?()
return
}
dismissController?()
if component.call.invitePeer(peer.id) {
if groupCall.invitePeer(peer.id) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}))
} else if case let .legacyGroup(groupPeer) = groupPeer {
@ -224,7 +224,7 @@ extension VideoChatScreenComponent.View {
return
}
let inviteDisposable = self.inviteDisposable
var inviteSignal = component.call.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
var inviteSignal = groupCall.accountContext.engine.peers.addGroupMember(peerId: groupPeer.id, memberId: peer.id)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { [weak selfController] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
@ -253,19 +253,19 @@ extension VideoChatScreenComponent.View {
inviteDisposable.set((inviteSignal |> deliverOnMainQueue).start(error: { [weak self] error in
dismissController?()
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
let context = component.call.accountContext
let context = groupCall.accountContext
switch error {
case .privacy:
let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peer.id)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let component = self.component, let environment = self.environment else {
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peer.id)
|> deliverOnMainQueue).start(next: { [weak self] peer in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
environment.controller()?.present(textAlertController(context: component.call.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
environment.controller()?.present(textAlertController(context: groupCall.accountContext, title: nil, text: environment.strings.Privacy_GroupsAndChannels_InviteToGroupError(EnginePeer(peer).compactDisplayTitle, EnginePeer(peer).compactDisplayTitle).string, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
})
case .notMutualContact:
environment.controller()?.present(textAlertController(context: context, title: nil, text: environment.strings.GroupInfo_AddUserLeftError, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
@ -275,20 +275,20 @@ extension VideoChatScreenComponent.View {
environment.controller()?.present(textAlertController(context: context, forceTheme: environment.theme, title: nil, text: environment.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {})]), in: .window(.root))
}
}, completed: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
dismissController?()
return
}
dismissController?()
if component.call.invitePeer(peer.id) {
if groupCall.invitePeer(peer.id) {
let text: String
if case let .channel(channel) = self.peer, case .broadcast = channel.info {
text = environment.strings.LiveStream_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder)).string
}
self.presentUndoOverlay(content: .invitedToVoiceChat(context: component.call.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
self.presentUndoOverlay(content: .invitedToVoiceChat(context: groupCall.accountContext, peer: peer, title: nil, text: text, action: nil, duration: 3), action: { _ in return false })
}
}))
}
@ -299,14 +299,14 @@ extension VideoChatScreenComponent.View {
controller.copyInviteLink = { [weak self] in
dismissController?()
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
guard let callPeerId = component.call.peerId else {
guard let callPeerId = groupCall.peerId else {
return
}
let _ = (component.call.accountContext.engine.data.get(
let _ = (groupCall.accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: callPeerId),
TelegramEngine.EngineData.Item.Peer.ExportedInvitation(id: callPeerId)
)
@ -321,7 +321,7 @@ extension VideoChatScreenComponent.View {
return nil
}
}
|> deliverOnMainQueue).start(next: { [weak self] link in
|> deliverOnMainQueue).start(next: { [weak self] link in
guard let self, let environment = self.environment else {
return
}

View File

@ -20,7 +20,10 @@ extension VideoChatScreenComponent.View {
guard let sourceView = self.navigationLeftButton.view else {
return
}
guard let component = self.component, let environment = self.environment, let controller = environment.controller() else {
guard let environment = self.environment, let controller = environment.controller() else {
return
}
guard let currentCall = self.currentCall else {
return
}
guard let callState = self.callState else {
@ -35,7 +38,7 @@ extension VideoChatScreenComponent.View {
for peer in displayAsPeers {
if peer.peer.id == callState.myPeerId {
let avatarSize = CGSize(width: 28.0, height: 28.0)
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_DisplayAs, textLayout: .secondLineWithValue(EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)), icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: avatarSize, signal: peerAvatarCompleteImage(account: currentCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)), action: { [weak self] c, _ in
guard let self else {
return
}
@ -201,19 +204,19 @@ extension VideoChatScreenComponent.View {
}
if callState.isVideoEnabled && (callState.muteState?.canUnmute ?? true) {
if component.call.hasScreencast {
if currentCall.hasScreencast {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_StopScreenSharing, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.default)
guard let self, let component = self.component else {
guard let self, let currentCall = self.currentCall else {
return
}
component.call.disableScreencast()
currentCall.disableScreencast()
})))
} else {
items.append(.custom(VoiceChatShareScreenContextItem(context: component.call.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in
items.append(.custom(VoiceChatShareScreenContextItem(context: currentCall.accountContext, text: environment.strings.VoiceChat_ShareScreen, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/ShareScreen"), color: theme.actionSheet.primaryTextColor)
}, action: { _, _ in }), false))
}
@ -224,15 +227,15 @@ extension VideoChatScreenComponent.View {
items.append(.custom(VoiceChatRecordingContextItem(timestamp: recordingStartTimestamp, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return
}
let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in
guard let self, let component = self.component, let environment = self.environment else {
let alertController = textAlertController(context: currentCall.accountContext, forceTheme: environment.theme, title: nil, text: environment.strings.VoiceChat_StopRecordingTitle, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: environment.strings.VoiceChat_StopRecordingStop, action: { [weak self] in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
component.call.setShouldBeRecording(false, title: nil, videoOrientation: nil)
groupCall.setShouldBeRecording(false, title: nil, videoOrientation: nil)
Queue.mainQueue().after(0.88) {
HapticFeedback().success()
@ -245,8 +248,8 @@ extension VideoChatScreenComponent.View {
text = environment.strings.VideoChat_RecordingSaved
}
self.presentUndoOverlay(content: .forward(savedMessages: true, text: text), action: { [weak self] value in
if case .info = value, let self, let component = self.component, let environment = self.environment, let navigationController = environment.controller()?.navigationController as? NavigationController {
let context = component.call.accountContext
if case .info = value, let self, let environment = self.environment, let currentCall = self.currentCall, let navigationController = environment.controller()?.navigationController as? NavigationController {
let context = currentCall.accountContext
environment.controller()?.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().justDispatch {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
@ -279,12 +282,12 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else {
guard let self, let environment = self.environment, let currentCall = self.currentCall, let peer = self.peer else {
return
}
let controller = VoiceChatRecordingSetupController(context: component.call.accountContext, peer: peer, completion: { [weak self] videoOrientation in
guard let self, let component = self.component, let environment = self.environment, let peer = self.peer else {
let controller = VoiceChatRecordingSetupController(context: currentCall.accountContext, peer: peer, completion: { [weak self] videoOrientation in
guard let self, let environment = self.environment, let currentCall = self.currentCall, let peer = self.peer else {
return
}
let title: String
@ -311,12 +314,12 @@ extension VideoChatScreenComponent.View {
}
}
let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in
guard let self, let component = self.component, let environment = self.environment, let peer = self.peer, let title else {
let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: title, text: text, placeholder: placeholder, value: nil, maxLength: 40, apply: { [weak self] title in
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall, let peer = self.peer, let title else {
return
}
component.call.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation)
groupCall.setShouldBeRecording(true, title: title, videoOrientation: videoOrientation)
let text: String
if case let .channel(channel) = peer, case .broadcast = channel.info {
@ -326,7 +329,7 @@ extension VideoChatScreenComponent.View {
}
self.presentUndoOverlay(content: .voiceChatRecording(text: text), action: { _ in return false })
component.call.playTone(.recordingStarted)
groupCall.playTone(.recordingStarted)
})
environment.controller()?.present(controller, in: .window(.root))
})
@ -348,24 +351,37 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return
}
let action: () -> Void = { [weak self] in
guard let self, let component = self.component else {
guard let self, let currentCall = self.currentCall else {
return
}
let _ = (component.call.leave(terminateIfPossible: true)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
switch currentCall {
case let .group(groupCall):
let _ = (groupCall.leave(terminateIfPossible: true)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
case let .conferenceSource(conferenceSource):
let _ = (conferenceSource.hangUp()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
}
}
let title: String
@ -378,7 +394,7 @@ extension VideoChatScreenComponent.View {
text = isScheduled ? environment.strings.VoiceChat_CancelConfirmationText : environment.strings.VoiceChat_EndConfirmationText
}
let alertController = textAlertController(context: component.call.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: {
let alertController = textAlertController(context: currentCall.accountContext, forceTheme: environment.theme, title: title, text: text, actions: [TextAlertAction(type: .defaultAction, title: environment.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: isScheduled ? environment.strings.VoiceChat_CancelConfirmationEnd : environment.strings.VoiceChat_EndConfirmationEnd, action: {
action()
})])
environment.controller()?.present(alertController, in: .window(.root))
@ -395,29 +411,45 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component else {
guard let self, let currentCall = self.currentCall else {
return
}
let _ = (component.call.leave(terminateIfPossible: false)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
switch currentCall {
case let .group(groupCall):
let _ = (groupCall.leave(terminateIfPossible: false)
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
case let .conferenceSource(conferenceSource):
let _ = (conferenceSource.hangUp()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let environment = self.environment else {
return
}
environment.controller()?.dismiss()
})
}
})))
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let contextController = ContextController(presentationData: presentationData, source: .reference(VoiceChatContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
controller.presentInGlobalOverlay(contextController)
}
private func contextMenuDisplayAsItems() -> [ContextMenuItem] {
guard let component = self.component, let environment = self.environment else {
guard let environment = self.environment else {
return []
}
guard case let .group(groupCall) = self.currentCall else {
return []
}
guard let callState = self.callState else {
@ -469,7 +501,7 @@ extension VideoChatScreenComponent.View {
let isSelected = peer.peer.id == myPeerId
let extendedAvatarSize = CGSize(width: 35.0, height: 35.0)
let theme = environment.theme
let avatarSignal = peerAvatarCompleteImage(account: component.call.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)
let avatarSignal = peerAvatarCompleteImage(account: groupCall.accountContext.account, peer: EnginePeer(peer.peer), size: avatarSize)
|> map { image -> UIImage? in
if isSelected, let image = image {
return generateImage(extendedAvatarSize, rotatedContext: { size, context in
@ -490,15 +522,15 @@ extension VideoChatScreenComponent.View {
}
}
items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in
items.append(.action(ContextMenuActionItem(text: EnginePeer(peer.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder), textLayout: subtitle.flatMap { .secondLineWithValue($0) } ?? .singleLine, icon: { _ in nil }, iconSource: ContextMenuActionItemIconSource(size: isSelected ? extendedAvatarSize : avatarSize, signal: avatarSignal), action: { [weak self] _, f in
f(.default)
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if peer.peer.id != myPeerId {
component.call.reconnect(as: peer.peer.id)
groupCall.reconnect(as: peer.peer.id)
}
})))
@ -548,11 +580,11 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.default)
guard let self, let component = self.component else {
guard let self, let currentCall = self.currentCall else {
return
}
component.call.setCurrentAudioOutput(output)
currentCall.setCurrentAudioOutput(output)
})))
}
@ -583,10 +615,10 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
component.call.updateDefaultParticipantsAreMuted(isMuted: false)
groupCall.updateDefaultParticipantsAreMuted(isMuted: false)
})))
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_SpeakPermissionAdmin, icon: { theme in
if !isMuted {
@ -597,10 +629,10 @@ extension VideoChatScreenComponent.View {
}, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
component.call.updateDefaultParticipantsAreMuted(isMuted: true)
groupCall.updateDefaultParticipantsAreMuted(isMuted: true)
})))
}
return items

View File

@ -15,17 +15,20 @@ import LegacyMediaPickerUI
extension VideoChatScreenComponent.View {
func openParticipantContextMenu(id: EnginePeer.Id, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?) {
guard let component = self.component, let environment = self.environment else {
guard let environment = self.environment else {
return
}
guard let members = self.members, let participant = members.participants.first(where: { $0.peer.id == id }) else {
return
}
guard let currentCall = self.currentCall else {
return
}
let muteStatePromise = Promise<GroupCallParticipantsContext.Participant.MuteState?>(participant.muteState)
let itemsForEntry: (GroupCallParticipantsContext.Participant.MuteState?) -> [ContextMenuItem] = { [weak self] muteState in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return []
}
guard let callState = self.callState else {
@ -48,15 +51,15 @@ extension VideoChatScreenComponent.View {
minValue = 0.0
}
items.append(.custom(VoiceChatVolumeContextItem(minValue: minValue, value: participant.volume.flatMap { CGFloat($0) / 10000.0 } ?? 1.0, valueChanged: { [weak self] newValue, finished in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
if finished && newValue.isZero {
let updatedMuteState = component.call.updateMuteState(peerId: peer.id, isMuted: true)
let updatedMuteState = groupCall.updateMuteState(peerId: peer.id, isMuted: true)
muteStatePromise.set(.single(updatedMuteState))
} else {
component.call.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished)
groupCall.setVolume(peerId: peer.id, volume: Int32(newValue * 10000), sync: finished)
}
}), true))
}
@ -73,10 +76,10 @@ extension VideoChatScreenComponent.View {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_CancelSpeakRequest, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/RevokeSpeak"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
component.call.lowerHand()
groupCall.lowerHand()
f(.default)
})))
@ -100,7 +103,7 @@ extension VideoChatScreenComponent.View {
f(.default)
Queue.mainQueue().after(0.1) {
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return
}
let maxBioLength: Int
@ -109,17 +112,17 @@ extension VideoChatScreenComponent.View {
} else {
maxBioLength = 100
}
let controller = voiceChatTitleEditController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in
guard let self, let component = self.component, let environment = self.environment, let bio else {
let controller = voiceChatTitleEditController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_EditBioTitle, text: environment.strings.VoiceChat_EditBioText, placeholder: environment.strings.VoiceChat_EditBioPlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, value: participant.about, maxLength: maxBioLength, apply: { [weak self] bio in
guard let self, let environment = self.environment, let currentCall = self.currentCall, let bio else {
return
}
if peer.id.namespace == Namespaces.Peer.CloudUser {
let _ = (component.call.accountContext.engine.accountData.updateAbout(about: bio)
let _ = (currentCall.accountContext.engine.accountData.updateAbout(about: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
} else {
let _ = (component.call.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio)
let _ = (currentCall.accountContext.engine.peers.updatePeerDescription(peerId: peer.id, description: bio)
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}).start()
@ -138,14 +141,14 @@ extension VideoChatScreenComponent.View {
f(.default)
Queue.mainQueue().after(0.1) {
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return
}
let controller = voiceChatUserNameController(sharedContext: component.call.accountContext.sharedContext, account: component.call.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in
guard let self, let component = self.component, let environment = self.environment, let (firstName, lastName) = firstAndLastName else {
let controller = voiceChatUserNameController(sharedContext: currentCall.accountContext.sharedContext, account: currentCall.accountContext.account, forceTheme: environment.theme, title: environment.strings.VoiceChat_ChangeNameTitle, firstNamePlaceholder: environment.strings.UserInfo_FirstNamePlaceholder, lastNamePlaceholder: environment.strings.UserInfo_LastNamePlaceholder, doneButtonTitle: environment.strings.VoiceChat_EditBioSave, firstName: peer.firstName, lastName: peer.lastName, maxLength: 128, apply: { [weak self] firstAndLastName in
guard let self, let environment = self.environment, let currentCall = self.currentCall, let (firstName, lastName) = firstAndLastName else {
return
}
let _ = component.call.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone()
let _ = currentCall.accountContext.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).startStandalone()
self.presentUndoOverlay(content: .info(title: nil, text: environment.strings.VoiceChat_EditNameSuccess, timeout: nil, customUndoText: nil), action: { _ in return false })
})
@ -154,18 +157,18 @@ extension VideoChatScreenComponent.View {
})))
}
} else {
if (callState.canManageCall || callState.adminIds.contains(component.call.accountContext.account.peerId)) {
if (callState.canManageCall || callState.adminIds.contains(currentCall.accountContext.account.peerId)) {
if callState.adminIds.contains(peer.id) {
if let _ = muteState {
} else {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
@ -174,24 +177,24 @@ extension VideoChatScreenComponent.View {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: participant.hasRaiseHand ? "Call/Context Menu/AllowToSpeak" : "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false)
let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false)
f(.default)
self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
self.presentUndoOverlay(content: .voiceChatCanSpeak(text: environment.strings.VoiceChat_UserCanNowSpeak(EnginePeer(participant.peer).displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string), action: { _ in return true })
})))
} else {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MutePeer, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
@ -201,22 +204,22 @@ extension VideoChatScreenComponent.View {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_UnmuteForMe, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Unmute"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: false)
let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: false)
f(.default)
})))
} else {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_MuteForMe, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Call/Context Menu/Mute"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
let _ = component.call.updateMuteState(peerId: peer.id, isMuted: true)
let _ = groupCall.updateMuteState(peerId: peer.id, isMuted: true)
f(.default)
})))
}
@ -239,7 +242,7 @@ extension VideoChatScreenComponent.View {
items.append(.action(ContextMenuActionItem(text: openTitle, icon: { theme in
return generateTintedImage(image: openIcon, color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, let currentCall = self.currentCall else {
return
}
@ -247,7 +250,7 @@ extension VideoChatScreenComponent.View {
return
}
let context = component.call.accountContext
let context = currentCall.accountContext
controller.dismiss(completion: { [weak navigationController] in
Queue.mainQueue().after(0.1) {
guard let navigationController else {
@ -260,43 +263,43 @@ extension VideoChatScreenComponent.View {
f(.dismissWithoutContent)
})))
if (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != component.call.peerId {
if case let .group(groupCall) = self.currentCall, (callState.canManageCall && !callState.adminIds.contains(peer.id)), peer.id != groupCall.peerId {
items.append(.action(ContextMenuActionItem(text: environment.strings.VoiceChat_RemovePeer, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.actionSheet.destructiveActionTextColor)
}, action: { [weak self] c, _ in
c?.dismiss(completion: {
guard let self, let component = self.component else {
guard let self, case let .group(groupCall) = self.currentCall else {
return
}
guard let peerId = component.call.peerId else {
guard let peerId = groupCall.peerId else {
return
}
let _ = (component.call.accountContext.account.postbox.loadedPeerWithId(peerId)
let _ = (groupCall.accountContext.account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] chatPeer in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let presentationData = groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
let nameDisplayOrder = presentationData.nameDisplayOrder
items.append(DeleteChatPeerActionSheetItem(context: component.call.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder))
items.append(DeleteChatPeerActionSheetItem(context: groupCall.accountContext, peer: EnginePeer(peer), chatPeer: EnginePeer(chatPeer), action: .removeFromGroup, strings: environment.strings, nameDisplayOrder: nameDisplayOrder))
items.append(ActionSheetButtonItem(title: environment.strings.VoiceChat_RemovePeerRemove, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let environment = self.environment, case let .group(groupCall) = self.currentCall else {
return
}
guard let callPeerId = component.call.peerId else {
guard let callPeerId = groupCall.peerId else {
return
}
let _ = component.call.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: component.call.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
component.call.removedPeer(peer.id)
let _ = groupCall.accountContext.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: groupCall.accountContext.engine, peerId: callPeerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)).start()
groupCall.removedPeer(peer.id)
self.presentUndoOverlay(content: .banned(text: environment.strings.VoiceChat_RemovedPeerText(EnginePeer(peer).displayTitle(strings: environment.strings, displayOrder: nameDisplayOrder)).string), action: { _ in return false })
}))
@ -323,7 +326,7 @@ extension VideoChatScreenComponent.View {
return itemsForEntry(muteState)
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let contextController = ContextController(
presentationData: presentationData,
source: .extracted(ParticipantExtractedContentSource(contentView: sourceView)),
@ -345,7 +348,7 @@ extension VideoChatScreenComponent.View {
}
private func openAvatarForEditing(fromGallery: Bool = false, completion: @escaping () -> Void = {}) {
guard let component = self.component else {
guard let currentCall = self.currentCall else {
return
}
guard let callState = self.callState else {
@ -353,19 +356,19 @@ extension VideoChatScreenComponent.View {
}
let peerId = callState.myPeerId
let _ = (component.call.accountContext.engine.data.get(
let _ = (currentCall.accountContext.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
TelegramEngine.EngineData.Item.Configuration.SearchBots()
)
|> deliverOnMainQueue).start(next: { [weak self] peer, searchBotsConfiguration in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let currentCall = self.currentCall, let environment = self.environment else {
return
}
guard let peer else {
return
}
let presentationData = component.call.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let presentationData = currentCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme)
let legacyController = LegacyController(presentation: .custom, theme: environment.theme)
legacyController.statusBar.statusBarStyle = .Ignore
@ -387,13 +390,13 @@ extension VideoChatScreenComponent.View {
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos && !fromGallery, hasViewButton: false, personalPhoto: peerId.namespace == Namespaces.Peer.CloudUser, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)!
mixin.forceDark = true
mixin.stickersContext = LegacyPaintStickersContext(context: component.call.accountContext)
mixin.stickersContext = LegacyPaintStickersContext(context: currentCall.accountContext)
let _ = self.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let currentCall = self.currentCall, let environment = self.environment else {
return
}
let controller = WebSearchController(context: component.call.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
let controller = WebSearchController(context: currentCall.accountContext, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer.id.namespace == Namespaces.Peer.CloudUser ? nil : peer.displayTitle(strings: environment.strings, displayOrder: presentationData.nameDisplayOrder), completion: { [weak self] result in
assetsController?.dismiss()
guard let self else {
@ -426,13 +429,13 @@ extension VideoChatScreenComponent.View {
}
let proceed = { [weak self] in
guard let self, let component = self.component else {
guard let self, let currentCall = self.currentCall else {
return
}
let _ = self.currentAvatarMixin.swap(nil)
let postbox = component.call.accountContext.account.postbox
self.updateAvatarDisposable.set((component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
let postbox = currentCall.accountContext.account.postbox
self.updateAvatarDisposable.set((currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
|> deliverOnMainQueue).start())
@ -473,7 +476,7 @@ extension VideoChatScreenComponent.View {
}
private func updateProfilePhoto(_ image: UIImage) {
guard let component = self.component else {
guard let currentCall = self.currentCall else {
return
}
guard let callState = self.callState else {
@ -486,15 +489,15 @@ extension VideoChatScreenComponent.View {
let peerId = callState.myPeerId
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
component.call.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
currentCall.accountContext.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
self.currentUpdatingAvatar = (representation, 0.0)
let postbox = component.call.account.postbox
let signal = peerId.namespace == Namespaces.Peer.CloudUser ? component.call.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in
let postbox = currentCall.accountContext.account.postbox
let signal = peerId.namespace == Namespaces.Peer.CloudUser ? currentCall.accountContext.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, markup: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
}) : component.call.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: component.call.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in
}) : currentCall.accountContext.engine.peers.updatePeerPhoto(peerId: peerId, photo: currentCall.accountContext.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: postbox, resource: resource, representations: representations)
})
@ -516,7 +519,7 @@ extension VideoChatScreenComponent.View {
}
private func updateProfileVideo(_ image: UIImage, asset: Any?, adjustments: TGVideoEditAdjustments?) {
guard let component = self.component else {
guard let currentCall = self.currentCall else {
return
}
guard let callState = self.callState else {
@ -528,7 +531,7 @@ extension VideoChatScreenComponent.View {
let peerId = callState.myPeerId
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
component.call.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
currentCall.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
self.currentUpdatingAvatar = (representation, 0.0)
@ -538,7 +541,7 @@ extension VideoChatScreenComponent.View {
videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue
}
let context = component.call.accountContext
let context = currentCall.accountContext
let account = context.account
let signal = Signal<TelegramMediaResource, UploadPeerPhotoError> { [weak self] subscriber in
let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in

View File

@ -242,7 +242,7 @@ struct VoiceChatPeerEntry: Identifiable {
}
public protocol VoiceChatController: ViewController {
var call: PresentationGroupCall { get }
var call: VideoChatCall { get }
var currentOverlayController: VoiceChatOverlayController? { get }
var parentNavigationController: NavigationController? { get set }
var onViewDidAppear: (() -> Void)? { get set }
@ -6859,7 +6859,10 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
}
private let sharedContext: SharedAccountContext
public let call: PresentationGroupCall
public let callImpl: PresentationGroupCall
public var call: VideoChatCall {
return .group(self.callImpl)
}
private let presentationData: PresentationData
public var parentNavigationController: NavigationController?
@ -6891,7 +6894,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
public init(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) {
self.sharedContext = sharedContext
self.call = call
self.callImpl = call
self.presentationData = sharedContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: nil)
@ -6936,7 +6939,7 @@ final class VoiceChatControllerImpl: ViewController, VoiceChatController {
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self, sharedContext: self.sharedContext, call: self.call)
self.displayNode = Node(controller: self, sharedContext: self.sharedContext, call: self.callImpl)
self.displayNodeDidLoad()
}
@ -7138,7 +7141,7 @@ public func shouldUseV2VideoChatImpl(context: AccountContext) -> Bool {
return useV2
}
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) -> Signal<Any, NoError> {
public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountContext, accountContext: AccountContext, call: VideoChatCall) -> Signal<Any, NoError> {
let useV2 = shouldUseV2VideoChatImpl(context: accountContext)
if useV2 {
@ -7148,6 +7151,6 @@ public func makeVoiceChatControllerInitialData(sharedContext: SharedAccountConte
}
}
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall, initialData: Any, sourceCallController: CallController?) -> VoiceChatController {
public func makeVoiceChatController(sharedContext: SharedAccountContext, accountContext: AccountContext, call: VideoChatCall, initialData: Any, sourceCallController: CallController?) -> VoiceChatController {
return VideoChatScreenV2Impl(initialData: initialData as! VideoChatScreenV2Impl.InitialData, call: call, sourceCallController: sourceCallController)
}

View File

@ -26,17 +26,20 @@ public final class AnimatedTextComponent: Component {
public let color: UIColor
public let items: [Item]
public let noDelay: Bool
public let animateScale: Bool
public init(
font: UIFont,
color: UIColor,
items: [Item],
noDelay: Bool = false
noDelay: Bool = false,
animateScale: Bool = true
) {
self.font = font
self.color = color
self.items = items
self.noDelay = noDelay
self.animateScale = animateScale
}
public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool {
@ -52,6 +55,9 @@ public final class AnimatedTextComponent: Component {
if lhs.noDelay != rhs.noDelay {
return false
}
if lhs.animateScale != rhs.animateScale {
return false
}
return true
}
@ -172,7 +178,9 @@ public final class AnimatedTextComponent: Component {
}
}
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
if component.animateScale {
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
}
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
}
@ -202,7 +210,9 @@ public final class AnimatedTextComponent: Component {
outFirstDelayWidth = characterComponentView.frame.minX
}
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
if component.animateScale {
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
}
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth)
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
characterComponentView?.removeFromSuperview()

View File

@ -626,13 +626,6 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
transition.updateAlpha(node: statusNode, alpha: 1.0 - factor)
}
}
self.imageNode.imageUpdated = { [weak self] image in
guard let self else {
return
}
self.timestampMaskView?.image = image
}
}
deinit {
@ -794,6 +787,66 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
private struct MaskImageCornerKey: Hashable {
var bottomLeft: CGFloat
var bottomRight: CGFloat
var leftInset: CGFloat
var rightInset: CGFloat
init(bottomLeft: CGFloat, bottomRight: CGFloat, leftInset: CGFloat, rightInset: CGFloat) {
self.bottomLeft = bottomLeft
self.bottomRight = bottomRight
self.leftInset = leftInset
self.rightInset = rightInset
}
}
private static var timestampMaskImageCache: [MaskImageCornerKey: UIImage] = [:]
private func generateTimestampMaskImage(corners: ImageCorners) -> UIImage? {
var insets = corners.extendedEdges
insets.top = 0.0
insets.bottom = 0.0
let cacheKey = MaskImageCornerKey(bottomLeft: corners.bottomLeft.radius, bottomRight: corners.bottomRight.radius, leftInset: insets.left, rightInset: insets.right)
if let image = ChatMessageInteractiveMediaNode.timestampMaskImageCache[cacheKey] {
return image
}
let imageSize = CGSize(width: corners.bottomLeft.radius + corners.bottomRight.radius + insets.left + insets.right + 1.0, height: 1.0 + max(corners.bottomLeft.radius, corners.bottomRight.radius))
guard let context = DrawingContext(size: imageSize, clear: true) else {
return nil
}
context.withContext { c in
c.setFillColor(UIColor.white.cgColor)
c.move(to: CGPoint(x: insets.left, y: insets.top))
c.addLine(to: CGPoint(x: insets.left, y: imageSize.height - insets.bottom - corners.bottomLeft.radius))
c.addArc(tangent1End: CGPoint(x: insets.left, y: imageSize.height - insets.bottom), tangent2End: CGPoint(x: insets.left + corners.bottomLeft.radius, y: imageSize.height - insets.bottom), radius: corners.bottomLeft.radius)
c.addLine(to: CGPoint(x: imageSize.width - insets.right - corners.bottomRight.radius, y: imageSize.height - insets.bottom))
c.addArc(tangent1End: CGPoint(x: imageSize.width - insets.right, y: imageSize.height - insets.bottom), tangent2End: CGPoint(x: imageSize.width - insets.right, y: imageSize.height - insets.bottom - corners.bottomRight.radius), radius: corners.bottomRight.radius)
c.addLine(to: CGPoint(x: imageSize.width - insets.right, y: insets.top))
c.closePath()
c.fillPath()
}
let image = context.generateImage()?.resizableImage(
withCapInsets: UIEdgeInsets(
top: 0,
left: corners.bottomLeft.radius + insets.left,
bottom: imageSize.height - 1.0,
right: corners.bottomRight.radius + insets.right
),
resizingMode: .stretch
)
if let image {
ChatMessageInteractiveMediaNode.timestampMaskImageCache[cacheKey] = image
}
return image
}
public func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ mediaIndex: Int?, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let currentMessage = self.message
let currentMedia = self.media
@ -1771,6 +1824,9 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.pinchContainerNode.update(size: imageFrame.size, transition: .immediate)
strongSelf.imageNode.frame = CGRect(origin: CGPoint(), size: imageFrame.size)
}
if strongSelf.currentImageArguments?.corners != arguments.corners, let timestampMaskView = strongSelf.timestampMaskView {
timestampMaskView.image = strongSelf.generateTimestampMaskImage(corners: arguments.corners)
}
strongSelf.currentImageArguments = arguments
imageApply()
@ -1950,8 +2006,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}
//TODO:wip-release
/*var videoTimestamp: Int32?
var videoTimestamp: Int32?
var storedVideoTimestamp: Int32?
for attribute in message.attributes {
if let attribute = attribute as? ForwardVideoTimestampAttribute {
@ -1985,7 +2040,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.timestampMaskView = timestampMaskView
timestampContainerView.mask = timestampMaskView
timestampMaskView.image = strongSelf.imageNode.image
timestampMaskView.image = strongSelf.generateTimestampMaskImage(corners: arguments.corners)
}
let videoTimestampBackgroundLayer: SimpleLayer
@ -2038,7 +2093,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
strongSelf.videoTimestampForegroundLayer = nil
videoTimestampForegroundLayer.removeFromSuperlayer()
}
}*/
}
if let animatedStickerNode = strongSelf.animatedStickerNode {
animatedStickerNode.frame = imageFrame
@ -3037,6 +3092,72 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
})
}
public func scrubberTransition() -> GalleryItemScrubberTransition? {
if let timestampContainerView = self.timestampContainerView, let timestampMaskView = self.timestampMaskView, let videoTimestampBackgroundLayer = self.videoTimestampBackgroundLayer, let videoTimestampForegroundLayer = self.videoTimestampForegroundLayer {
final class TimestampContainerTransitionView: UIView {
let containerView: UIView
let containerMaskView: UIImageView
let backgroundLayer: SimpleLayer
let foregroundLayer: SimpleLayer
let fraction: CGFloat
init(timestampContainerView: UIView?, timestampMaskView: UIImageView?, videoTimestampBackgroundLayer: SimpleLayer?, videoTimestampForegroundLayer: SimpleLayer?) {
self.containerView = UIView()
self.containerMaskView = UIImageView()
self.backgroundLayer = SimpleLayer()
self.foregroundLayer = SimpleLayer()
if let videoTimestampBackgroundLayer, let videoTimestampForegroundLayer {
self.fraction = videoTimestampForegroundLayer.bounds.width / videoTimestampBackgroundLayer.bounds.width
} else {
self.fraction = 0.0
}
super.init(frame: CGRect())
self.addSubview(self.containerView)
self.containerView.mask = self.containerMaskView
self.containerMaskView.image = timestampMaskView?.image
self.containerView.layer.addSublayer(self.backgroundLayer)
self.containerView.layer.addSublayer(self.foregroundLayer)
self.backgroundLayer.backgroundColor = videoTimestampBackgroundLayer?.backgroundColor
self.foregroundLayer.backgroundColor = videoTimestampForegroundLayer?.backgroundColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(state: GalleryItemScrubberTransition.TransitionState, transition: ContainedViewLayoutTransition) {
let containerFrame = CGRect(origin: CGPoint(), size: state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress))
transition.updateFrame(view: self.containerView, frame: containerFrame)
transition.updateFrame(view: self.containerMaskView, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
transition.updateFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.height - 3.0), size: CGSize(width: containerFrame.width, height: 3.0)))
transition.updateFrame(layer: self.foregroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.height - 3.0), size: CGSize(width: containerFrame.width * self.fraction, height: 3.0)))
}
}
return GalleryItemScrubberTransition(
view: timestampContainerView,
makeView: { [weak timestampContainerView, weak timestampMaskView, weak videoTimestampBackgroundLayer, weak videoTimestampForegroundLayer] in
return TimestampContainerTransitionView(timestampContainerView: timestampContainerView, timestampMaskView: timestampMaskView, videoTimestampBackgroundLayer: videoTimestampBackgroundLayer, videoTimestampForegroundLayer: videoTimestampForegroundLayer)
},
updateView: { view, state, transition in
if let view = view as? TimestampContainerTransitionView {
view.update(state: state, transition: transition)
}
},
insertCloneTransitionView: nil
)
} else {
return nil
}
}
public func playMediaWithSound() -> (action: (Double?) -> Void, soundEnabled: Bool, isVideoMessage: Bool, isUnread: Bool, badgeNode: ASDisplayNode?)? {
var isAnimated = false
if let file = self.media as? TelegramMediaFile {

View File

@ -176,7 +176,7 @@ public func presentPeerReportOptions(
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
message = text
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: {
@ -309,7 +309,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe
var message = ""
var items: [ActionSheetItem] = []
items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText))
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, strings: presentationData.strings, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in
message = text
}))
items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: {

View File

@ -11,18 +11,20 @@ import AppBundle
public final class ReportPeerDetailsActionSheetItem: ActionSheetItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let placeholderText: String
let textUpdated: (String) -> Void
public init(context: AccountContext, theme: PresentationTheme, placeholderText: String, textUpdated: @escaping (String) -> Void) {
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
self.placeholderText = placeholderText
self.textUpdated = textUpdated
}
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated)
return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, strings: self.strings, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated)
}
public func updateNode(_ node: ActionSheetItemNode) {
@ -36,10 +38,10 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode {
private let accessibilityArea: AccessibilityAreaNode
init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, strings: PresentationStrings, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) {
self.theme = theme
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), placeholder: placeholderText)
self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), strings: strings, placeholder: placeholderText)
self.accessibilityArea = AccessibilityAreaNode()
@ -58,7 +60,7 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode {
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let inputHeight = self.inputFieldNode.updateLayout(width: constrainedSize.width, transition: .immediate)
let inputHeight = self.inputFieldNode.updateLayout(width: constrainedSize.width, inputCopyText: nil, transition: .immediate)
self.inputFieldNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: constrainedSize.width, height: inputHeight))
let size = CGSize(width: constrainedSize.width, height: inputHeight)

View File

@ -830,7 +830,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.hasOngoingCall.set(true)
setNotificationCall(call)
self.callIsConferenceDisposable = (call.hasConference
self.callIsConferenceDisposable = (call.conferenceState
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).startStrict(next: { [weak self] _ in
guard let self else {
return
@ -911,7 +913,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.groupCallDisposable = (callManager.currentGroupCallSignal
|> deliverOnMainQueue).start(next: { [weak self] call in
if let strongSelf = self {
if call !== strongSelf.groupCallController?.call {
if call.flatMap(VideoChatCall.group) != strongSelf.groupCallController?.call {
strongSelf.groupCallController?.dismiss(closing: true, manual: false)
strongSelf.groupCallController = nil
strongSelf.hasOngoingCall.set(false)
@ -939,13 +941,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
} else {
strongSelf.hasGroupCallOnScreenPromise.set(true)
let _ = (makeVoiceChatControllerInitialData(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
let _ = (makeVoiceChatControllerInitialData(sharedContext: strongSelf, accountContext: call.accountContext, call: .group(call))
|> deliverOnMainQueue).start(next: { [weak strongSelf, weak navigationController] initialData in
guard let strongSelf, let navigationController else {
return
}
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call, initialData: initialData, sourceCallController: nil)
let groupCallController = makeVoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: .group(call), initialData: initialData, sourceCallController: nil)
groupCallController.onViewDidAppear = { [weak strongSelf] in
if let strongSelf {
strongSelf.hasGroupCallOnScreenPromise.set(true)
@ -979,14 +981,11 @@ public final class SharedAccountContextImpl: SharedAccountContext {
guard let call else {
return .single((nil, nil))
}
return combineLatest(call.state, call.hasConference)
|> map { [weak call] state, _ -> (PresentationCall?, PresentationGroupCall?) in
return call.state
|> map { [weak call] state -> (PresentationCall?, PresentationGroupCall?) in
guard let call else {
return (nil, nil)
}
if let conferenceCall = call.conferenceCall {
return (nil, conferenceCall)
}
switch state.state {
case .ringing:
return (nil, nil)
@ -1217,9 +1216,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return
}
if let conferenceCall = call.conferenceCall {
if call.conferenceStateValue != nil {
if let groupCallController = self.groupCallController {
if groupCallController.call === conferenceCall {
if groupCallController.call == call.conferenceCall.flatMap(VideoChatCall.group) || groupCallController.call == .conferenceSource(call) {
return
}
groupCallController.dismiss(closing: true, manual: false)
@ -1228,11 +1227,19 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var transitioniongCallController: CallController?
if let callController = self.callController {
transitioniongCallController = callController
callController.dismissWithoutAnimation()
if callController.navigationPresentation != .flatModal {
callController.dismissWithoutAnimation()
}
self.callController = nil
}
let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall)
let groupCall: VideoChatCall
if let conferenceCall = call.conferenceCall, case .ready = call.conferenceStateValue {
groupCall = .group(conferenceCall)
} else {
groupCall = .conferenceSource(call)
}
let _ = (makeVoiceChatControllerInitialData(sharedContext: self, accountContext: call.context, call: groupCall)
|> deliverOnMainQueue).start(next: { [weak self, weak transitioniongCallController] initialData in
guard let self else {
return
@ -1240,11 +1247,18 @@ public final class SharedAccountContextImpl: SharedAccountContext {
guard let navigationController = self.mainWindow?.viewController as? NavigationController else {
return
}
guard let call = self.call, let conferenceCall = call.conferenceCall else {
guard let call = self.call else {
return
}
let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: conferenceCall.accountContext, call: conferenceCall, initialData: initialData, sourceCallController: transitioniongCallController)
let groupCall: VideoChatCall
if let conferenceCall = call.conferenceCall, case .ready = call.conferenceStateValue {
groupCall = .group(conferenceCall)
} else {
groupCall = .conferenceSource(call)
}
let groupCallController = makeVoiceChatController(sharedContext: self, accountContext: call.context, call: groupCall, initialData: initialData, sourceCallController: transitioniongCallController)
groupCallController.onViewDidAppear = { [weak self] in
if let self {
self.hasGroupCallOnScreenPromise.set(true)
@ -1258,7 +1272,24 @@ public final class SharedAccountContextImpl: SharedAccountContext {
groupCallController.navigationPresentation = .flatModal
groupCallController.parentNavigationController = navigationController
self.groupCallController = groupCallController
navigationController.pushViewController(groupCallController)
transitioniongCallController?.onViewDidAppear = nil
transitioniongCallController?.onViewDidDisappear = nil
self.hasGroupCallOnScreenPromise.set(true)
if let transitioniongCallController, let navigationController = transitioniongCallController.navigationController as? NavigationController {
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 === transitioniongCallController }) {
viewControllers.insert(groupCallController, at: index)
navigationController.setViewControllers(viewControllers, animated: false)
viewControllers.remove(at: index + 1)
navigationController.setViewControllers(viewControllers, animated: false)
} else {
navigationController.pushViewController(groupCallController)
}
} else {
navigationController.pushViewController(groupCallController)
}
})
} else {
if let currentCallController = self.callController {

View File

@ -497,6 +497,7 @@ public final class OngoingGroupCallContext {
onMutedSpeechActivityDetected: @escaping (Bool) -> Void,
encryptionKey: Data?,
isConference: Bool,
isStream: Bool,
sharedAudioDevice: OngoingCallContext.AudioDevice?
) {
self.queue = queue
@ -506,12 +507,14 @@ public final class OngoingGroupCallContext {
self.tempStatsLogFile = EngineTempBox.shared.tempFile(fileName: "CallStats.json")
let tempStatsLogPath = self.tempStatsLogFile.path
#if os(iOS)
self.audioDevice = sharedAudioDevice
#if os(iOS)
if sharedAudioDevice == nil && !isStream {
self.audioDevice = OngoingCallContext.AudioDevice.create(enableSystemMute: false)
} else {
self.audioDevice = sharedAudioDevice
}
let audioDevice = self.audioDevice
#endif
#endif
var networkStateUpdatedImpl: ((GroupCallNetworkState) -> Void)?
var audioLevelsUpdatedImpl: (([NSNumber]) -> Void)?
var activityUpdatedImpl: (([UInt32]) -> Void)?
@ -1178,10 +1181,10 @@ public final class OngoingGroupCallContext {
}
}
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?, isConference: Bool, sharedAudioDevice: OngoingCallContext.AudioDevice?) {
public init(inputDeviceId: String = "", outputDeviceId: String = "", audioSessionActive: Signal<Bool, NoError>, video: OngoingCallVideoCapturer?, requestMediaChannelDescriptions: @escaping (Set<UInt32>, @escaping ([MediaChannelDescription]) -> Void) -> Disposable, rejoinNeeded: @escaping () -> Void, outgoingAudioBitrateKbit: Int32?, videoContentType: VideoContentType, enableNoiseSuppression: Bool, disableAudioInput: Bool, enableSystemMute: Bool, preferX264: Bool, logPath: String, onMutedSpeechActivityDetected: @escaping (Bool) -> Void, encryptionKey: Data?, isConference: Bool, isStream: Bool, sharedAudioDevice: OngoingCallContext.AudioDevice?) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey, isConference: isConference, sharedAudioDevice: sharedAudioDevice)
return Impl(queue: queue, inputDeviceId: inputDeviceId, outputDeviceId: outputDeviceId, audioSessionActive: audioSessionActive, video: video, requestMediaChannelDescriptions: requestMediaChannelDescriptions, rejoinNeeded: rejoinNeeded, outgoingAudioBitrateKbit: outgoingAudioBitrateKbit, videoContentType: videoContentType, enableNoiseSuppression: enableNoiseSuppression, disableAudioInput: disableAudioInput, enableSystemMute: enableSystemMute, preferX264: preferX264, logPath: logPath, onMutedSpeechActivityDetected: onMutedSpeechActivityDetected, encryptionKey: encryptionKey, isConference: isConference, isStream: isStream, sharedAudioDevice: sharedAudioDevice)
})
}