Voice Chat UI improvements

This commit is contained in:
Ilya Laktyushin 2020-12-02 07:49:51 +04:00
parent 811f6981ca
commit 7d3353c01b
12 changed files with 5906 additions and 5657 deletions

View File

@ -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$@?";

View File

@ -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))
}
}

View File

@ -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

View File

@ -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

View 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
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}))

View File

@ -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)
}