mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat UI improvements
This commit is contained in:
parent
811f6981ca
commit
7d3353c01b
@ -5917,8 +5917,6 @@ Sorry for the inconvenience.";
|
||||
"Media.LimitedAccessText" = "You've given Telegram access only to select number of photos.";
|
||||
"Media.LimitedAccessManage" = "Manage";
|
||||
|
||||
"VoiceChat.BackTitle" = "Chat";
|
||||
|
||||
"VoiceChat.StatusSpeaking" = "speaking";
|
||||
"VoiceChat.StatusListening" = "listening";
|
||||
|
||||
@ -5944,7 +5942,6 @@ Sorry for the inconvenience.";
|
||||
|
||||
"VoiceChat.UnmutePeer" = "Allow to Speak";
|
||||
"VoiceChat.MutePeer" = "Mute";
|
||||
"VoiceChat.InvitePeer" = "Invite";
|
||||
"VoiceChat.RemovePeer" = "Remove";
|
||||
"VoiceChat.RemovePeerConfirmation" = "Are you sure you want to remove %@ from the group chat?";
|
||||
"VoiceChat.RemovePeerRemove" = "Remove";
|
||||
@ -5985,3 +5982,6 @@ Sorry for the inconvenience.";
|
||||
"CHAT_VOICECHAT_START" = "%1$@ has started voice chat in the group %2$@";
|
||||
"CHAT_VOICECHAT_INVITE" = "%1$@ has invited %3$@ in the group %2$@";
|
||||
"CHAT_VOICECHAT_INVITE_YOU" = "%1$@ has invited you to voice chat in the group %1$@";
|
||||
|
||||
"Call.VoiceChatInProgressTitle" = "Voice Chat in Progress";
|
||||
"Call.VoiceChatInProgressMessageCall" = "Leave voice chat in %1$@ and start a call with %2$@?";
|
||||
|
@ -125,8 +125,7 @@ private final class Curve {
|
||||
self.minOffset = minOffset
|
||||
self.maxOffset = maxOffset
|
||||
|
||||
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||
self.smoothness = 0.35
|
||||
|
||||
self.currentOffset = minOffset
|
||||
|
||||
@ -169,7 +168,13 @@ private final class Curve {
|
||||
let timestamp = CACurrentMediaTime()
|
||||
|
||||
if let (startTime, duration) = self.transitionArguments, duration > 0.0 {
|
||||
self.transition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
|
||||
var t = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
|
||||
if t < 0.5 {
|
||||
t = 2 * t * t
|
||||
} else {
|
||||
t = -1 + (4 - 2 * t) * t
|
||||
}
|
||||
self.transition = t
|
||||
if self.transition < 1.0 {
|
||||
} else {
|
||||
if self.loop {
|
||||
@ -187,12 +192,12 @@ private final class Curve {
|
||||
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
|
||||
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
||||
return curve(pointsCount: pointsCount, randomness: randomness).map {
|
||||
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 17.0 + $0.y * 12.0)
|
||||
return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 18.0 + $0.y * 12.0)
|
||||
}
|
||||
}
|
||||
|
||||
private func curve(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
||||
let segment = 1.0 / CGFloat(pointsCount)
|
||||
let segment = 1.0 / CGFloat(pointsCount - 1)
|
||||
|
||||
let rgen = { () -> CGFloat in
|
||||
let accuracy: UInt32 = 1000
|
||||
@ -218,9 +223,10 @@ private final class Curve {
|
||||
randomXDelta = 0.0
|
||||
} else {
|
||||
pointX = segment * CGFloat(i)
|
||||
pointY = cos(segment * CGFloat(i)) * ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset
|
||||
pointY = ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset
|
||||
randomXDelta = segment - segment * randPointOffset
|
||||
}
|
||||
|
||||
return CGPoint(x: pointX + randomXDelta, y: pointY)
|
||||
}
|
||||
|
||||
@ -264,9 +270,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||
let bigCurveRange: CurveRange = (0.1, 1.0)
|
||||
|
||||
let size = CGSize(width: 375.0, height: 44.0)
|
||||
let smallCurve = Curve(size: size, alpha: 1.0, pointsCount: 6, minRandomness: 1, maxRandomness: 1, minSpeed: 2.5, maxSpeed: 7, minOffset: smallCurveRange.min, maxOffset: smallCurveRange.max)
|
||||
let mediumCurve = Curve(size: size, alpha: 0.55, pointsCount: 6, minRandomness: 1, maxRandomness: 2, minSpeed: 2.5, maxSpeed: 7, minOffset: mediumCurveRange.min, maxOffset: mediumCurveRange.max)
|
||||
let largeCurve = Curve(size: size, alpha: 0.35, pointsCount: 6, minRandomness: 1, maxRandomness: 2, minSpeed: 2.5, maxSpeed: 7, minOffset: bigCurveRange.min, maxOffset: bigCurveRange.max)
|
||||
let smallCurve = Curve(size: size, alpha: 1.0, pointsCount: 7, minRandomness: 1, maxRandomness: 1.3, minSpeed: 1.0, maxSpeed: 3.5, minOffset: smallCurveRange.min, maxOffset: smallCurveRange.max)
|
||||
let mediumCurve = Curve(size: size, alpha: 0.55, pointsCount: 7, minRandomness: 1.2, maxRandomness: 1.5, minSpeed: 1.0, maxSpeed: 4.5, minOffset: mediumCurveRange.min, maxOffset: mediumCurveRange.max)
|
||||
let largeCurve = Curve(size: size, alpha: 0.35, pointsCount: 7, minRandomness: 1.2, maxRandomness: 1.7, minSpeed: 1.0, maxSpeed: 6.0, minOffset: bigCurveRange.min, maxOffset: bigCurveRange.max)
|
||||
|
||||
self.curves = [smallCurve, mediumCurve, largeCurve]
|
||||
|
||||
@ -314,7 +320,6 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.updateAnimations()
|
||||
})
|
||||
animator.frameInterval = 2
|
||||
self.animator = animator
|
||||
}
|
||||
animator.isPaused = false
|
||||
@ -526,12 +531,14 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
if let currentPeer = self.currentPeer {
|
||||
title = currentPeer.displayTitle(strings: strings, displayOrder: self.nameDisplayOrder)
|
||||
}
|
||||
var membersCount: Int32?
|
||||
if let groupCallState = self.currentGroupCallState {
|
||||
if groupCallState.numberOfActiveSpeakers != 0 {
|
||||
subtitle = strings.VoiceChat_Panel_MembersSpeaking(Int32(groupCallState.numberOfActiveSpeakers))
|
||||
} else {
|
||||
subtitle = strings.VoiceChat_Panel_Members(Int32(max(1, groupCallState.participantCount)))
|
||||
membersCount = Int32(max(1, groupCallState.participantCount))
|
||||
} else if let content = self.currentContent, case .groupCall = content {
|
||||
membersCount = 1
|
||||
}
|
||||
if let membersCount = membersCount {
|
||||
subtitle = strings.VoiceChat_Panel_Members(membersCount)
|
||||
}
|
||||
}
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white)
|
||||
@ -556,6 +563,6 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
self.subtitleNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin + animationSize + iconSpacing + titleSize.width + spacing, y: verticalOrigin + floor((contentHeight - subtitleSize.height) / 2.0)), size: subtitleSize)
|
||||
|
||||
self.backgroundNode.speaking = !self.currentIsMuted
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 17.0))
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height + 18.0))
|
||||
}
|
||||
}
|
||||
|
@ -282,17 +282,12 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
|
||||
let membersText: String
|
||||
let membersTextIsActive: Bool
|
||||
if summaryState.numberOfActiveSpeakers != 0 {
|
||||
membersText = strongSelf.strings.VoiceChat_Panel_MembersSpeaking(Int32(summaryState.numberOfActiveSpeakers))
|
||||
membersTextIsActive = true
|
||||
} else {
|
||||
if summaryState.participantCount == 0 {
|
||||
membersText = strongSelf.strings.VoiceChat_Panel_TapToJoin
|
||||
} else {
|
||||
membersText = strongSelf.strings.VoiceChat_Panel_Members(Int32(summaryState.participantCount))
|
||||
}
|
||||
membersTextIsActive = false
|
||||
}
|
||||
|
||||
if strongSelf.textIsActive != membersTextIsActive {
|
||||
strongSelf.textIsActive = membersTextIsActive
|
||||
@ -373,17 +368,12 @@ public final class GroupCallNavigationAccessoryPanel: ASDisplayNode {
|
||||
|
||||
let membersText: String
|
||||
let membersTextIsActive: Bool
|
||||
if data.numberOfActiveSpeakers != 0 {
|
||||
membersText = self.strings.VoiceChat_Panel_MembersSpeaking(Int32(data.numberOfActiveSpeakers))
|
||||
membersTextIsActive = true
|
||||
} else {
|
||||
if data.participantCount == 0 {
|
||||
membersText = self.strings.VoiceChat_Panel_TapToJoin
|
||||
} else {
|
||||
membersText = self.strings.VoiceChat_Panel_Members(Int32(data.participantCount))
|
||||
}
|
||||
membersTextIsActive = false
|
||||
}
|
||||
|
||||
if self.textIsActive != membersTextIsActive {
|
||||
self.textIsActive = membersTextIsActive
|
||||
|
@ -709,7 +709,6 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|
||||
peerId: peerId,
|
||||
peer: nil
|
||||
)
|
||||
call.sourcePanel = sourcePanel
|
||||
strongSelf.updateCurrentGroupCall(call)
|
||||
strongSelf.currentGroupCallPromise.set(.single(call))
|
||||
strongSelf.hasActiveGroupCallsPromise.set(true)
|
||||
|
File diff suppressed because it is too large
Load Diff
260
submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift
Normal file
260
submodules/TelegramCallsUI/Sources/VoiceChatActionItem.swift
Normal file
@ -0,0 +1,260 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
|
||||
public enum VoiceChatActionItemIcon : Equatable {
|
||||
case none
|
||||
case generic(UIImage)
|
||||
|
||||
public var image: UIImage? {
|
||||
switch self {
|
||||
case .none:
|
||||
return nil
|
||||
case let .generic(image):
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: VoiceChatActionItemIcon, rhs: VoiceChatActionItemIcon) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .generic(image):
|
||||
if case .generic(image) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceChatActionItem: ListViewItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let title: String
|
||||
let icon: VoiceChatActionItemIcon
|
||||
let action: () -> Void
|
||||
|
||||
init(presentationData: ItemListPresentationData, title: String, icon: VoiceChatActionItemIcon, action: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? VoiceChatActionItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, false, nextItem == nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectable: Bool = true
|
||||
func selected(listView: ListView){
|
||||
self.action()
|
||||
listView.clearHighlightAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceChatActionItemNode: ListViewItemNode {
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
|
||||
private var item: VoiceChatActionItem?
|
||||
|
||||
init() {
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.contentMode = .left
|
||||
self.titleNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.activateArea)
|
||||
|
||||
self.activateArea.activate = { [weak self] in
|
||||
self?.item?.action()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: VoiceChatActionItem, _ params: ListViewItemLayoutParams, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, firstWithHeader, last in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
updatedTheme = item.presentationData.theme
|
||||
}
|
||||
|
||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
|
||||
var leftInset: CGFloat = 16.0 + params.leftInset
|
||||
if case .generic = item.icon {
|
||||
leftInset += 49.0
|
||||
}
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(rgb: 0xffffff)), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
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 separatorHeight = UIScreenPixel
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.activateArea.accessibilityLabel = item.title
|
||||
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: layout.contentSize.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
|
||||
strongSelf.iconNode.image = generateTintedImage(image: item.icon.image, color: UIColor(rgb: 0xffffff))
|
||||
}
|
||||
|
||||
let _ = titleApply()
|
||||
|
||||
let titleOffset = leftInset
|
||||
let hideBottomStripe: Bool = last
|
||||
if let image = item.icon.image {
|
||||
let iconFrame = CGRect(origin: CGPoint(x: params.leftInset + floor((leftInset - params.leftInset - image.size.width) / 2.0) + 3.0, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
|
||||
strongSelf.iconNode.frame = iconFrame
|
||||
}
|
||||
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 0)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
|
||||
}
|
||||
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
strongSelf.bottomStripeNode.isHidden = hideBottomStripe
|
||||
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
|
||||
|
||||
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floor((contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
var anchorNode: ASDisplayNode?
|
||||
if self.bottomStripeNode.supernode != nil {
|
||||
anchorNode = self.bottomStripeNode
|
||||
} else if self.topStripeNode.supernode != nil {
|
||||
anchorNode = self.topStripeNode
|
||||
}
|
||||
if let anchorNode = anchorNode {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
|
||||
} else {
|
||||
self.addSubnode(self.highlightedBackgroundNode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
if animated {
|
||||
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
|
||||
if let strongSelf = self {
|
||||
if completed {
|
||||
strongSelf.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
} else {
|
||||
self.highlightedBackgroundNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override public func header() -> ListViewItemHeader? {
|
||||
return nil
|
||||
}
|
||||
}
|
@ -108,7 +108,8 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
private final class Interaction {
|
||||
let updateIsMuted: (PeerId, Bool) -> Void
|
||||
let invitePeer: (Peer) -> Void
|
||||
let openPeer: (PeerId) -> Void
|
||||
let openInvite: () -> Void
|
||||
let peerContextAction: (PeerEntry, ASDisplayNode, ContextGesture?) -> Void
|
||||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
|
||||
@ -116,12 +117,14 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
init(
|
||||
updateIsMuted: @escaping (PeerId, Bool) -> Void,
|
||||
invitePeer: @escaping (Peer) -> Void,
|
||||
openPeer: @escaping (PeerId) -> Void,
|
||||
openInvite: @escaping () -> Void,
|
||||
peerContextAction: @escaping (PeerEntry, ASDisplayNode, ContextGesture?) -> Void,
|
||||
setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void
|
||||
) {
|
||||
self.updateIsMuted = updateIsMuted
|
||||
self.invitePeer = invitePeer
|
||||
self.openPeer = openPeer
|
||||
self.openInvite = openInvite
|
||||
self.peerContextAction = peerContextAction
|
||||
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
||||
}
|
||||
@ -141,17 +144,21 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func updateAudioLevels(_ levels: [(PeerId, Float)], ignore: Set<PeerId> = Set()) {
|
||||
func updateAudioLevels(_ levels: [(PeerId, Float)], accountPeerId: PeerId, updateAll: Bool) {
|
||||
var updated = Set<PeerId>()
|
||||
for (peerId, level) in levels {
|
||||
if let pipe = self.audioLevels[peerId] {
|
||||
pipe.putNext(max(0.001, level))
|
||||
var level = level
|
||||
if peerId != accountPeerId {
|
||||
level = max(0.001, level)
|
||||
}
|
||||
pipe.putNext(level)
|
||||
updated.insert(peerId)
|
||||
}
|
||||
}
|
||||
if !ignore.isEmpty {
|
||||
if updateAll {
|
||||
for (peerId, pipe) in self.audioLevels {
|
||||
if !updated.contains(peerId) && !ignore.contains(peerId) {
|
||||
if !updated.contains(peerId) && peerId != accountPeerId {
|
||||
pipe.putNext(0.0)
|
||||
}
|
||||
}
|
||||
@ -161,7 +168,6 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
private struct PeerEntry: Comparable, Identifiable {
|
||||
enum State {
|
||||
case inactive
|
||||
case listening
|
||||
case speaking
|
||||
}
|
||||
@ -171,7 +177,6 @@ public final class VoiceChatController: ViewController {
|
||||
var activityTimestamp: Int32
|
||||
var state: State
|
||||
var muteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
var invited: Bool
|
||||
var revealed: Bool?
|
||||
|
||||
var stableId: PeerId {
|
||||
@ -194,9 +199,6 @@ public final class VoiceChatController: ViewController {
|
||||
if lhs.muteState != rhs.muteState {
|
||||
return false
|
||||
}
|
||||
if lhs.invited != rhs.invited {
|
||||
return false
|
||||
}
|
||||
if lhs.revealed != rhs.revealed {
|
||||
return false
|
||||
}
|
||||
@ -209,25 +211,102 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
return lhs.peer.id < rhs.peer.id
|
||||
}
|
||||
}
|
||||
|
||||
private enum EntryId: Hashable {
|
||||
case invite
|
||||
case peerId(PeerId)
|
||||
|
||||
static func <(lhs: EntryId, rhs: EntryId) -> Bool {
|
||||
return lhs.hashValue < rhs.hashValue
|
||||
}
|
||||
|
||||
static func ==(lhs: EntryId, rhs: EntryId) -> Bool {
|
||||
switch lhs {
|
||||
case .invite:
|
||||
switch rhs {
|
||||
case .invite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .peerId(lhsId):
|
||||
switch rhs {
|
||||
case let .peerId(rhsId):
|
||||
return lhsId == rhsId
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ListEntry: Comparable, Identifiable {
|
||||
case invite(PresentationTheme, PresentationStrings, String)
|
||||
case peer(PeerEntry)
|
||||
|
||||
var stableId: EntryId {
|
||||
switch self {
|
||||
case .invite:
|
||||
return .invite
|
||||
case let .peer(peerEntry):
|
||||
return .peerId(peerEntry.peer.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ListEntry, rhs: ListEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .invite(lhsTheme, lhsStrings, lhsText):
|
||||
if case let .invite(rhsTheme, rhsStrings, rhsText) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsText == rhsText {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peer(lhsPeerEntry):
|
||||
switch rhs {
|
||||
case let .peer(rhsPeerEntry):
|
||||
return lhsPeerEntry == rhsPeerEntry
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: ListEntry, rhs: ListEntry) -> Bool {
|
||||
switch lhs {
|
||||
case .invite:
|
||||
return true
|
||||
case let .peer(lhsPeerEntry):
|
||||
switch rhs {
|
||||
case .invite:
|
||||
return false
|
||||
case let .peer(rhsPeerEntry):
|
||||
return lhsPeerEntry < rhsPeerEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListViewItem {
|
||||
let peer = self.peer
|
||||
switch self {
|
||||
case let .invite(_, _, text):
|
||||
return VoiceChatActionItem(presentationData: ItemListPresentationData(presentationData), title: text, icon: .generic(UIImage(bundleImageName: "Chat/Context Menu/AddUser")!), action: {
|
||||
|
||||
})
|
||||
case let .peer(peerEntry):
|
||||
let peer = peerEntry.peer
|
||||
|
||||
let text: VoiceChatParticipantItem.ParticipantText
|
||||
let icon: VoiceChatParticipantItem.Icon
|
||||
switch self.state {
|
||||
case .inactive:
|
||||
text = .presence
|
||||
icon = .invite(self.invited)
|
||||
switch peerEntry.state {
|
||||
case .listening:
|
||||
text = .text(presentationData.strings.VoiceChat_StatusListening, .accent)
|
||||
let microphoneColor: UIColor
|
||||
if let muteState = self.muteState, !muteState.canUnmute {
|
||||
if let muteState = peerEntry.muteState, !muteState.canUnmute {
|
||||
microphoneColor = UIColor(rgb: 0xff3b30)
|
||||
} else {
|
||||
microphoneColor = UIColor(rgb: 0x979797)
|
||||
}
|
||||
icon = .microphone(self.muteState != nil, microphoneColor)
|
||||
icon = .microphone(peerEntry.muteState != nil, microphoneColor)
|
||||
case .speaking:
|
||||
text = .text(presentationData.strings.VoiceChat_StatusSpeaking, .constructive)
|
||||
icon = .microphone(false, UIColor(rgb: 0x34c759))
|
||||
@ -235,17 +314,18 @@ public final class VoiceChatController: ViewController {
|
||||
|
||||
let revealOptions: [VoiceChatParticipantItem.RevealOption] = []
|
||||
|
||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: self.presence, text: text, icon: icon, enabled: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, revealOptions: revealOptions, revealed: self.revealed, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
return VoiceChatParticipantItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: peer, presence: peerEntry.presence, text: text, icon: icon, enabled: true, getAudioLevel: { return interaction.getAudioLevel(peer.id) }, revealOptions: revealOptions, revealed: peerEntry.revealed, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
interaction.setPeerIdWithRevealedOptions(peerId, fromPeerId)
|
||||
}, action: {
|
||||
interaction.invitePeer(peer)
|
||||
interaction.openPeer(peer.id)
|
||||
}, contextAction: { node, gesture in
|
||||
interaction.peerContextAction(self, node, gesture)
|
||||
interaction.peerContextAction(peerEntry, node, gesture)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preparedTransition(from fromEntries: [PeerEntry], to toEntries: [PeerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition {
|
||||
private func preparedTransition(from fromEntries: [ListEntry], to toEntries: [ListEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, context: AccountContext, presentationData: PresentationData, interaction: Interaction) -> ListTransition {
|
||||
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
||||
|
||||
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
||||
@ -263,12 +343,18 @@ public final class VoiceChatController: ViewController {
|
||||
private var darkTheme: PresentationTheme
|
||||
|
||||
private let optionsButton: VoiceChatOptionsButton
|
||||
private let closeButton: HighlightableButtonNode
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let contentContainer: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let listNode: ListView
|
||||
private let audioOutputNode: CallControllerButtonItemNode
|
||||
private let leaveNode: CallControllerButtonItemNode
|
||||
private let actionButton: VoiceChatActionButton
|
||||
|
||||
private let titleView: VoiceChatControllerTitleView
|
||||
|
||||
private var enqueuedTransitions: [ListTransition] = []
|
||||
private var maxListHeight: CGFloat?
|
||||
|
||||
@ -279,10 +365,9 @@ public final class VoiceChatController: ViewController {
|
||||
private var currentGroupMembers: [RenderedChannelParticipant]?
|
||||
private var currentCallMembers: [GroupCallParticipantsContext.Participant]?
|
||||
private var currentSpeakingPeers: Set<PeerId>?
|
||||
private var currentInvitedPeers: Set<PeerId>?
|
||||
private var accountPeer: Peer?
|
||||
|
||||
private var currentEntries: [PeerEntry] = []
|
||||
private var peersDisposable: Disposable?
|
||||
private var currentEntries: [ListEntry] = []
|
||||
|
||||
private var peerViewDisposable: Disposable?
|
||||
private let leaveDisposable = MetaDisposable()
|
||||
@ -309,7 +394,6 @@ public final class VoiceChatController: ViewController {
|
||||
private var audioLevelsDisposable: Disposable?
|
||||
private var myAudioLevelDisposable: Disposable?
|
||||
private var memberStatesDisposable: Disposable?
|
||||
private var invitedPeersDisposable: Disposable?
|
||||
|
||||
private var itemInteraction: Interaction?
|
||||
|
||||
@ -323,20 +407,30 @@ public final class VoiceChatController: ViewController {
|
||||
self.darkTheme = defaultDarkColorPresentationTheme
|
||||
|
||||
self.optionsButton = VoiceChatOptionsButton()
|
||||
self.closeButton = HighlightableButtonNode()
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.contentContainer = ASDisplayNode()
|
||||
self.contentContainer.backgroundColor = .black
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = UIColor(rgb: 0x000000)
|
||||
self.backgroundNode.cornerRadius = 12.0
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.backgroundColor = self.darkTheme.list.itemBlocksBackgroundColor
|
||||
self.listNode.verticalScrollIndicatorColor = UIColor(white: 1.0, alpha: 0.3)
|
||||
self.listNode.clipsToBounds = true
|
||||
self.listNode.cornerRadius = 16.0
|
||||
self.listNode.cornerRadius = 12.0
|
||||
|
||||
self.audioOutputNode = CallControllerButtonItemNode()
|
||||
self.leaveNode = CallControllerButtonItemNode()
|
||||
self.actionButton = VoiceChatActionButton()
|
||||
|
||||
self.titleView = VoiceChatControllerTitleView(theme: self.presentationData.theme)
|
||||
self.titleView.set(title: self.presentationData.strings.VoiceChat_Title, subtitle: self.presentationData.strings.SocksProxySetup_ProxyStatusConnecting)
|
||||
|
||||
super.init()
|
||||
|
||||
let statePromise = ValuePromise(State(), ignoreRepeated: true)
|
||||
@ -345,38 +439,15 @@ public final class VoiceChatController: ViewController {
|
||||
statePromise.set(stateValue.modify { f($0) })
|
||||
}
|
||||
|
||||
let invitePeer: (Peer) -> Void = { [weak self] peer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if let invitedPeers = strongSelf.currentInvitedPeers, invitedPeers.contains(peer.id) {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.controller?.present(
|
||||
UndoOverlayController(
|
||||
presentationData: strongSelf.presentationData,
|
||||
content: .invitedToVoiceChat(
|
||||
context: strongSelf.context,
|
||||
peer: peer,
|
||||
text: strongSelf.presentationData.strings.VoiceChat_UserInvited(peer.compactDisplayTitle).0
|
||||
),
|
||||
elevatedLayout: false,
|
||||
action: { action in
|
||||
return true
|
||||
}
|
||||
),
|
||||
in: .current
|
||||
)
|
||||
strongSelf.call.invitePeer(peer.id)
|
||||
}
|
||||
|
||||
self.itemInteraction = Interaction(
|
||||
updateIsMuted: { [weak self] peerId, isMuted in
|
||||
self?.call.updateMuteState(peerId: peerId, isMuted: isMuted)
|
||||
}, invitePeer: { peer in
|
||||
invitePeer(peer)
|
||||
}, openPeer: { [weak self] peerId in
|
||||
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), keepStack: .always, purposefulAction: {}, peekData: nil))
|
||||
}
|
||||
}, openInvite: {
|
||||
|
||||
}, peerContextAction: { [weak self] entry, sourceNode, gesture in
|
||||
guard let strongSelf = self, let controller = strongSelf.controller, let sourceNode = sourceNode as? ContextExtractedContentContainingNode else {
|
||||
return
|
||||
@ -385,18 +456,7 @@ public final class VoiceChatController: ViewController {
|
||||
let peer = entry.peer
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
switch entry.state {
|
||||
case .inactive:
|
||||
if let invitedPeers = strongSelf.currentInvitedPeers, invitedPeers.contains(peer.id) {
|
||||
} else {
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.VoiceChat_InvitePeer, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.actionSheet.primaryTextColor)
|
||||
}, action: { _, f in
|
||||
invitePeer(peer)
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
default:
|
||||
|
||||
if peer.id != strongSelf.context.account.peerId {
|
||||
if let callState = strongSelf.callState, (callState.canManageCall || callState.adminIds.contains(strongSelf.context.account.peerId)), !callState.adminIds.contains(peer.id) {
|
||||
if let muteState = entry.muteState, !muteState.canUnmute {
|
||||
@ -457,7 +517,6 @@ public final class VoiceChatController: ViewController {
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
@ -473,21 +532,17 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
})
|
||||
|
||||
self.contentContainer.addSubnode(self.listNode)
|
||||
self.contentContainer.addSubnode(self.audioOutputNode)
|
||||
self.contentContainer.addSubnode(self.leaveNode)
|
||||
self.contentContainer.addSubnode(self.actionButton)
|
||||
self.backgroundNode.addSubnode(self.listNode)
|
||||
self.backgroundNode.addSubnode(self.audioOutputNode)
|
||||
self.backgroundNode.addSubnode(self.leaveNode)
|
||||
self.backgroundNode.addSubnode(self.actionButton)
|
||||
self.backgroundNode.view.addSubview(self.titleView)
|
||||
self.backgroundNode.addSubnode(self.optionsButton)
|
||||
self.backgroundNode.addSubnode(self.closeButton)
|
||||
|
||||
self.addSubnode(self.dimNode)
|
||||
self.addSubnode(self.contentContainer)
|
||||
|
||||
let (disposable, loadMoreControl) = self.context.peerChannelMemberCategoriesContextsManager.recent(postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: self.context.account.peerId, peerId: self.call.peerId, updated: { [weak self] state in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: state.list, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: strongSelf.currentInvitedPeers ?? Set())
|
||||
}
|
||||
})
|
||||
self.contentContainer.addSubnode(self.backgroundNode)
|
||||
|
||||
self.memberStatesDisposable = (self.call.members
|
||||
|> deliverOnMainQueue).start(next: { [weak self] callMembers in
|
||||
@ -495,27 +550,13 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
if let groupMembers = strongSelf.currentGroupMembers {
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: callMembers.participants, speakingPeers: callMembers.speakingParticipants, invitedPeers: strongSelf.currentInvitedPeers ?? Set())
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: callMembers.participants, speakingPeers: callMembers.speakingParticipants)
|
||||
} else {
|
||||
strongSelf.currentCallMembers = callMembers.participants
|
||||
}
|
||||
|
||||
let subtitle = strongSelf.presentationData.strings.VoiceChat_Panel_Members(Int32(max(1, callMembers.totalCount)))
|
||||
if let titleView = strongSelf.controller?.navigationItem.titleView as? VoiceChatControllerTitleView {
|
||||
titleView.set(title: strongSelf.presentationData.strings.VoiceChat_Title, subtitle: subtitle)
|
||||
}
|
||||
})
|
||||
|
||||
self.invitedPeersDisposable = (self.call.invitedPeers
|
||||
|> deliverOnMainQueue).start(next: { [weak self] invitedPeers in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let groupMembers = strongSelf.currentGroupMembers {
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: invitedPeers)
|
||||
} else {
|
||||
strongSelf.currentInvitedPeers = invitedPeers
|
||||
}
|
||||
strongSelf.titleView.set(title: strongSelf.presentationData.strings.VoiceChat_Title, subtitle: subtitle)
|
||||
})
|
||||
|
||||
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
|
||||
@ -523,19 +564,19 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
if case let .known(value) = offset, value < 40.0 {
|
||||
strongSelf.context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: strongSelf.call.peerId, control: loadMoreControl)
|
||||
}
|
||||
}
|
||||
|
||||
self.peersDisposable = disposable
|
||||
|
||||
self.peerViewDisposable = (self.context.account.viewTracker.peerView(self.call.peerId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view in
|
||||
self.peerViewDisposable = (combineLatest(self.context.account.viewTracker.peerView(self.call.peerId), self.context.account.postbox.loadedPeerWithId(self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] view, accountPeer in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if !strongSelf.didSetDataReady {
|
||||
strongSelf.accountPeer = accountPeer
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: [], callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
|
||||
|
||||
if let peer = peerViewMainPeer(view), let channel = peer as? TelegramChannel {
|
||||
let addressName = channel.addressName ?? ""
|
||||
if !addressName.isEmpty || (channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers)) {
|
||||
@ -572,7 +613,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
if wasMuted != (state.muteState != nil), let groupMembers = strongSelf.currentGroupMembers {
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set(), invitedPeers: strongSelf.currentInvitedPeers ?? Set())
|
||||
strongSelf.updateMembers(muteState: strongSelf.effectiveMuteState, groupMembers: groupMembers, callMembers: strongSelf.currentCallMembers ?? [], speakingPeers: strongSelf.currentSpeakingPeers ?? Set())
|
||||
}
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
@ -597,7 +638,7 @@ public final class VoiceChatController: ViewController {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.itemInteraction?.updateAudioLevels(levels, ignore: Set([strongSelf.context.account.peerId]))
|
||||
strongSelf.itemInteraction?.updateAudioLevels(levels, accountPeerId: strongSelf.context.account.peerId, updateAll: true)
|
||||
})
|
||||
|
||||
self.myAudioLevelDisposable = (call.myAudioLevel
|
||||
@ -606,10 +647,10 @@ public final class VoiceChatController: ViewController {
|
||||
return
|
||||
}
|
||||
var effectiveLevel: Float = 0.0
|
||||
if let state = strongSelf.callState, state.muteState == nil {
|
||||
if let state = strongSelf.callState, state.muteState == nil || strongSelf.pushingToTalk {
|
||||
effectiveLevel = level
|
||||
}
|
||||
strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)])
|
||||
strongSelf.itemInteraction?.updateAudioLevels([(strongSelf.context.account.peerId, effectiveLevel)], accountPeerId: strongSelf.context.account.peerId, updateAll: false)
|
||||
strongSelf.actionButton.updateLevel(CGFloat(effectiveLevel))
|
||||
})
|
||||
|
||||
@ -726,21 +767,20 @@ public final class VoiceChatController: ViewController {
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData.withUpdated(theme: strongSelf.darkTheme), source: .extracted(VoiceChatContextExtractedContentSource(controller: controller, sourceNode: strongOptionsButton.extractedContainerNode, keepInPlace: true)), items: .single(items), reactionItems: [], gesture: gesture)
|
||||
strongSelf.controller?.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
let optionsButtonItem = UIBarButtonItem(customDisplayNode: self.optionsButton)!
|
||||
optionsButtonItem.target = self
|
||||
optionsButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||||
self.controller?.navigationItem.setRightBarButton(optionsButtonItem, animated: false)
|
||||
|
||||
self.closeButton.setImage(closeButtonImage(), for: [.normal])
|
||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.optionsButton.addTarget(self, action: #selector(self.optionsPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.peersDisposable?.dispose()
|
||||
self.peerViewDisposable?.dispose()
|
||||
self.leaveDisposable.dispose()
|
||||
self.isMutedDisposable?.dispose()
|
||||
self.callStateDisposable?.dispose()
|
||||
self.audioOutputStateDisposable?.dispose()
|
||||
self.memberStatesDisposable?.dispose()
|
||||
self.invitedPeersDisposable?.dispose()
|
||||
self.audioLevelsDisposable?.dispose()
|
||||
self.myAudioLevelDisposable?.dispose()
|
||||
}
|
||||
@ -748,10 +788,6 @@ public final class VoiceChatController: ViewController {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let titleView = VoiceChatControllerTitleView(theme: self.presentationData.theme)
|
||||
titleView.set(title: self.presentationData.strings.VoiceChat_Title, subtitle: self.presentationData.strings.SocksProxySetup_ProxyStatusConnecting)
|
||||
self.controller?.navigationItem.titleView = titleView
|
||||
|
||||
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.actionButtonPressGesture(_:)))
|
||||
longTapRecognizer.minimumPressDuration = 0.001
|
||||
longTapRecognizer.delegate = self
|
||||
@ -767,13 +803,19 @@ public final class VoiceChatController: ViewController {
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
@objc private func rightNavigationButtonAction() {
|
||||
@objc private func optionsPressed() {
|
||||
if self.optionsButton.isUserInteractionEnabled {
|
||||
self.optionsButton.contextAction?(self.optionsButton.containerNode, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
self.controller?.dismiss()
|
||||
}
|
||||
|
||||
@objc private func leavePressed() {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
|
||||
self.leaveDisposable.set((self.call.leave(terminateIfPossible: false)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||
self?.controller?.dismiss()
|
||||
@ -812,7 +854,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), invitedPeers: self.currentInvitedPeers ?? Set())
|
||||
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
|
||||
case .ended, .cancelled:
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
|
||||
@ -827,7 +869,7 @@ public final class VoiceChatController: ViewController {
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set(), invitedPeers: self.currentInvitedPeers ?? Set())
|
||||
self.updateMembers(muteState: self.effectiveMuteState, groupMembers: self.currentGroupMembers ?? [], callMembers: self.currentCallMembers ?? [], speakingPeers: self.currentSpeakingPeers ?? Set())
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -844,6 +886,8 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
|
||||
@objc private func audioOutputPressed() {
|
||||
self.hapticFeedback.impact(.veryLight)
|
||||
|
||||
guard let (availableOutputs, currentOutput) = self.audioOutputState else {
|
||||
return
|
||||
}
|
||||
@ -909,15 +953,23 @@ public final class VoiceChatController: ViewController {
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
transition.updateFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: CGSize(width: layout.size.width, height: 44.0)))
|
||||
transition.updateFrame(node: self.optionsButton, frame: CGRect(origin: CGPoint(x: 20.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 20.0 - 28.0, y: 18.0), size: CGSize(width: 28.0, height: 28.0)))
|
||||
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let contentHeight: CGFloat = layout.size.height - 240.0
|
||||
|
||||
transition.updateFrame(node: self.contentContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - contentHeight), size: CGSize(width: layout.size.width, height: contentHeight + 1000.0)))
|
||||
|
||||
let bottomAreaHeight: CGFloat = 290.0
|
||||
let listOrigin = CGPoint(x: 16.0, y: 64.0)
|
||||
|
||||
let listOrigin = CGPoint(x: 16.0, y: navigationHeight + 10.0)
|
||||
|
||||
var listHeight: CGFloat = 56.0
|
||||
var listHeight: CGFloat = 44.0 + 56.0
|
||||
if let maxListHeight = self.maxListHeight {
|
||||
listHeight = min(max(1.0, layout.size.height - bottomAreaHeight - listOrigin.y - layout.intrinsicInsets.bottom + 25.0), maxListHeight)
|
||||
listHeight = min(max(1.0, contentHeight - bottomAreaHeight - listOrigin.y - layout.intrinsicInsets.bottom + 25.0), maxListHeight + 44.0)
|
||||
}
|
||||
|
||||
let listFrame = CGRect(origin: listOrigin, size: CGSize(width: layout.size.width - 16.0 * 2.0, height: listHeight))
|
||||
@ -931,9 +983,9 @@ public final class VoiceChatController: ViewController {
|
||||
let sideButtonSize = CGSize(width: 60.0, height: 60.0)
|
||||
let centralButtonSize = CGSize(width: 300.0, height: 300.0)
|
||||
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: layout.size.height - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - centralButtonSize.width) / 2.0), y: contentHeight - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - centralButtonSize.height) / 2.0)), size: centralButtonSize)
|
||||
|
||||
let actionButtonState: VoiceChatActionButtonState
|
||||
let actionButtonState: VoiceChatActionButton.State
|
||||
let actionButtonTitle: String
|
||||
let actionButtonSubtitle: String
|
||||
let audioButtonAppearance: CallControllerButtonItemNode.Content.Appearance
|
||||
@ -1014,7 +1066,7 @@ public final class VoiceChatController: ViewController {
|
||||
soundImage = .speaker
|
||||
case .speaker:
|
||||
soundImage = .speaker
|
||||
soundAppearance = .blurred(isFilled: true)
|
||||
// soundAppearance = .blurred(isFilled: true)
|
||||
case .headphones:
|
||||
soundImage = .bluetooth
|
||||
case let .bluetooth(type):
|
||||
@ -1028,7 +1080,7 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .animated(duration: 0.4, curve: .linear))
|
||||
self.audioOutputNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: soundAppearance, image: soundImage), text: self.presentationData.strings.VoiceChat_Audio, transition: .animated(duration: 0.3, curve: .linear))
|
||||
|
||||
self.leaveNode.update(size: sideButtonSize, content: CallControllerButtonItemNode.Content(appearance: .color(.custom(0x4d120e)), image: .end), text: self.presentationData.strings.VoiceChat_Leave, transition: .immediate)
|
||||
|
||||
@ -1036,8 +1088,8 @@ public final class VoiceChatController: ViewController {
|
||||
let sideButtonOffset = min(36.0, floor((((layout.size.width - 144.0) / 2.0) - sideButtonSize.width) / 2.0))
|
||||
let sideButtonOrigin = max(sideButtonMinimalInset, floor((layout.size.width - 144.0) / 2.0) - sideButtonOffset - sideButtonSize.width)
|
||||
|
||||
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonOrigin, y: layout.size.height - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonOrigin - sideButtonSize.width, y: layout.size.height - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
transition.updateFrame(node: self.audioOutputNode, frame: CGRect(origin: CGPoint(x: sideButtonOrigin, y: contentHeight - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
transition.updateFrame(node: self.leaveNode, frame: CGRect(origin: CGPoint(x: layout.size.width - sideButtonOrigin - sideButtonSize.width, y: contentHeight - bottomAreaHeight - layout.intrinsicInsets.bottom + floor((bottomAreaHeight - sideButtonSize.height) / 2.0)), size: sideButtonSize))
|
||||
|
||||
if isFirstTime {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
@ -1046,116 +1098,48 @@ public final class VoiceChatController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(sourcePanel: ASDisplayNode?) {
|
||||
self.alpha = 1.0
|
||||
|
||||
func animateIn() {
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
if let sourcePanel = sourcePanel as? GroupCallNavigationAccessoryPanel {
|
||||
let sourceFrame = sourcePanel.view.convert(sourcePanel.bounds, to: self.view)
|
||||
self.contentContainer.clipsToBounds = true
|
||||
self.isHidden = false
|
||||
|
||||
let duration: Double = 0.4
|
||||
if let titleView = self.controller?.navigationItem.titleView as? VoiceChatControllerTitleView {
|
||||
titleView.animateIn(duration: duration)
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
|
||||
self.controller?.navigationBar?.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
|
||||
if let panelTitleView = sourcePanel.titleNode.view.snapshotContentTree() {
|
||||
let frame = sourcePanel.titleNode.view.convert(sourcePanel.titleNode.bounds, to: self.view)
|
||||
panelTitleView.frame = frame
|
||||
self.view.addSubview(panelTitleView)
|
||||
|
||||
panelTitleView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -49.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
panelTitleView.layer.animateScale(from: 1.0, to: 1.13, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
panelTitleView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak panelTitleView] _ in
|
||||
panelTitleView?.removeFromSuperview()
|
||||
let offset: CGFloat = layout.size.height - 240.0
|
||||
self.contentContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true, completion: { _ in
|
||||
})
|
||||
}
|
||||
if let panelTextView = sourcePanel.textNode.view.snapshotContentTree() {
|
||||
let frame = sourcePanel.textNode.view.convert(sourcePanel.textNode.bounds, to: self.view)
|
||||
panelTextView.frame = frame
|
||||
self.view.addSubview(panelTextView)
|
||||
|
||||
panelTextView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -49.0), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
|
||||
panelTextView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak panelTextView] _ in
|
||||
panelTextView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let (backgroundView, foregroundView) = sourcePanel.rightButtonSnapshotViews(), self.optionsButton.isUserInteractionEnabled {
|
||||
self.view.addSubview(backgroundView)
|
||||
self.view.addSubview(foregroundView)
|
||||
|
||||
self.optionsButton.isHidden = true
|
||||
let optionsFrame = self.optionsButton.view.convert(self.optionsButton.bounds, to: self.view)
|
||||
|
||||
let dotsView = UIImageView(image: optionsButtonImage())
|
||||
dotsView.center = foregroundView.center
|
||||
self.view.addSubview(dotsView)
|
||||
|
||||
backgroundView.layer.animateBounds(from: backgroundView.bounds, to: CGRect(origin: CGPoint(), size: CGSize(width: backgroundView.bounds.height, height: backgroundView.bounds.height)), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
backgroundView.layer.animatePosition(from: backgroundView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
backgroundView.layer.animateScale(from: 1.0, to: optionsFrame.height / backgroundView.frame.height, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
foregroundView.layer.animatePosition(from: foregroundView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
backgroundView.layer.animate(from: backgroundView.backgroundColor!.cgColor, to: UIColor(rgb: 0x1c1c1e).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration - 0.1, removeOnCompletion: false)
|
||||
foregroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration - 0.1, removeOnCompletion: false, completion: { [weak self, weak foregroundView, weak backgroundView, weak dotsView] _ in
|
||||
backgroundView?.removeFromSuperview()
|
||||
foregroundView?.removeFromSuperview()
|
||||
dotsView?.removeFromSuperview()
|
||||
|
||||
self?.optionsButton.isHidden = false
|
||||
})
|
||||
|
||||
foregroundView.layer.animateScale(from: 1.0, to: 0.3, duration: duration - 0.1, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
dotsView.layer.animatePosition(from: dotsView.center, to: CGPoint(x: optionsFrame.midX, y: optionsFrame.midY), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
dotsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration - 0.1, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
self.contentContainer.layer.animateFrame(from: sourceFrame, to: self.contentContainer.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.contentContainer.layer.animate(from: 0.0 as NSNumber, to: layout.deviceMetrics.screenCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: duration, removeOnCompletion: true, completion: { [weak self] value in
|
||||
if value {
|
||||
self?.contentContainer.clipsToBounds = false
|
||||
}
|
||||
})
|
||||
|
||||
self.contentContainer.layer.animate(from: self.presentationData.theme.rootController.navigationBar.backgroundColor.cgColor, to: UIColor.black.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration - 0.25)
|
||||
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
} else {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.actionButton.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.audioOutputNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.leaveNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.actionButton.titleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.actionButton.subtitleLabel.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.audioOutputNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.leaveNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
self.contentContainer.layer.animateBoundsOriginYAdditive(from: 80.0, to: 0.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)?) {
|
||||
self.alpha = 0.0
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak self] _ in
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
var dimCompleted = false
|
||||
var offsetCompleted = false
|
||||
|
||||
let internalCompletion: () -> Void = { [weak self] in
|
||||
if dimCompleted && offsetCompleted {
|
||||
if let strongSelf = self {
|
||||
strongSelf.dimNode.layer.removeAllAnimations()
|
||||
strongSelf.contentContainer.layer.removeAllAnimations()
|
||||
}
|
||||
completion?()
|
||||
self?.layer.allowsGroupOpacity = false
|
||||
}
|
||||
}
|
||||
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
dimCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
|
||||
let offset: CGFloat = layout.size.height - 240.0
|
||||
self.contentContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
offsetCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
self.contentContainer.layer.animateScale(from: 1.0, to: 1.04, duration: 0.3)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: ListTransition) {
|
||||
@ -1197,7 +1181,7 @@ public final class VoiceChatController: ViewController {
|
||||
itemHeight = node.frame.height
|
||||
}
|
||||
}
|
||||
strongSelf.maxListHeight = CGFloat(transition.count) * itemHeight
|
||||
strongSelf.maxListHeight = CGFloat(transition.count - 1) * itemHeight
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
|
||||
}
|
||||
@ -1205,31 +1189,8 @@ public final class VoiceChatController: ViewController {
|
||||
})
|
||||
}
|
||||
|
||||
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], speakingPeers: Set<PeerId>, invitedPeers: Set<PeerId>) {
|
||||
var groupMembers = groupMembers
|
||||
groupMembers.sort(by: { lhs, rhs in
|
||||
if lhs.peer.id == self.context.account.peerId {
|
||||
return true
|
||||
} else if rhs.peer.id == self.context.account.peerId {
|
||||
return false
|
||||
}
|
||||
|
||||
let lhsPresence = lhs.presences[lhs.peer.id]
|
||||
let rhsPresence = lhs.presences[lhs.peer.id]
|
||||
|
||||
if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence {
|
||||
return lhsPresence.status > rhsPresence.status
|
||||
} else if let _ = lhsPresence as? TelegramUserPresence {
|
||||
return true
|
||||
} else if let _ = rhsPresence as? TelegramUserPresence {
|
||||
return false
|
||||
}
|
||||
|
||||
return lhs.peer.id < rhs.peer.id
|
||||
})
|
||||
|
||||
private func updateMembers(muteState: GroupCallParticipantsContext.Participant.MuteState?, groupMembers: [RenderedChannelParticipant], callMembers: [GroupCallParticipantsContext.Participant], speakingPeers: Set<PeerId>) {
|
||||
var callMembers = callMembers
|
||||
|
||||
callMembers.sort()
|
||||
|
||||
for i in 0 ..< callMembers.count {
|
||||
@ -1244,15 +1205,16 @@ public final class VoiceChatController: ViewController {
|
||||
self.currentGroupMembers = groupMembers
|
||||
self.currentCallMembers = callMembers
|
||||
self.currentSpeakingPeers = speakingPeers
|
||||
self.currentInvitedPeers = invitedPeers
|
||||
|
||||
let previousEntries = self.currentEntries
|
||||
var entries: [PeerEntry] = []
|
||||
var entries: [ListEntry] = []
|
||||
|
||||
var index: Int32 = 0
|
||||
|
||||
var processedPeerIds = Set<PeerId>()
|
||||
|
||||
entries.append(.invite(self.presentationData.theme, self.presentationData.strings, "Invite Member"))
|
||||
|
||||
for member in callMembers {
|
||||
if processedPeerIds.contains(member.peer.id) {
|
||||
continue
|
||||
@ -1273,48 +1235,24 @@ public final class VoiceChatController: ViewController {
|
||||
memberMuteState = member.muteState
|
||||
}
|
||||
|
||||
entries.append(PeerEntry(
|
||||
entries.append(.peer(PeerEntry(
|
||||
peer: member.peer,
|
||||
presence: nil,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
invited: false
|
||||
))
|
||||
muteState: memberMuteState
|
||||
)))
|
||||
index += 1
|
||||
}
|
||||
|
||||
for member in groupMembers {
|
||||
if processedPeerIds.contains(member.peer.id) {
|
||||
continue
|
||||
}
|
||||
processedPeerIds.insert(member.peer.id)
|
||||
|
||||
if let user = member.peer as? TelegramUser, user.botInfo != nil || user.isDeleted {
|
||||
continue
|
||||
}
|
||||
|
||||
let memberState: PeerEntry.State
|
||||
var memberMuteState: GroupCallParticipantsContext.Participant.MuteState?
|
||||
if member.peer.id == self.context.account.peerId {
|
||||
if muteState == nil {
|
||||
memberState = .speaking
|
||||
} else {
|
||||
memberState = .listening
|
||||
}
|
||||
} else {
|
||||
memberState = .inactive
|
||||
}
|
||||
|
||||
entries.append(PeerEntry(
|
||||
peer: member.peer,
|
||||
presence: member.presences[member.peer.id] as? TelegramUserPresence,
|
||||
if callMembers.isEmpty, let accountPeer = self.accountPeer {
|
||||
entries.append(.peer(PeerEntry(
|
||||
peer: accountPeer,
|
||||
presence: nil,
|
||||
activityTimestamp: Int32.max - 1 - index,
|
||||
state: memberState,
|
||||
muteState: memberMuteState,
|
||||
invited: invitedPeers.contains(member.peer.id)
|
||||
))
|
||||
index += 1
|
||||
state: .listening,
|
||||
muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true)
|
||||
)))
|
||||
}
|
||||
|
||||
self.currentEntries = entries
|
||||
@ -1327,46 +1265,30 @@ public final class VoiceChatController: ViewController {
|
||||
@objc private func panGesture(_ recognizer: CallPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
guard let (layout, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
self.contentContainer.clipsToBounds = true
|
||||
self.contentContainer.cornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
case .changed:
|
||||
let offset = recognizer.translation(in: self.view).y
|
||||
var bounds = self.bounds
|
||||
var bounds = self.contentContainer.bounds
|
||||
bounds.origin.y = -offset
|
||||
|
||||
let transition = offset / bounds.height
|
||||
if transition > 0.02 {
|
||||
self.controller?.statusBar.statusBarStyle = .Ignore
|
||||
} else {
|
||||
self.controller?.statusBar.statusBarStyle = .White
|
||||
}
|
||||
self.bounds = bounds
|
||||
self.contentContainer.bounds = bounds
|
||||
case .cancelled, .ended:
|
||||
let velocity = recognizer.velocity(in: self.view).y
|
||||
if abs(velocity) < 200.0 {
|
||||
var bounds = self.bounds
|
||||
var bounds = self.contentContainer.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint()
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.contentContainer.cornerRadius = 0.0
|
||||
})
|
||||
self.controller?.statusBar.statusBarStyle = .White
|
||||
self.contentContainer.bounds = bounds
|
||||
self.contentContainer.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
} else {
|
||||
var bounds = self.bounds
|
||||
var bounds = self.contentContainer.bounds
|
||||
let previous = bounds
|
||||
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
|
||||
self.bounds = bounds
|
||||
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self.contentContainer.bounds = bounds
|
||||
self.contentContainer.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
|
||||
self?.controller?.dismissInteractively()
|
||||
var initialBounds = bounds
|
||||
initialBounds.origin = CGPoint()
|
||||
self?.bounds = initialBounds
|
||||
self?.controller?.statusBar.statusBarStyle = .White
|
||||
self?.contentContainer.cornerRadius = 0.0
|
||||
self?.contentContainer.bounds = initialBounds
|
||||
})
|
||||
}
|
||||
default:
|
||||
@ -1379,8 +1301,6 @@ public final class VoiceChatController: ViewController {
|
||||
public let call: PresentationGroupCall
|
||||
private let presentationData: PresentationData
|
||||
|
||||
public weak var sourcePanel: ASDisplayNode?
|
||||
|
||||
fileprivate let contentsReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
fileprivate let dataReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
private let _ready = Promise<Bool>(false)
|
||||
@ -1395,21 +1315,18 @@ public final class VoiceChatController: ViewController {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
private let idleTimerExtensionDisposable = MetaDisposable()
|
||||
|
||||
public init(sharedContext: SharedAccountContext, accountContext: AccountContext, call: PresentationGroupCall) {
|
||||
self.sharedContext = sharedContext
|
||||
self.call = call
|
||||
self.presentationData = sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let darkNavigationTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: UIColor(rgb: 0x525252), primaryTextColor: .white, backgroundColor: .clear, separatorColor: UIColor(white: 0.0, alpha: 0.8), badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.VoiceChat_BackTitle, target: self, action: #selector(self.closePressed))
|
||||
self.navigationItem.leftBarButtonItem = backItem
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self._ready.set(combineLatest([
|
||||
self.contentsReady.get(),
|
||||
@ -1430,8 +1347,8 @@ public final class VoiceChatController: ViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func closePressed() {
|
||||
self.dismiss()
|
||||
deinit {
|
||||
self.idleTimerExtensionDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
@ -1448,11 +1365,18 @@ public final class VoiceChatController: ViewController {
|
||||
if !self.didAppearOnce {
|
||||
self.didAppearOnce = true
|
||||
|
||||
self.controllerNode.animateIn(sourcePanel: self.sourcePanel)
|
||||
self.sourcePanel = nil
|
||||
self.controllerNode.animateIn()
|
||||
|
||||
self.idleTimerExtensionDisposable.set(self.sharedContext.applicationBindings.pushIdleTimerExtension())
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.idleTimerExtensionDisposable.set(nil)
|
||||
}
|
||||
|
||||
func dismissInteractively(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
|
@ -14,14 +14,35 @@ func optionsButtonImage() -> UIImage? {
|
||||
})
|
||||
}
|
||||
|
||||
final class VoiceChatOptionsButton: ASDisplayNode {
|
||||
func closeButtonImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x1c1c1e).cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setLineWidth(2.0)
|
||||
context.setLineCap(.round)
|
||||
context.setStrokeColor(UIColor.white.cgColor)
|
||||
|
||||
context.move(to: CGPoint(x: 9.0, y: 9.0))
|
||||
context.addLine(to: CGPoint(x: 19.0, y: 19.0))
|
||||
context.strokePath()
|
||||
|
||||
context.move(to: CGPoint(x: 19.0, y: 9.0))
|
||||
context.addLine(to: CGPoint(x: 9.0, y: 19.0))
|
||||
context.strokePath()
|
||||
})
|
||||
}
|
||||
|
||||
final class VoiceChatOptionsButton: HighlightableButtonNode {
|
||||
let extractedContainerNode: ContextExtractedContentContainingNode
|
||||
let containerNode: ContextControllerSourceNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
override init() {
|
||||
init() {
|
||||
self.extractedContainerNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.containerNode.isGestureEnabled = false
|
||||
|
@ -90,7 +90,7 @@ public final class VoiceChatParticipantItem: ListViewItem {
|
||||
self.contextAction = contextAction
|
||||
}
|
||||
|
||||
public var selectable: Bool = false
|
||||
public var selectable: Bool = true
|
||||
|
||||
public 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 {
|
||||
@ -171,10 +171,6 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
private var peerPresenceManager: PeerPresenceStatusManager?
|
||||
private var layoutParams: (VoiceChatParticipantItem, ListViewItemLayoutParams, Bool, Bool)?
|
||||
|
||||
override public var canBeSelected: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
@ -586,13 +582,13 @@ public class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode {
|
||||
let avatarScale: CGFloat
|
||||
if value > 0.0 {
|
||||
audioLevelView.startAnimating()
|
||||
avatarScale = 1.03 + level * 0.1
|
||||
avatarScale = 1.03 + level * 0.07
|
||||
} else {
|
||||
audioLevelView.stopAnimating(duration: 0.5)
|
||||
avatarScale = 1.0
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .spring)
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.2, curve: .easeInOut)
|
||||
transition.updateTransformScale(node: strongSelf.avatarNode, scale: avatarScale, beginWithCurrentState: true)
|
||||
}
|
||||
}))
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -640,8 +640,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
if let call = call {
|
||||
mainWindow.hostView.containerView.endEditing(true)
|
||||
let groupCallController = VoiceChatController(sharedContext: strongSelf, accountContext: call.accountContext, call: call)
|
||||
groupCallController.sourcePanel = call.sourcePanel
|
||||
call.sourcePanel = nil
|
||||
strongSelf.groupCallController = groupCallController
|
||||
strongSelf.mainWindow?.present(groupCallController, on: .calls)
|
||||
strongSelf.hasOngoingCall.set(true)
|
||||
@ -987,7 +985,6 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
}
|
||||
} else if let groupCallController = self.groupCallController {
|
||||
if groupCallController.isNodeLoaded && groupCallController.view.superview == nil {
|
||||
groupCallController.sourcePanel = sourcePanel
|
||||
mainWindow.hostView.containerView.endEditing(true)
|
||||
mainWindow.present(groupCallController, on: .calls)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user