mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge commit '5eb61351f9'
This commit is contained in:
commit
9e99110a55
19 changed files with 2464 additions and 2251 deletions
|
|
@ -6475,3 +6475,5 @@ Sorry for the inconvenience.";
|
|||
"VoiceChat.ParticipantIsSpeaking" = "%1$@ is speaking";
|
||||
|
||||
"WallpaperPreview.WallpaperColors" = "Colors";
|
||||
|
||||
"VoiceChat.UnmuteSuggestion" = "You are on mute. Tap here to speak.";
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ public protocol PresentationGroupCall: class {
|
|||
|
||||
var audioOutputState: Signal<([AudioSessionOutput], AudioSessionOutput?), NoError> { get }
|
||||
|
||||
var isSpeaking: Signal<Bool, NoError> { get }
|
||||
var canBeRemoved: Signal<Bool, NoError> { get }
|
||||
var state: Signal<PresentationGroupCallState, NoError> { get }
|
||||
var stateVersion: Signal<Int, NoError> { get }
|
||||
|
|
|
|||
|
|
@ -185,11 +185,10 @@ final class BlobView: UIView {
|
|||
|
||||
var level: CGFloat = 0 {
|
||||
didSet {
|
||||
return;
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
let lv = minScale + (maxScale - minScale) * level
|
||||
shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
|
||||
let lv = self.minScale + (self.maxScale - self.minScale) * self.level
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
|
@ -257,7 +256,7 @@ final class BlobView: UIView {
|
|||
|
||||
layer.addSublayer(shapeLayer)
|
||||
|
||||
shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -265,75 +264,59 @@ final class BlobView: UIView {
|
|||
}
|
||||
|
||||
func setColor(_ color: UIColor, animated: Bool) {
|
||||
let previousColor = shapeLayer.fillColor
|
||||
shapeLayer.fillColor = color.cgColor
|
||||
let previousColor = self.shapeLayer.fillColor
|
||||
self.shapeLayer.fillColor = color.cgColor
|
||||
if animated, let previousColor = previousColor {
|
||||
shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||
self.shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||
speedLevel = max(speedLevel, newSpeedLevel)
|
||||
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
||||
|
||||
if abs(lastSpeedLevel - newSpeedLevel) > 0.5 {
|
||||
animateToNewShape()
|
||||
}
|
||||
// if abs(lastSpeedLevel - newSpeedLevel) > 0.5 {
|
||||
// animateToNewShape()
|
||||
// }
|
||||
}
|
||||
|
||||
func startAnimating() {
|
||||
animateToNewShape()
|
||||
self.animateToNewShape()
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
self.shapeLayer.removeAnimation(forKey: "path")
|
||||
}
|
||||
|
||||
private func animateToNewShape() {
|
||||
guard !isCircle else { return }
|
||||
|
||||
if pop_animation(forKey: "blob") != nil {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
if self.shapeLayer.path == nil {
|
||||
let points = generateNextBlob(for: self.bounds.size)
|
||||
self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath
|
||||
}
|
||||
|
||||
if fromPoints == nil {
|
||||
fromPoints = generateNextBlob(for: bounds.size)
|
||||
}
|
||||
if toPoints == nil {
|
||||
toPoints = generateNextBlob(for: bounds.size)
|
||||
}
|
||||
let nextPoints = generateNextBlob(for: self.bounds.size)
|
||||
let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath
|
||||
|
||||
let animation = POPBasicAnimation()
|
||||
animation.property = POPAnimatableProperty.property(withName: "blob.transition", initializer: { property in
|
||||
property?.readBlock = { blobView, values in
|
||||
guard let blobView = blobView as? BlobView, let values = values else { return }
|
||||
|
||||
values.pointee = blobView.transition
|
||||
}
|
||||
property?.writeBlock = { blobView, values in
|
||||
guard let blobView = blobView as? BlobView, let values = values else { return }
|
||||
|
||||
blobView.transition = values.pointee
|
||||
}
|
||||
}) as? POPAnimatableProperty
|
||||
animation.completionBlock = { [weak self] animation, finished in
|
||||
let animation = CABasicAnimation(keyPath: "path")
|
||||
let previousPath = self.shapeLayer.path
|
||||
self.shapeLayer.path = nextPath
|
||||
animation.duration = CFTimeInterval(1 / (self.minSpeed + (self.maxSpeed - self.minSpeed) * self.speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = previousPath
|
||||
animation.toValue = nextPath
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.fillMode = .forwards
|
||||
animation.completion = { [weak self] finished in
|
||||
if finished {
|
||||
self?.fromPoints = self?.currentPoints
|
||||
self?.toPoints = nil
|
||||
self?.animateToNewShape()
|
||||
}
|
||||
}
|
||||
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 1
|
||||
//pop_add(animation, forKey: "blob")
|
||||
|
||||
lastSpeedLevel = speedLevel
|
||||
speedLevel = 0
|
||||
|
||||
self.shapeLayer.add(animation, forKey: "path")
|
||||
|
||||
self.lastSpeedLevel = self.speedLevel
|
||||
self.speedLevel = 0
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
|
|
|||
|
|
@ -653,7 +653,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||
}
|
||||
case let .extracted(extracted, keepInPlace):
|
||||
let springDuration: Double = 0.42 * animationDurationFactor
|
||||
let springDamping: CGFloat = 104.0
|
||||
var springDamping: CGFloat = 104.0
|
||||
if case let .extracted(source) = self.source, source.centerVertically {
|
||||
springDamping = 124.0
|
||||
}
|
||||
|
||||
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor)
|
||||
self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping)
|
||||
|
|
@ -662,13 +665,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||
let contentParentNode = extracted
|
||||
let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view)
|
||||
|
||||
var actionsSpringDamping = springDamping
|
||||
var actionsDuration = springDuration
|
||||
var actionsOffset: CGFloat = 0.0
|
||||
var contentDuration = springDuration
|
||||
if case let .extracted(source) = self.source, source.centerVertically {
|
||||
actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57
|
||||
actionsSpringDamping *= 1.2
|
||||
actionsDuration *= 1.0
|
||||
contentDuration *= 0.9
|
||||
}
|
||||
|
|
@ -684,7 +685,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||
reactionContextNode.animateIn(from: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size))
|
||||
}
|
||||
|
||||
self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: actionsSpringDamping, additive: true)
|
||||
self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY)
|
||||
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||
contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping)
|
||||
|
|
|
|||
|
|
@ -425,14 +425,32 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
|
|||
|
||||
additionalDisposable.set((combineLatest(queue: .mainQueue(),
|
||||
membersState.get(),
|
||||
context.account.postbox.peerView(id: peerId),
|
||||
context.account.postbox.contactPeersView(accountPeerId: context.account.peerId, includePresences: true)
|
||||
).start(next: { [weak self] state, contactsView in
|
||||
).start(next: { [weak self] state, peerView, contactsView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var entries: [ChannelMembersSearchEntry] = []
|
||||
|
||||
if case .inviteToCall = mode, !filters.contains(where: { filter in
|
||||
var canInviteByLink = false
|
||||
if let peer = peerViewMainPeer(peerView) {
|
||||
if !(peer.addressName?.isEmpty ?? true) {
|
||||
canInviteByLink = true
|
||||
} else if let peer = peer as? TelegramChannel {
|
||||
if peer.flags.contains(.isCreator) || (peer.adminRights?.rights.contains(.canInviteUsers) == true) {
|
||||
canInviteByLink = true
|
||||
}
|
||||
} else if let peer = peer as? TelegramGroup {
|
||||
if case .creator = peer.role {
|
||||
canInviteByLink = true
|
||||
} else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canInviteUsers) {
|
||||
canInviteByLink = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .inviteToCall = mode, canInviteByLink, !filters.contains(where: { filter in
|
||||
if case .excludeNonMembers = filter {
|
||||
return true
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
import LegacyComponents
|
||||
import AnimatedCountLabelNode
|
||||
|
||||
private let blue = UIColor(rgb: 0x007fff)
|
||||
|
|
@ -680,8 +679,8 @@ final class CurveView: UIView {
|
|||
}
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
let lv = minOffset + (maxOffset - minOffset) * level
|
||||
shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0)
|
||||
let lv = self.minOffset + (self.maxOffset - self.minOffset) * self.level
|
||||
self.shapeLayer.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0)
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
|
@ -695,39 +694,16 @@ final class CurveView: UIView {
|
|||
return layer
|
||||
}()
|
||||
|
||||
private var transition: CGFloat = 0 {
|
||||
didSet {
|
||||
guard let currentPoints = currentPoints else { return }
|
||||
|
||||
shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness, curve: true).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
override var frame: CGRect {
|
||||
didSet {
|
||||
if self.frame.size != oldValue.size {
|
||||
self.fromPoints = nil
|
||||
self.toPoints = nil
|
||||
self.shapeLayer.path = nil
|
||||
self.animateToNewShape()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var fromPoints: [CGPoint]?
|
||||
private var toPoints: [CGPoint]?
|
||||
|
||||
private var currentPoints: [CGPoint]? {
|
||||
guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil }
|
||||
|
||||
return fromPoints.enumerated().map { offset, fromPoint in
|
||||
let toPoint = toPoints[offset]
|
||||
return CGPoint(
|
||||
x: fromPoint.x + (toPoint.x - fromPoint.x) * transition,
|
||||
y: fromPoint.y + (toPoint.y - fromPoint.y) * transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
pointsCount: Int,
|
||||
minRandomness: CGFloat,
|
||||
|
|
@ -749,7 +725,7 @@ final class CurveView: UIView {
|
|||
|
||||
super.init(frame: .zero)
|
||||
|
||||
layer.addSublayer(shapeLayer)
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -757,7 +733,7 @@ final class CurveView: UIView {
|
|||
}
|
||||
|
||||
func setColor(_ color: UIColor) {
|
||||
shapeLayer.fillColor = color.cgColor
|
||||
self.shapeLayer.fillColor = color.cgColor
|
||||
}
|
||||
|
||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||
|
|
@ -769,57 +745,40 @@ final class CurveView: UIView {
|
|||
}
|
||||
|
||||
func startAnimating() {
|
||||
animateToNewShape()
|
||||
self.animateToNewShape()
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "curve")
|
||||
self.shapeLayer.removeAnimation(forKey: "path")
|
||||
}
|
||||
|
||||
private func animateToNewShape() {
|
||||
if pop_animation(forKey: "curve") != nil {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "curve")
|
||||
if self.shapeLayer.path == nil {
|
||||
let points = self.generateNextCurve(for: self.bounds.size)
|
||||
self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: self.smoothness, curve: true).cgPath
|
||||
}
|
||||
|
||||
if fromPoints == nil {
|
||||
fromPoints = generateNextCurve(for: bounds.size)
|
||||
}
|
||||
if toPoints == nil {
|
||||
toPoints = generateNextCurve(for: bounds.size)
|
||||
}
|
||||
let nextPoints = self.generateNextCurve(for: self.bounds.size)
|
||||
let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: self.smoothness, curve: true).cgPath
|
||||
|
||||
let animation = POPBasicAnimation()
|
||||
animation.property = POPAnimatableProperty.property(withName: "curve.transition", initializer: { property in
|
||||
property?.readBlock = { curveView, values in
|
||||
guard let curveView = curveView as? CurveView, let values = values else { return }
|
||||
|
||||
values.pointee = curveView.transition
|
||||
}
|
||||
property?.writeBlock = { curveView, values in
|
||||
guard let curveView = curveView as? CurveView, let values = values else { return }
|
||||
|
||||
curveView.transition = values.pointee
|
||||
}
|
||||
}) as? POPAnimatableProperty
|
||||
animation.completionBlock = { [weak self] animation, finished in
|
||||
let animation = CABasicAnimation(keyPath: "path")
|
||||
let previousPath = self.shapeLayer.path
|
||||
self.shapeLayer.path = nextPath
|
||||
animation.duration = CFTimeInterval(1 / (self.minSpeed + (self.maxSpeed - self.minSpeed) * self.speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = previousPath
|
||||
animation.toValue = nextPath
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.fillMode = .forwards
|
||||
animation.completion = { [weak self] finished in
|
||||
if finished {
|
||||
self?.fromPoints = self?.currentPoints
|
||||
self?.toPoints = nil
|
||||
self?.animateToNewShape()
|
||||
}
|
||||
}
|
||||
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 1
|
||||
pop_add(animation, forKey: "curve")
|
||||
self.shapeLayer.add(animation, forKey: "path")
|
||||
|
||||
lastSpeedLevel = speedLevel
|
||||
speedLevel = 0
|
||||
self.lastSpeedLevel = self.speedLevel
|
||||
self.speedLevel = 0
|
||||
}
|
||||
|
||||
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
|
||||
|
|
@ -871,8 +830,8 @@ final class CurveView: UIView {
|
|||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
shapeLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
shapeLayer.bounds = self.bounds
|
||||
self.shapeLayer.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0)
|
||||
self.shapeLayer.bounds = self.bounds
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -625,6 +625,11 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
private var isScheduled = false
|
||||
private var isScheduledStarted = false
|
||||
|
||||
private let isSpeakingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
public var isSpeaking: Signal<Bool, NoError> {
|
||||
return self.isSpeakingPromise.get()
|
||||
}
|
||||
|
||||
private var screencastFramesDisposable: Disposable?
|
||||
private var screencastStateDisposable: Disposable?
|
||||
|
||||
|
|
@ -1608,6 +1613,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
var result: [(PeerId, UInt32, Float, Bool)] = []
|
||||
var myLevel: Float = 0.0
|
||||
var myLevelHasVoice: Bool = false
|
||||
var orignalMyLevelHasVoice: Bool = false
|
||||
var missingSsrcs = Set<UInt32>()
|
||||
for (ssrcKey, level, hasVoice) in levels {
|
||||
var peerId: PeerId?
|
||||
|
|
@ -1622,6 +1628,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
}
|
||||
if let peerId = peerId {
|
||||
if case .local = ssrcKey {
|
||||
orignalMyLevelHasVoice = hasVoice
|
||||
if !strongSelf.isMutedValue.isEffectivelyMuted {
|
||||
myLevel = level
|
||||
myLevelHasVoice = hasVoice
|
||||
|
|
@ -1638,6 +1645,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
let mappedLevel = myLevel * 1.5
|
||||
strongSelf.myAudioLevelPipe.putNext(mappedLevel)
|
||||
strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice)
|
||||
strongSelf.isSpeakingPromise.set(orignalMyLevelHasVoice)
|
||||
|
||||
if !missingSsrcs.isEmpty {
|
||||
strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs)
|
||||
|
|
@ -2919,22 +2927,22 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
}
|
||||
|
||||
public func makeIncomingVideoView(endpointId: String, requestClone: Bool, completion: @escaping (PresentationCallVideoView?, PresentationCallVideoView?) -> Void) {
|
||||
self.genericCallContext?.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: { view, _ in
|
||||
if let view = view {
|
||||
let setOnFirstFrameReceived = view.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = view.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated
|
||||
let updateIsEnabled = view.updateIsEnabled
|
||||
completion(PresentationCallVideoView(
|
||||
holder: view,
|
||||
view: view.view,
|
||||
self.genericCallContext?.makeIncomingVideoView(endpointId: endpointId, requestClone: requestClone, completion: { mainView, cloneView in
|
||||
if let mainView = mainView {
|
||||
let setOnFirstFrameReceived = mainView.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = mainView.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = mainView.setOnIsMirroredUpdated
|
||||
let updateIsEnabled = mainView.updateIsEnabled
|
||||
let mainVideoView = PresentationCallVideoView(
|
||||
holder: mainView,
|
||||
view: mainView.view,
|
||||
setOnFirstFrameReceived: { f in
|
||||
setOnFirstFrameReceived(f)
|
||||
},
|
||||
getOrientation: { [weak view] in
|
||||
if let view = view {
|
||||
getOrientation: { [weak mainView] in
|
||||
if let mainView = mainView {
|
||||
let mappedValue: PresentationCallVideoView.Orientation
|
||||
switch view.getOrientation() {
|
||||
switch mainView.getOrientation() {
|
||||
case .rotation0:
|
||||
mappedValue = .rotation0
|
||||
case .rotation90:
|
||||
|
|
@ -2949,9 +2957,9 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
return .rotation0
|
||||
}
|
||||
},
|
||||
getAspect: { [weak view] in
|
||||
if let view = view {
|
||||
return view.getAspect()
|
||||
getAspect: { [weak mainView] in
|
||||
if let mainView = mainView {
|
||||
return mainView.getAspect()
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
|
|
@ -2980,7 +2988,73 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
|
|||
updateIsEnabled: { value in
|
||||
updateIsEnabled(value)
|
||||
}
|
||||
), nil)
|
||||
)
|
||||
|
||||
var cloneVideoView: PresentationCallVideoView?
|
||||
if let cloneView = cloneView {
|
||||
let setOnFirstFrameReceived = cloneView.setOnFirstFrameReceived
|
||||
let setOnOrientationUpdated = cloneView.setOnOrientationUpdated
|
||||
let setOnIsMirroredUpdated = cloneView.setOnIsMirroredUpdated
|
||||
let updateIsEnabled = cloneView.updateIsEnabled
|
||||
cloneVideoView = PresentationCallVideoView(
|
||||
holder: cloneView,
|
||||
view: cloneView.view,
|
||||
setOnFirstFrameReceived: { f in
|
||||
setOnFirstFrameReceived(f)
|
||||
},
|
||||
getOrientation: { [weak cloneView] in
|
||||
if let cloneView = cloneView {
|
||||
let mappedValue: PresentationCallVideoView.Orientation
|
||||
switch cloneView.getOrientation() {
|
||||
case .rotation0:
|
||||
mappedValue = .rotation0
|
||||
case .rotation90:
|
||||
mappedValue = .rotation90
|
||||
case .rotation180:
|
||||
mappedValue = .rotation180
|
||||
case .rotation270:
|
||||
mappedValue = .rotation270
|
||||
}
|
||||
return mappedValue
|
||||
} else {
|
||||
return .rotation0
|
||||
}
|
||||
},
|
||||
getAspect: { [weak cloneView] in
|
||||
if let cloneView = cloneView {
|
||||
return cloneView.getAspect()
|
||||
} else {
|
||||
return 0.0
|
||||
}
|
||||
},
|
||||
setOnOrientationUpdated: { f in
|
||||
setOnOrientationUpdated { value, aspect in
|
||||
let mappedValue: PresentationCallVideoView.Orientation
|
||||
switch value {
|
||||
case .rotation0:
|
||||
mappedValue = .rotation0
|
||||
case .rotation90:
|
||||
mappedValue = .rotation90
|
||||
case .rotation180:
|
||||
mappedValue = .rotation180
|
||||
case .rotation270:
|
||||
mappedValue = .rotation270
|
||||
}
|
||||
f?(mappedValue, aspect)
|
||||
}
|
||||
},
|
||||
setOnIsMirroredUpdated: { f in
|
||||
setOnIsMirroredUpdated { value in
|
||||
f?(value)
|
||||
}
|
||||
},
|
||||
updateIsEnabled: { value in
|
||||
updateIsEnabled(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
completion(mainVideoView, cloneVideoView)
|
||||
} else {
|
||||
completion(nil, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import UIKit
|
|||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import LegacyComponents
|
||||
import AnimationUI
|
||||
import AppBundle
|
||||
import ManagedAnimationNode
|
||||
|
|
@ -1370,13 +1369,11 @@ final class BlobView: UIView {
|
|||
|
||||
var level: CGFloat = 0 {
|
||||
didSet {
|
||||
return;
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
let lv = minScale + (maxScale - minScale) * level
|
||||
shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
|
||||
self.scaleUpdated?(level)
|
||||
let lv = self.minScale + (self.maxScale - self.minScale) * self.level
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1)
|
||||
self.scaleUpdated?(self.level)
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
|
@ -1389,30 +1386,7 @@ final class BlobView: UIView {
|
|||
layer.strokeColor = nil
|
||||
return layer
|
||||
}()
|
||||
|
||||
private var transition: CGFloat = 0 {
|
||||
didSet {
|
||||
guard let currentPoints = currentPoints else { return }
|
||||
|
||||
shapeLayer.path = UIBezierPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness).cgPath
|
||||
}
|
||||
}
|
||||
|
||||
private var fromPoints: [CGPoint]?
|
||||
private var toPoints: [CGPoint]?
|
||||
|
||||
private var currentPoints: [CGPoint]? {
|
||||
guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil }
|
||||
|
||||
return fromPoints.enumerated().map { offset, fromPoint in
|
||||
let toPoint = toPoints[offset]
|
||||
return CGPoint(
|
||||
x: fromPoint.x + (toPoint.x - fromPoint.x) * transition,
|
||||
y: fromPoint.y + (toPoint.y - fromPoint.y) * transition
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
pointsCount: Int,
|
||||
minRandomness: CGFloat,
|
||||
|
|
@ -1435,9 +1409,9 @@ final class BlobView: UIView {
|
|||
|
||||
super.init(frame: .zero)
|
||||
|
||||
layer.addSublayer(shapeLayer)
|
||||
self.layer.addSublayer(self.shapeLayer)
|
||||
|
||||
shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
@ -1445,11 +1419,11 @@ final class BlobView: UIView {
|
|||
}
|
||||
|
||||
func setColor(_ color: UIColor) {
|
||||
shapeLayer.fillColor = color.cgColor
|
||||
self.shapeLayer.fillColor = color.cgColor
|
||||
}
|
||||
|
||||
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||
speedLevel = max(speedLevel, newSpeedLevel)
|
||||
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
||||
|
||||
// if abs(lastSpeedLevel - newSpeedLevel) > 0.45 {
|
||||
// animateToNewShape()
|
||||
|
|
@ -1457,57 +1431,41 @@ final class BlobView: UIView {
|
|||
}
|
||||
|
||||
func startAnimating() {
|
||||
animateToNewShape()
|
||||
self.animateToNewShape()
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
self.shapeLayer.removeAnimation(forKey: "path")
|
||||
}
|
||||
|
||||
private func animateToNewShape() {
|
||||
if pop_animation(forKey: "blob") != nil {
|
||||
fromPoints = currentPoints
|
||||
toPoints = nil
|
||||
pop_removeAnimation(forKey: "blob")
|
||||
if self.shapeLayer.path == nil {
|
||||
let points = generateNextBlob(for: self.bounds.size)
|
||||
self.shapeLayer.path = UIBezierPath.smoothCurve(through: points, length: bounds.width, smoothness: smoothness).cgPath
|
||||
}
|
||||
|
||||
if fromPoints == nil {
|
||||
fromPoints = generateNextBlob(for: bounds.size)
|
||||
}
|
||||
if toPoints == nil {
|
||||
toPoints = generateNextBlob(for: bounds.size)
|
||||
}
|
||||
let nextPoints = generateNextBlob(for: self.bounds.size)
|
||||
let nextPath = UIBezierPath.smoothCurve(through: nextPoints, length: bounds.width, smoothness: smoothness).cgPath
|
||||
|
||||
let animation = POPBasicAnimation()
|
||||
animation.property = POPAnimatableProperty.property(withName: "blob.transition", initializer: { property in
|
||||
property?.readBlock = { blobView, values in
|
||||
guard let blobView = blobView as? BlobView, let values = values else { return }
|
||||
|
||||
values.pointee = blobView.transition
|
||||
}
|
||||
property?.writeBlock = { blobView, values in
|
||||
guard let blobView = blobView as? BlobView, let values = values else { return }
|
||||
|
||||
blobView.transition = values.pointee
|
||||
}
|
||||
}) as? POPAnimatableProperty
|
||||
animation.completionBlock = { [weak self] animation, finished in
|
||||
let animation = CABasicAnimation(keyPath: "path")
|
||||
let previousPath = self.shapeLayer.path
|
||||
self.shapeLayer.path = nextPath
|
||||
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = previousPath
|
||||
animation.toValue = nextPath
|
||||
animation.isRemovedOnCompletion = false
|
||||
animation.fillMode = .forwards
|
||||
animation.completion = { [weak self] finished in
|
||||
if finished {
|
||||
self?.fromPoints = self?.currentPoints
|
||||
self?.toPoints = nil
|
||||
self?.animateToNewShape()
|
||||
}
|
||||
}
|
||||
animation.duration = CFTimeInterval(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel))
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 1
|
||||
//pop_add(animation, forKey: "blob")
|
||||
|
||||
self.shapeLayer.add(animation, forKey: "path")
|
||||
|
||||
lastSpeedLevel = speedLevel
|
||||
speedLevel = 0
|
||||
self.lastSpeedLevel = self.speedLevel
|
||||
self.speedLevel = 0
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class VoiceChatActionItem: ListViewItem {
|
|||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = VoiceChatActionItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, false, nextItem == nil)
|
||||
let (layout, apply) = node.asyncLayout()(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
|
@ -74,7 +74,7 @@ class VoiceChatActionItem: ListViewItem {
|
|||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, false, nextItem == nil)
|
||||
let (layout, apply) = makeLayout(self, params, previousItem == nil || previousItem is VoiceChatTilesGridItem, nextItem == nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
|
|
@ -95,6 +95,7 @@ class VoiceChatActionItem: ListViewItem {
|
|||
class VoiceChatActionItemNode: ListViewItemNode {
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightContainerNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
|
|
@ -121,13 +122,17 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.highlightContainerNode = ASDisplayNode()
|
||||
self.highlightContainerNode.clipsToBounds = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.highlightContainerNode.addSubnode(self.highlightedBackgroundNode)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
|
@ -138,11 +143,19 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: VoiceChatActionItem, _ params: ListViewItemLayoutParams, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOS 13.0, *) {
|
||||
self.highlightContainerNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: VoiceChatActionItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, firstWithHeader, last in
|
||||
return { item, params, first, last in
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedContent = false
|
||||
|
||||
|
|
@ -165,7 +178,7 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
let contentHeight: CGFloat = 12.0 * 2.0 + titleLayout.size.height
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: contentHeight)
|
||||
let insets = UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
let insets = UIEdgeInsets()
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
|
@ -214,6 +227,10 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset + 1.0, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightContainerNode.frame = CGRect(origin: CGPoint(x: params.leftInset, y: -UIScreenPixel), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height + UIScreenPixel + UIScreenPixel + 11.0))
|
||||
|
||||
strongSelf.highlightContainerNode.cornerRadius = first ? 11.0 : 0.0
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
|
|
@ -224,8 +241,8 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.highlightContainerNode.alpha = 1.0
|
||||
if self.highlightContainerNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
|
|
@ -233,24 +250,24 @@ class VoiceChatActionItemNode: ListViewItemNode {
|
|||
anchorNode = self.topStripeNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
self.insertSubnode(self.highlightContainerNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
self.addSubnode(self.highlightContainerNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if self.highlightContainerNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
self.highlightContainerNode.layer.animateAlpha(from: self.highlightContainerNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
strongSelf.highlightContainerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
self.highlightContainerNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
self.highlightContainerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
private let contentBackgroundNode: ASDisplayNode
|
||||
private let titleNode: ASTextNode
|
||||
private let previewContainerNode: ASDisplayNode
|
||||
private let shimmerNode: ShimmerEffectForegroundNode
|
||||
private let cameraButton: SolidRoundedButtonNode
|
||||
private let screenButton: SolidRoundedButtonNode
|
||||
private var broadcastPickerView: UIView?
|
||||
|
|
@ -135,6 +136,8 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private let readyDisposable = MetaDisposable()
|
||||
|
||||
var shareCamera: ((Bool) -> Void)?
|
||||
var switchCamera: (() -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
|
|
@ -200,7 +203,10 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
self.previewContainerNode = ASDisplayNode()
|
||||
self.previewContainerNode.clipsToBounds = true
|
||||
self.previewContainerNode.cornerRadius = 11.0
|
||||
self.previewContainerNode.backgroundColor = .black
|
||||
self.previewContainerNode.backgroundColor = UIColor(rgb: 0x2b2b2f)
|
||||
|
||||
self.shimmerNode = ShimmerEffectForegroundNode(size: 200.0)
|
||||
self.previewContainerNode.addSubnode(self.shimmerNode)
|
||||
|
||||
self.microphoneButton = HighlightTrackingButtonNode()
|
||||
self.microphoneButton.isSelected = true
|
||||
|
|
@ -293,9 +299,19 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.readyDisposable.set(self.cameraNode.ready.start(next: { [weak self] ready in
|
||||
if let strongSelf = self {
|
||||
Queue.mainQueue().after(0.07) {
|
||||
strongSelf.shimmerNode.alpha = 0.0
|
||||
strongSelf.shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.readyDisposable.dispose()
|
||||
self.applicationStateDisposable?.dispose()
|
||||
}
|
||||
|
||||
|
|
@ -407,6 +423,13 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
let isLandscape: Bool
|
||||
if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass {
|
||||
isLandscape = true
|
||||
} else {
|
||||
isLandscape = false
|
||||
}
|
||||
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
|
@ -419,9 +442,14 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
let titleHeight: CGFloat = 54.0
|
||||
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
|
||||
let innerContentHeight: CGFloat = layout.size.height - contentHeight - 160.0
|
||||
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + innerContentHeight + buttonOffset
|
||||
var width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
if isLandscape {
|
||||
contentHeight = layout.size.height
|
||||
width = layout.size.width
|
||||
} else {
|
||||
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + innerContentHeight + buttonOffset
|
||||
}
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
let previewInset: CGFloat = 16.0
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
|
|
@ -432,6 +460,7 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
if backgroundFrame.minY < contentFrame.minY {
|
||||
backgroundFrame.origin.y = contentFrame.minY
|
||||
}
|
||||
transition.updateAlpha(node: self.titleNode, alpha: isLandscape ? 0.0 : 1.0)
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
|
|
@ -442,11 +471,23 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 18.0), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
let previewSize = CGSize(width: contentFrame.width - previewInset * 2.0, height: contentHeight - 243.0 - bottomInset)
|
||||
transition.updateFrame(node: self.previewContainerNode, frame: CGRect(origin: CGPoint(x: previewInset, y: 56.0), size: previewSize))
|
||||
let previewSize: CGSize
|
||||
let previewFrame: CGRect
|
||||
if isLandscape {
|
||||
let previewHeight = contentHeight - layout.intrinsicInsets.bottom - 52.0 - 10.0
|
||||
previewSize = CGSize(width: min(contentFrame.width - layout.safeInsets.left - layout.safeInsets.right, previewHeight * 1.7778), height: previewHeight)
|
||||
previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((contentFrame.width - previewSize.width) / 2.0), y: 0.0), size: previewSize)
|
||||
} else {
|
||||
previewSize = CGSize(width: contentFrame.width - previewInset * 2.0, height: contentHeight - 243.0 - bottomInset)
|
||||
previewFrame = CGRect(origin: CGPoint(x: previewInset, y: 56.0), size: previewSize)
|
||||
}
|
||||
transition.updateFrame(node: self.previewContainerNode, frame: previewFrame)
|
||||
transition.updateFrame(node: self.shimmerNode, frame: CGRect(origin: CGPoint(), size: previewFrame.size))
|
||||
self.shimmerNode.update(foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.07))
|
||||
self.shimmerNode.updateAbsoluteRect(previewFrame, within: layout.size)
|
||||
|
||||
self.cameraNode.frame = CGRect(origin: CGPoint(), size: previewSize)
|
||||
self.cameraNode.updateLayout(size: previewSize, layoutMode: .fillVertical, transition: .immediate)
|
||||
self.cameraNode.updateLayout(size: previewSize, layoutMode: isLandscape ? .fillHorizontal : .fillVertical, transition: .immediate)
|
||||
|
||||
let microphoneFrame = CGRect(x: 16.0, y: previewSize.height - 48.0 - 16.0, width: 48.0, height: 48.0)
|
||||
transition.updateFrame(node: self.microphoneButton, frame: microphoneFrame)
|
||||
|
|
@ -459,20 +500,46 @@ private class VoiceChatCameraPreviewControllerNode: ViewControllerTracingNode, U
|
|||
transition.updateFrame(view: self.switchCameraEffectView, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
|
||||
transition.updateFrame(node: self.switchCameraIconNode, frame: CGRect(origin: CGPoint(), size: switchCameraFrame.size))
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let cameraButtonHeight = self.cameraButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: cameraButtonHeight))
|
||||
|
||||
let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: screenButtonHeight))
|
||||
if let broadcastPickerView = self.broadcastPickerView {
|
||||
broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width + 1000.0, height: screenButtonHeight)
|
||||
if isLandscape {
|
||||
var buttonsCount: Int = 2
|
||||
if let _ = self.broadcastPickerView {
|
||||
buttonsCount += 1
|
||||
} else {
|
||||
self.screenButton.isHidden = true
|
||||
}
|
||||
let buttonInset: CGFloat = 6.0
|
||||
|
||||
let buttonWidth = floorToScreenPixels((contentFrame.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(buttonsCount + 1) * buttonInset) / CGFloat(buttonsCount))
|
||||
|
||||
let cameraButtonHeight = self.cameraButton.updateLayout(width: buttonWidth, transition: transition)
|
||||
let screenButtonHeight = self.screenButton.updateLayout(width: buttonWidth, transition: transition)
|
||||
let cancelButtonHeight = self.cancelButton.updateLayout(width: buttonWidth, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: layout.safeInsets.left + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cancelButtonHeight))
|
||||
if let broadcastPickerView = self.broadcastPickerView {
|
||||
transition.updateFrame(node: self.screenButton, frame: CGRect(x: layout.safeInsets.left + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight))
|
||||
broadcastPickerView.frame = CGRect(x: layout.safeInsets.left + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: screenButtonHeight)
|
||||
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + buttonInset + buttonWidth + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight))
|
||||
} else {
|
||||
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: layout.safeInsets.left + buttonInset + buttonWidth + buttonInset, y: previewFrame.maxY + 10.0, width: buttonWidth, height: cameraButtonHeight))
|
||||
}
|
||||
|
||||
} else {
|
||||
self.screenButton.isHidden = true
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let cameraButtonHeight = self.cameraButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cameraButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: cameraButtonHeight))
|
||||
|
||||
let screenButtonHeight = self.screenButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.screenButton, frame: CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: screenButtonHeight))
|
||||
if let broadcastPickerView = self.broadcastPickerView {
|
||||
broadcastPickerView.frame = CGRect(x: buttonInset, y: contentHeight - cameraButtonHeight - 8.0 - screenButtonHeight - insets.bottom - 16.0, width: contentFrame.width + 1000.0, height: screenButtonHeight)
|
||||
} else {
|
||||
self.screenButton.isHidden = true
|
||||
}
|
||||
|
||||
let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight))
|
||||
}
|
||||
|
||||
let cancelButtonHeight = self.cancelButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(x: buttonInset, y: contentHeight - cancelButtonHeight - insets.bottom - 16.0, width: contentFrame.width, height: cancelButtonHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ struct VoiceChatPeerEntry: Identifiable {
|
|||
var peer: Peer
|
||||
var about: String?
|
||||
var isMyPeer: Bool
|
||||
var videoEndpointId: String?
|
||||
var presentationEndpointId: String?
|
||||
var _videoEndpointId: String?
|
||||
var _presentationEndpointId: String?
|
||||
var state: State
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
var canManageCall: Bool
|
||||
|
|
@ -107,6 +107,22 @@ struct VoiceChatPeerEntry: Identifiable {
|
|||
var active: Bool
|
||||
var isLandscape: Bool
|
||||
|
||||
var videoEndpointId: String? {
|
||||
if let muteState = self.muteState, !muteState.canUnmute || muteState.mutedByYou {
|
||||
return nil
|
||||
} else {
|
||||
return self._videoEndpointId
|
||||
}
|
||||
}
|
||||
|
||||
var presentationEndpointId: String? {
|
||||
if let muteState = self.muteState, !muteState.canUnmute || muteState.mutedByYou {
|
||||
return nil
|
||||
} else {
|
||||
return self._presentationEndpointId
|
||||
}
|
||||
}
|
||||
|
||||
var effectiveVideoEndpointId: String? {
|
||||
return self.presentationEndpointId ?? self.videoEndpointId
|
||||
}
|
||||
|
|
@ -129,8 +145,8 @@ struct VoiceChatPeerEntry: Identifiable {
|
|||
self.peer = peer
|
||||
self.about = about
|
||||
self.isMyPeer = isMyPeer
|
||||
self.videoEndpointId = videoEndpointId
|
||||
self.presentationEndpointId = presentationEndpointId
|
||||
self._videoEndpointId = videoEndpointId
|
||||
self._presentationEndpointId = presentationEndpointId
|
||||
self.state = state
|
||||
self.muteState = muteState
|
||||
self.canManageCall = canManageCall
|
||||
|
|
@ -155,10 +171,10 @@ struct VoiceChatPeerEntry: Identifiable {
|
|||
if lhs.isMyPeer != rhs.isMyPeer {
|
||||
return false
|
||||
}
|
||||
if lhs.videoEndpointId != rhs.videoEndpointId {
|
||||
if lhs._videoEndpointId != rhs._videoEndpointId {
|
||||
return false
|
||||
}
|
||||
if lhs.presentationEndpointId != rhs.presentationEndpointId {
|
||||
if lhs._presentationEndpointId != rhs._presentationEndpointId {
|
||||
return false
|
||||
}
|
||||
if lhs.state != rhs.state {
|
||||
|
|
@ -763,6 +779,7 @@ public final class VoiceChatController: ViewController {
|
|||
private var callStateDisposable: Disposable?
|
||||
|
||||
private var pushingToTalk = false
|
||||
private var temporaryPushingToTalk = false
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var callState: PresentationGroupCallState?
|
||||
|
|
@ -782,6 +799,7 @@ public final class VoiceChatController: ViewController {
|
|||
|
||||
private var audioLevelsDisposable: Disposable?
|
||||
private var myAudioLevelDisposable: Disposable?
|
||||
private var isSpeakingDisposable: Disposable?
|
||||
private var memberStatesDisposable: Disposable?
|
||||
private var actionButtonColorDisposable: Disposable?
|
||||
|
||||
|
|
@ -827,7 +845,10 @@ public final class VoiceChatController: ViewController {
|
|||
private var connectedOnce = false
|
||||
private var ignoreConnecting = false
|
||||
private var ignoreConnectingTimer: SwiftSignalKit.Timer?
|
||||
|
||||
|
||||
private var displayUnmuteTooltipTimer: SwiftSignalKit.Timer?
|
||||
private var lastUnmuteTooltipDisplayTimestamp: Double?
|
||||
|
||||
private var displayMode: DisplayMode = .modal(isExpanded: false, isFilled: false) {
|
||||
didSet {
|
||||
if case let .modal(isExpanded, _) = self.displayMode {
|
||||
|
|
@ -1858,7 +1879,7 @@ public final class VoiceChatController: ViewController {
|
|||
if let (currentPeerId, _, timestamp) = strongSelf.currentDominantSpeaker {
|
||||
if CACurrentMediaTime() - timestamp > 2.5 && peerId != currentPeerId {
|
||||
strongSelf.currentDominantSpeaker = (peerId, nil, CACurrentMediaTime())
|
||||
strongSelf.updateMainVideo(waitForFullSize: false)
|
||||
strongSelf.updateMainVideo(waitForFullSize: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1879,6 +1900,40 @@ public final class VoiceChatController: ViewController {
|
|||
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
|
||||
})
|
||||
|
||||
self.isSpeakingDisposable = (self.call.isSpeaking
|
||||
|> deliverOnMainQueue).start(next: { [weak self] isSpeaking in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
|
||||
strongSelf.displayUnmuteTooltipTimer?.invalidate()
|
||||
} else {
|
||||
if isSpeaking {
|
||||
var shouldDisplayTooltip = false
|
||||
if let previousTimstamp = strongSelf.lastUnmuteTooltipDisplayTimestamp, CACurrentMediaTime() < previousTimstamp + 60.0 {
|
||||
shouldDisplayTooltip = true
|
||||
} else if strongSelf.lastUnmuteTooltipDisplayTimestamp == nil {
|
||||
shouldDisplayTooltip = true
|
||||
}
|
||||
if shouldDisplayTooltip {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.lastUnmuteTooltipDisplayTimestamp = CACurrentMediaTime()
|
||||
strongSelf.displayUnmuteTooltip()
|
||||
strongSelf.displayUnmuteTooltipTimer?.invalidate()
|
||||
strongSelf.displayUnmuteTooltipTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
timer.start()
|
||||
strongSelf.displayUnmuteTooltipTimer = timer
|
||||
}
|
||||
} else {
|
||||
strongSelf.displayUnmuteTooltipTimer?.invalidate()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.leaveButton.addTarget(self, action: #selector(self.leavePressed), forControlEvents: .touchUpInside)
|
||||
self.actionButton.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside)
|
||||
self.audioButton.addTarget(self, action: #selector(self.audioPressed), forControlEvents: .touchUpInside)
|
||||
|
|
@ -2063,6 +2118,7 @@ public final class VoiceChatController: ViewController {
|
|||
self.memberStatesDisposable?.dispose()
|
||||
self.audioLevelsDisposable?.dispose()
|
||||
self.myAudioLevelDisposable?.dispose()
|
||||
self.isSpeakingDisposable?.dispose()
|
||||
self.inviteDisposable.dispose()
|
||||
self.memberEventsDisposable.dispose()
|
||||
self.reconnectedAsEventsDisposable.dispose()
|
||||
|
|
@ -2827,9 +2883,11 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
private var actionButtonPressTimer: SwiftSignalKit.Timer?
|
||||
private var actionButtonPressedTimestamp: Double?
|
||||
private func startActionButtonPressTimer() {
|
||||
self.actionButtonPressTimer?.invalidate()
|
||||
let pressTimer = SwiftSignalKit.Timer(timeout: 0.185, repeat: false, completion: { [weak self] in
|
||||
self?.actionButtonPressedTimestamp = CACurrentMediaTime()
|
||||
self?.actionButtonPressTimerFired()
|
||||
self?.actionButtonPressTimer = nil
|
||||
}, queue: Queue.mainQueue())
|
||||
|
|
@ -2919,20 +2977,35 @@ public final class VoiceChatController: ViewController {
|
|||
case .began:
|
||||
self.actionButton.pressing = true
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.actionButtonPressedTimestamp = nil
|
||||
self.startActionButtonPressTimer()
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
self.pushingToTalk = false
|
||||
self.actionButton.pressing = false
|
||||
|
||||
if self.actionButtonPressTimer != nil {
|
||||
self.pushingToTalk = false
|
||||
self.actionButton.pressing = false
|
||||
|
||||
self.stopActionButtonPressTimer()
|
||||
self.call.toggleIsMuted()
|
||||
} else {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.call.setIsMuted(action: .muted(isPushToTalkActive: false))
|
||||
if self.pushingToTalk, let timestamp = self.actionButtonPressedTimestamp, CACurrentMediaTime() < timestamp + 0.5 {
|
||||
self.pushingToTalk = false
|
||||
self.temporaryPushingToTalk = true
|
||||
self.call.setIsMuted(action: .unmuted)
|
||||
|
||||
Queue.mainQueue().after(0.1) {
|
||||
self.temporaryPushingToTalk = false
|
||||
self.actionButton.pressing = false
|
||||
}
|
||||
} else {
|
||||
self.pushingToTalk = false
|
||||
self.actionButton.pressing = false
|
||||
|
||||
self.call.setIsMuted(action: .muted(isPushToTalkActive: false))
|
||||
}
|
||||
}
|
||||
|
||||
if let callState = self.callState {
|
||||
|
|
@ -3533,7 +3606,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
let videoButtonSize: CGSize
|
||||
let audioButtonSize: CGSize
|
||||
var buttonsTitleAlpha: CGFloat
|
||||
var effectiveDisplayMode = self.displayMode
|
||||
if let (layout, _) = self.validLayout, layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass {
|
||||
|
|
@ -3543,34 +3616,37 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
let hasCameraButton = self.cameraButton.isUserInteractionEnabled
|
||||
let hasVideo = self.call.hasVideo
|
||||
switch effectiveDisplayMode {
|
||||
case .modal:
|
||||
videoButtonSize = smallButtonSize
|
||||
audioButtonSize = hasCameraButton ? smallButtonSize : sideButtonSize
|
||||
buttonsTitleAlpha = 1.0
|
||||
case .fullscreen:
|
||||
videoButtonSize = sideButtonSize
|
||||
audioButtonSize = sideButtonSize
|
||||
buttonsTitleAlpha = 0.0
|
||||
}
|
||||
|
||||
let hasVideo = self.call.hasVideo
|
||||
self.cameraButton.update(size: hasVideo ? sideButtonSize : videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
|
||||
self.cameraButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: hasVideo ? activeButtonAppearance : normalButtonAppearance, image: hasVideo ? .cameraOn : .cameraOff), text: self.presentationData.strings.VoiceChat_Video, transition: transition)
|
||||
|
||||
self.switchCameraButton.update(size: videoButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition)
|
||||
self.switchCameraButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: normalButtonAppearance, image: .flipCamera), text: "", transition: transition)
|
||||
|
||||
transition.updateAlpha(node: self.switchCameraButton, alpha: hasVideo ? 1.0 : 0.0)
|
||||
transition.updateTransformScale(node: self.switchCameraButton, scale: hasVideo ? 1.0 : 0.0)
|
||||
|
||||
transition.updateTransformScale(node: self.cameraButton, scale: hasCameraButton ? 1.0 : 0.0)
|
||||
|
||||
transition.updateAlpha(node: self.audioButton, alpha: hasVideo ? 0.0 : 1.0)
|
||||
transition.updateTransformScale(node: self.audioButton, scale: hasVideo ? 0.0 : 1.0)
|
||||
|
||||
self.audioButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: isSoundEnabled), text: soundTitle, transition: transition)
|
||||
self.audioButton.update(size: audioButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage, isEnabled: isSoundEnabled), text: soundTitle, transition: transition)
|
||||
self.audioButton.isUserInteractionEnabled = isSoundEnabled
|
||||
|
||||
self.leaveButton.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0xff3b30, 0.3)), image: .cancel), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
|
||||
|
||||
transition.updateAlpha(node: self.cameraButton.textNode, alpha: hasVideo ? buttonsTitleAlpha : 0.0)
|
||||
transition.updateAlpha(node: self.cameraButton.textNode, alpha: hasCameraButton ? buttonsTitleAlpha : 0.0)
|
||||
transition.updateAlpha(node: self.switchCameraButton.textNode, alpha: buttonsTitleAlpha)
|
||||
transition.updateAlpha(node: self.audioButton.textNode, alpha: buttonsTitleAlpha)
|
||||
transition.updateAlpha(node: self.audioButton.textNode, alpha: hasCameraButton ? 0.0 : buttonsTitleAlpha)
|
||||
transition.updateAlpha(node: self.leaveButton.textNode, alpha: buttonsTitleAlpha)
|
||||
}
|
||||
|
||||
|
|
@ -3796,7 +3872,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
|
||||
if connected {
|
||||
if let muteState = callState.muteState, !self.pushingToTalk {
|
||||
if let muteState = callState.muteState, !self.pushingToTalk && !self.temporaryPushingToTalk {
|
||||
if muteState.canUnmute {
|
||||
actionButtonState = .active(state: .muted)
|
||||
|
||||
|
|
@ -3844,9 +3920,6 @@ public final class VoiceChatController: ViewController {
|
|||
self.actionButton.update(size: centralButtonSize, buttonSize: CGSize(width: 112.0, height: 112.0), state: actionButtonState, title: actionButtonTitle, subtitle: actionButtonSubtitle, dark: self.isFullscreen, small: smallButtons, animated: true)
|
||||
|
||||
var hasCameraButton = self.callState?.isVideoEnabled ?? false
|
||||
#if DEBUG
|
||||
hasCameraButton = true
|
||||
#endif
|
||||
|
||||
switch actionButtonState {
|
||||
case let .active(state):
|
||||
|
|
@ -3967,6 +4040,8 @@ public final class VoiceChatController: ViewController {
|
|||
} : nil)
|
||||
}
|
||||
|
||||
self.cameraButton.isUserInteractionEnabled = hasCameraButton
|
||||
|
||||
var buttonsTransition: ContainedViewLayoutTransition = .immediate
|
||||
if !isFirstTime {
|
||||
if case .animated(_, .spring) = transition {
|
||||
|
|
@ -3977,22 +4052,21 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
self.updateButtons(transition: buttonsTransition)
|
||||
|
||||
self.cameraButton.isUserInteractionEnabled = hasCameraButton
|
||||
if self.audioButton.supernode === self.bottomPanelNode {
|
||||
transition.updateAlpha(node: self.cameraButton, alpha: hasCameraButton ? 1.0 : 0.0)
|
||||
transition.updateFrameAsPositionAndBounds(node: self.switchCameraButton, frame: firstButtonFrame)
|
||||
|
||||
if !self.animatingButtonsSwap || transition.isAnimated {
|
||||
if self.call.hasVideo {
|
||||
transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame, completion: { [weak self] _ in
|
||||
if hasCameraButton {
|
||||
transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: firstButtonFrame, completion: { [weak self] _ in
|
||||
self?.animatingButtonsSwap = false
|
||||
})
|
||||
} else {
|
||||
transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: firstButtonFrame, completion: { [weak self] _ in
|
||||
transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame, completion: { [weak self] _ in
|
||||
self?.animatingButtonsSwap = false
|
||||
})
|
||||
}
|
||||
transition.updateFrameAsPositionAndBounds(node: self.audioButton, frame: secondButtonFrame)
|
||||
transition.updateFrameAsPositionAndBounds(node: self.cameraButton, frame: secondButtonFrame)
|
||||
}
|
||||
transition.updateFrameAsPositionAndBounds(node: self.leaveButton, frame: forthButtonFrame)
|
||||
}
|
||||
|
|
@ -4421,6 +4495,9 @@ public final class VoiceChatController: ViewController {
|
|||
return
|
||||
}
|
||||
}
|
||||
} else if self.effectiveSpeaker != nil, !fullscreenEntries.isEmpty {
|
||||
self.updateMainVideo(waitForFullSize: true, entries: fullscreenEntries, force: true)
|
||||
return
|
||||
}
|
||||
|
||||
self.updateRequestedVideoChannels()
|
||||
|
|
@ -4518,45 +4595,50 @@ public final class VoiceChatController: ViewController {
|
|||
if !self.requestedVideoSources.contains(channel.endpointId) {
|
||||
self.requestedVideoSources.insert(channel.endpointId)
|
||||
self.call.makeIncomingVideoView(endpointId: channel.endpointId, requestClone: true, completion: { [weak self] videoView, backdropVideoView in
|
||||
guard let strongSelf = self, let videoView = videoView else {
|
||||
return
|
||||
}
|
||||
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)
|
||||
strongSelf.readyVideoDisposables.set((videoNode.ready
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self, weak videoNode] _ in
|
||||
if let strongSelf = self, let videoNode = videoNode {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
strongSelf.readyVideoNodes.insert(channel.endpointId)
|
||||
if videoNode.aspectRatio <= 0.77 {
|
||||
strongSelf.wideVideoNodes.insert(channel.endpointId)
|
||||
}
|
||||
strongSelf.updateMembers()
|
||||
Queue.mainQueue().async {
|
||||
print("create main video \(channel.endpointId)")
|
||||
print("create blur video \(channel.endpointId)")
|
||||
guard let strongSelf = self, let videoView = videoView else {
|
||||
return
|
||||
}
|
||||
let videoNode = GroupVideoNode(videoView: videoView, backdropVideoView: backdropVideoView)
|
||||
strongSelf.readyVideoDisposables.set((videoNode.ready
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self, weak videoNode] _ in
|
||||
if let strongSelf = self, let videoNode = videoNode {
|
||||
print("video ready \(channel.endpointId)")
|
||||
Queue.mainQueue().after(0.1) {
|
||||
strongSelf.readyVideoNodes.insert(channel.endpointId)
|
||||
if videoNode.aspectRatio <= 0.77 {
|
||||
strongSelf.wideVideoNodes.insert(channel.endpointId)
|
||||
}
|
||||
strongSelf.updateMembers()
|
||||
|
||||
if let interaction = strongSelf.itemInteraction {
|
||||
loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count {
|
||||
let entry = strongSelf.currentFullscreenEntries[i]
|
||||
switch entry {
|
||||
case let .peer(peerEntry, _):
|
||||
if peerEntry.effectiveVideoEndpointId == channel.endpointId {
|
||||
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
|
||||
strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
|
||||
break loop
|
||||
if let interaction = strongSelf.itemInteraction {
|
||||
loop: for i in 0 ..< strongSelf.currentFullscreenEntries.count {
|
||||
let entry = strongSelf.currentFullscreenEntries[i]
|
||||
switch entry {
|
||||
case let .peer(peerEntry, _):
|
||||
if peerEntry.effectiveVideoEndpointId == channel.endpointId {
|
||||
let presentationData = strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme)
|
||||
strongSelf.fullscreenListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: i, previousIndex: i, item: entry.fullscreenItem(context: strongSelf.context, presentationData: presentationData, interaction: interaction), directionHint: nil)], options: [.Synchronous], updateOpaqueState: nil)
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}), forKey: channel.endpointId)
|
||||
strongSelf.videoNodes[channel.endpointId] = videoNode
|
||||
}), forKey: channel.endpointId)
|
||||
strongSelf.videoNodes[channel.endpointId] = videoNode
|
||||
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.updateMembers()
|
||||
if let _ = strongSelf.validLayout {
|
||||
strongSelf.updateMembers()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -4739,7 +4821,7 @@ public final class VoiceChatController: ViewController {
|
|||
}
|
||||
}
|
||||
case .fullscreen:
|
||||
if fabs(translation) > 32.0 {
|
||||
if abs(translation) > 32.0 {
|
||||
if self.fullscreenListNode.layer.animationKeys()?.contains("opacity") == true {
|
||||
self.fullscreenListNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
|
@ -5262,6 +5344,13 @@ public final class VoiceChatController: ViewController {
|
|||
}))
|
||||
}
|
||||
|
||||
private func displayUnmuteTooltip() {
|
||||
let location = self.actionButton.view.convert(self.actionButton.bounds, to: self.view).center
|
||||
let point = CGRect(origin: CGPoint(x: location.x - 5.0, y: location.y - 5.0 - 68.0), size: CGSize(width: 10.0, height: 10.0))
|
||||
self.controller?.present(TooltipScreen(text: self.presentationData.strings.VoiceChat_UnmuteSuggestion, style: .gradient(UIColor(rgb: 0x1d446c), UIColor(rgb: 0x193e63)), icon: nil, location: .point(point, .bottom), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .window(.root))
|
||||
}
|
||||
|
||||
private func displayToggleVideoSourceTooltip(screencast: Bool) {
|
||||
// guard let videoContainerNode = self.mainStageVideoContainerNode else {
|
||||
|
|
@ -5344,7 +5433,7 @@ public final class VoiceChatController: ViewController {
|
|||
self.mainStageContainerNode.isHidden = false
|
||||
self.mainStageContainerNode.isUserInteractionEnabled = isFullscreen
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.55, curve: .spring)
|
||||
|
||||
if case .modal = previousDisplayMode, case .fullscreen = self.displayMode {
|
||||
self.fullscreenListNode.isHidden = false
|
||||
|
|
@ -5407,7 +5496,7 @@ public final class VoiceChatController: ViewController {
|
|||
self.transitionMaskTopFillLayer.opacity = 1.0
|
||||
}
|
||||
self.transitionMaskBottomFillLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
Queue.mainQueue().after(0.2) {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self?.transitionMaskTopFillLayer.opacity = 0.0
|
||||
self?.transitionMaskBottomFillLayer.removeAllAnimations()
|
||||
}
|
||||
|
|
@ -5510,6 +5599,9 @@ public final class VoiceChatController: ViewController {
|
|||
})
|
||||
|
||||
self.transitionMaskTopFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15)
|
||||
if !transitionOffset.isZero {
|
||||
self.transitionMaskBottomFillLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: transition)
|
||||
|
|
|
|||
|
|
@ -773,7 +773,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
nodeToAnimateIn = animationNode
|
||||
}
|
||||
var color = color
|
||||
if color.rgb == 0x979797 {
|
||||
if hasVideo || color.rgb == 0x979797 {
|
||||
color = UIColor(rgb: 0xffffff)
|
||||
}
|
||||
animationNode.update(state: VoiceChatMicrophoneNode.State(muted: muted, filled: true, color: color), animated: true)
|
||||
|
|
@ -884,8 +884,13 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.videoContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -9.0), duration: 0.2, additive: true)
|
||||
strongSelf.audioLevelView?.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
transition.updateAlpha(node: videoNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0)
|
||||
if videoNodeUpdated {
|
||||
videoNode.alpha = 0.0
|
||||
strongSelf.videoFadeNode.alpha = 0.0
|
||||
} else {
|
||||
transition.updateAlpha(node: videoNode, alpha: 0.0)
|
||||
transition.updateAlpha(node: strongSelf.videoFadeNode, alpha: 0.0)
|
||||
}
|
||||
transition.updateAlpha(node: strongSelf.avatarNode, alpha: 1.0)
|
||||
if let audioLevelView = strongSelf.audioLevelView {
|
||||
transition.updateAlpha(layer: audioLevelView.layer, alpha: 1.0)
|
||||
|
|
|
|||
|
|
@ -334,7 +334,11 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
self.appeared = false
|
||||
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
|
||||
if offset.isZero {
|
||||
alphaTransition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
|
||||
} else {
|
||||
self.backgroundNode.alpha = 0.0
|
||||
}
|
||||
alphaTransition.updateAlpha(node: self.topFadeNode, alpha: 0.0)
|
||||
alphaTransition.updateAlpha(node: self.titleNode, alpha: 0.0)
|
||||
alphaTransition.updateAlpha(node: self.microphoneNode, alpha: 0.0)
|
||||
|
|
@ -430,7 +434,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
strongSelf.speakingContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
strongSelf.speakingContainerNode.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let blobFrame = strongSelf.speakingAvatarNode.frame.insetBy(dx: -10.0, dy: -10.0)
|
||||
let blobFrame = strongSelf.speakingAvatarNode.frame.insetBy(dx: -14.0, dy: -14.0)
|
||||
strongSelf.speakingAudioLevelDisposable.set((getAudioLevel(peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
|
|
@ -613,9 +617,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
var delayTransition = false
|
||||
if previousPeer?.0 == peer?.0 && self.appeared {
|
||||
delayTransition = true
|
||||
}
|
||||
let appeared = self.appeared
|
||||
|
||||
}
|
||||
if !delayTransition {
|
||||
self.setAvatarHidden(true)
|
||||
}
|
||||
|
|
@ -660,6 +662,11 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
videoNode.alpha = 0.0
|
||||
}
|
||||
if waitForFullSize {
|
||||
Queue.mainQueue().after(2.0) {
|
||||
if let previousVideoNode = previousVideoNode {
|
||||
previousVideoNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
strongSelf.videoReadyDisposable.set((videoNode.ready
|
||||
|> filter { $0 }
|
||||
|> take(1)
|
||||
|
|
@ -823,7 +830,7 @@ final class VoiceChatMainStageNode: ASDisplayNode {
|
|||
|
||||
let speakingInset: CGFloat = 16.0
|
||||
let speakingAvatarSize = CGSize(width: 30.0, height: 30.0)
|
||||
let speakingTitleSize = self.speakingTitleNode.updateLayout(CGSize(width: 220.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let speakingTitleSize = self.speakingTitleNode.updateLayout(CGSize(width: size.width - 100.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
let speakingContainerSize = CGSize(width: speakingTitleSize.width + speakingInset * 2.0 + speakingAvatarSize.width, height: 38.0)
|
||||
self.speakingEffectView?.frame = CGRect(origin: CGPoint(), size: speakingContainerSize)
|
||||
self.speakingAvatarNode.frame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: speakingAvatarSize)
|
||||
|
|
|
|||
|
|
@ -683,11 +683,11 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||
let startContainerAvatarPosition = sourceNode.avatarNode.view.convert(sourceNode.avatarNode.bounds, to: containerNode.view).center
|
||||
var animate = true
|
||||
if containerNode.frame.width > containerNode.frame.height {
|
||||
if startContainerAvatarPosition.y < -tileSize.height || startContainerAvatarPosition.y > containerNode.frame.height + tileSize.height {
|
||||
if startContainerAvatarPosition.y < -tileSize.height * 2.0 || startContainerAvatarPosition.y > containerNode.frame.height + tileSize.height * 2.0 {
|
||||
animate = false
|
||||
}
|
||||
} else {
|
||||
if startContainerAvatarPosition.x < -tileSize.width || startContainerAvatarPosition.x > containerNode.frame.width + tileSize.width {
|
||||
if startContainerAvatarPosition.x < -tileSize.width * 4.0 || startContainerAvatarPosition.x > containerNode.frame.width + tileSize.width * 4.0 {
|
||||
animate = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,7 +191,9 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
gesture.cancel()
|
||||
return
|
||||
}
|
||||
contextAction(strongSelf.contextSourceNode, gesture)
|
||||
if item.videoReady {
|
||||
contextAction(strongSelf.contextSourceNode, gesture)
|
||||
}
|
||||
}
|
||||
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||
guard let strongSelf = self, let _ = strongSelf.item else {
|
||||
|
|
@ -275,6 +277,8 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
let previousItem = self.item
|
||||
self.item = item
|
||||
|
||||
self.containerNode.isGestureEnabled = item.videoReady
|
||||
|
||||
if !item.videoReady {
|
||||
let shimmerNode: VoiceChatTileShimmeringNode
|
||||
if let current = self.shimmerNode {
|
||||
|
|
@ -451,7 +455,7 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||
|
||||
if animate {
|
||||
sourceNode.isHidden = true
|
||||
Queue.mainQueue().after(0.6) {
|
||||
Queue.mainQueue().after(0.7) {
|
||||
sourceNode.isHidden = false
|
||||
}
|
||||
|
||||
|
|
@ -642,7 +646,7 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
|
|||
}
|
||||
}
|
||||
|
||||
private final class ShimmerEffectForegroundNode: ASDisplayNode {
|
||||
final class ShimmerEffectForegroundNode: ASDisplayNode {
|
||||
private var currentForegroundColor: UIColor?
|
||||
private let imageNodeContainer: ASDisplayNode
|
||||
private let imageNode: ASImageNode
|
||||
|
|
|
|||
|
|
@ -156,6 +156,10 @@ public func getCurrentGroupCall(account: Account, callId: Int64, accessHash: Int
|
|||
presentationJsonDescription = data
|
||||
}
|
||||
}
|
||||
if muteState?.canUnmute == false {
|
||||
videoJsonDescription = nil
|
||||
presentationJsonDescription = nil
|
||||
}
|
||||
parsedParticipants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: peer,
|
||||
ssrc: ssrc,
|
||||
|
|
@ -481,6 +485,10 @@ public func getGroupCallParticipants(account: Account, callId: Int64, accessHash
|
|||
presentationJsonDescription = data
|
||||
}
|
||||
}
|
||||
if muteState?.canUnmute == false {
|
||||
videoJsonDescription = nil
|
||||
presentationJsonDescription = nil
|
||||
}
|
||||
parsedParticipants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: peer,
|
||||
ssrc: ssrc,
|
||||
|
|
@ -741,6 +749,10 @@ public func joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, cal
|
|||
presentationJsonDescription = data
|
||||
}
|
||||
}
|
||||
if muteState?.canUnmute == false {
|
||||
videoJsonDescription = nil
|
||||
presentationJsonDescription = nil
|
||||
}
|
||||
if !state.participants.contains(where: { $0.peer.id == peer.id }) {
|
||||
state.participants.append(GroupCallParticipantsContext.Participant(
|
||||
peer: peer,
|
||||
|
|
@ -2150,7 +2162,10 @@ extension GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate {
|
|||
presentationJsonDescription = data
|
||||
}
|
||||
}
|
||||
|
||||
if muteState?.canUnmute == false {
|
||||
videoJsonDescription = nil
|
||||
presentationJsonDescription = nil
|
||||
}
|
||||
self.init(
|
||||
peerId: peerId,
|
||||
ssrc: ssrc,
|
||||
|
|
@ -2213,7 +2228,10 @@ extension GroupCallParticipantsContext.Update.StateUpdate {
|
|||
presentationJsonDescription = data
|
||||
}
|
||||
}
|
||||
|
||||
if muteState?.canUnmute == false {
|
||||
videoJsonDescription = nil
|
||||
presentationJsonDescription = nil
|
||||
}
|
||||
participantUpdates.append(GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(
|
||||
peerId: peerId,
|
||||
ssrc: ssrc,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -1299,22 +1299,28 @@ private:
|
|||
completion(remoteRenderer, nil);
|
||||
} else if ([VideoMetalView isSupported]) {
|
||||
VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero];
|
||||
#if TARGET_OS_IPHONE
|
||||
remoteRenderer.videoContentMode = UIViewContentModeScaleToFill;
|
||||
#else
|
||||
remoteRenderer.videoContentMode = UIViewContentModeScaleAspect;
|
||||
#endif
|
||||
|
||||
VideoMetalView *cloneRenderer = nil;
|
||||
if (requestClone) {
|
||||
cloneRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero];
|
||||
cloneRenderer.videoContentMode = UIViewContentModeScaleToFill;
|
||||
}
|
||||
|
||||
std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> sink = [remoteRenderer getSink];
|
||||
std::shared_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> cloneSink = [cloneRenderer getSink];
|
||||
|
||||
[queue dispatch:^{
|
||||
__strong GroupCallThreadLocalContext *strongSelf = weakSelf;
|
||||
if (strongSelf && strongSelf->_instance) {
|
||||
strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, sink);
|
||||
if (cloneSink) {
|
||||
strongSelf->_instance->addIncomingVideoOutput(endpointId.UTF8String, cloneSink);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
completion(remoteRenderer, nil);
|
||||
completion(remoteRenderer, cloneRenderer);
|
||||
} else {
|
||||
GLVideoView *remoteRenderer = [[GLVideoView alloc] initWithFrame:CGRectZero];
|
||||
// [remoteRenderer setVideoContentMode:kCAGravityResizeAspectFill];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue